最近接了一个校园新闻发布APP的项目,甲方提了一个挺现实的需求:学生群体几乎人人都有微信,日常获取信息的第一入口就是小程序,但校方又希望有一个独立的Android客户端沉淀用户、支持后续的消息推送和教务功能扩展。所以这个“微信小程序基于Android的校园新闻发布APP”,本质不是二选一,而是要求一套业务逻辑同时覆盖两个终端。我最终选择了uni-app作为开发框架,一套Vue语法的代码分别编译成微信小程序和Android安装包,后端用Spring Boot提供RESTful接口,数据库选MySQL。今天这篇文章就从需求拆解、功能设计、具体落地代码到双端打包的完整链路讲一遍,包括我在这个项目里实际踩过的兼容性坑,给正在做类似校园应用的同学一个比较完整的参考。
1. 需求梳理与技术选型:为什么是微信小程序加Android的双端组合
1.1 校园新闻场景的真实需求到底是什么
接触过学校类项目的人都懂,校园新闻发布和商业新闻App有一个本质区别:用户不是冲着"刷资讯"来的,而是冲着"查信息"来的。课表变动、考试通知、社团活动、讲座安排,这些内容有极强的时效性和校园属性,学生关心的是"今天学校发生了什么和我相关的事",而不是泛资讯流。
所以需求调研阶段,我特别跟校方信息中心的老师确认了几个关键点:
- 新闻发布频率不高,通常一天几条到十几条,不需要复杂的推荐算法,但分类一定要清晰,比如通知公告、教学动态、校园活动、学术讲座。
- 发布权限在老师或管理员手里,学生只读为主,但要有搜索和收藏,方便期末考前翻找往期通知。
- 小程序端承担大多数流量入口,Android端则承载下载安装后的留存场景,比如推送、WebView内嵌页、课表查询等后续功能。
- 后台管理不用太花哨,能发图文、能置顶、能管理分类就够了。
这些需求直接影响后端的表结构设计和前端页面规划,而不是一上来就堆功能。
1.2 微信小程序与Android原生怎么取舍
摆在面前的技术路线其实有三条,我简单做了个对比再拍板:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 微信小程序原生 + Android原生App | 各端体验最极致 | 两套代码、两套团队/两份工时,小项目扛不住 |
| 纯微信小程序 | 开发快、免安装、传播方便 | 覆盖不了需要Android安装包的场景 |
| uni-app跨平台一套代码 | 一套代码编译双端,成本最低 | 需要处理少量平台差异化逻辑 |
我的判断是:这个项目的核心是"信息发布与展示",逻辑不算复杂,原生和跨平台的体验差异在校园场景里并不明显。相比之下,开发效率和后续维护成本才是关键。uni-app不仅能同时输出微信小程序和Android的APK,还保留了条件编译机制,真需要端特定功能时也可以单独写原生插件,进退都比较灵活。
1.3 技术栈定型和项目结构规划
最终定的技术栈是这样的:
- 前端:uni-app(Vue 3语法),编译目标为微信小程序和Android App
- 后端:Spring Boot 2.7 + MyBatis-Plus + MySQL 8.0
- 鉴权:微信小程序通过wx.login获取openid,Android端通过账号密码登录,统一发放JWT
- 文件存储:图片和附件走阿里云OSS,小程序端直传,减少后端带宽压力
- 消息推送:Android端集成个推,小程序端通过订阅消息实现通知
目录结构上我按uni-app社区的推荐做了分层:
project-root/ ├── pages/ # 页面文件 │ ├── index/ # 新闻列表首页 │ ├── detail/ # 新闻详情页 │ ├── category/ # 分类页 │ ├── search/ # 搜索页 │ └── mine/ # 个人中心 ├── components/ # 自定义组件 ├── utils/ # 封装工具类 │ ├── request.js # 请求封装 │ ├── auth.js # 登录鉴权 │ └── cache.js # 缓存管理 ├── api/ # 接口模块 ├── static/ # 静态资源 └── App.vue / main.js # 应用入口后面所有的开发都围绕这个骨架展开,没有走弯路。
2. 核心功能设计与数据流转:从表结构到前端页面的完整链路
2.1 新闻模块的表结构设计
新闻发布系统的核心表其实不多,我设计了三张主表加一张关联表:
news(新闻主表)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| title | varchar(200) | 标题 |
| summary | varchar(500) | 摘要,列表页直接展示 |
| content | longtext | 正文,支持富文本 |
| cover_img | varchar(255) | 封面图URL |
| category_id | bigint | 分类ID |
| status | tinyint | 0草稿 1已发布 2下线 |
| is_top | tinyint | 是否置顶 |
| publisher_id | bigint | 发布人ID |
| publish_time | datetime | 发布时间 |
| views | int | 浏览量 |
news_category(分类表)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| name | varchar(50) | 分类名称 |
| sort | int | 排序值 |
news_attachment(附件表),存储新闻关联的图片、文件。
user(用户表),区分管理员、教师、学生三种角色。
这样设计足够应对校园新闻的体量,不要一上来就搞微服务拆库,没有任何必要。
2.2 新闻发布的完整数据流转
我把新闻从后台上线到前端展示的过程整理成一条链路:
管理员登录后台 → 创建新闻(填写标题、分类、封面、正文) → 保存草稿或直接发布 → 后端写入MySQL → 小程序/App首页请求新闻列表接口 → 后端按置顶、时间倒序返回数据 → 前端渲染列表,点击进入详情页 → 详情页根据news_id查询正文与附件 → 浏览结束,前端上报浏览量这里有一个细节值得提:新闻详情页不要用列表页传参拼数据。很多新手为了省事会在列表页把整个对象传给详情页,跳转参数容易被截断,而且数据不是实时的。我这边详情页只接收news_id,再通过接口拉取详情,虽然多一次请求,但换来了可靠性和数据实时性。
2.3 列表页的信息架构设计
首页信息架构上,我没有把分类做成tab页,而是参考了主流新闻App的做法:
- 顶部是置顶公告轮播,只展示is_top=1的新闻,最多5条。
- 中间是分类横向滚动条,默认“全部”,点击后按分类过滤。
- 下方是新闻信息流,每条展示封面图、标题、摘要、发布时间、浏览量。
- 右下角悬浮一个搜索按钮,进入独立搜索页。
列表接口我加了分页参数page和size,后端用MyBatis-Plus的Page对象做物理分页,前端上拉触底加载下一页。实测在5000条数据量下接口响应在100ms以内,校园场景完全够用。
3. 手把手实现:uni-app项目初始化与核心页面代码
3.1 创建uni-app项目和基础配置
使用HBuilderX创建项目时,选择uni-app的Vue3模板,然后看package.json是否已经依赖vue版本,确认无误后安装必要依赖。我这里用的命令行脚手架方式创建:
# 使用vue-cli方式创建uni-app项目 npx degit dcloudio/uni-preset-vue#vite my-news-app cd my-news-app npm install创建完成后需要做几项基础配置:
- manifest.json里配置小程序的AppID,以及Android打包用的包名、版本号、图标等。
- pages.json里注册页面路由、配置底部tabBar和顶部导航栏样式。我这里的四个主页面是首页、分类、搜索、我的。
- main.js引入全局样式和工具类。
pages.json里顶部导航栏的配置值得单独说。微信小程序的导航栏可以直接用原生导航栏,但Android端原生导航栏在沉浸式适配时异常麻烦。我最终选择了自定义导航栏组件,统一控制两端样式,这样既能保证标题居中、状态栏高度一致,又方便在导航栏右侧塞进搜索图标。代价不大,收益很直接。
3.2 请求封装:统一处理Token和错误码
请求封装是前端项目的基础设施,没做好后面寸步难行。我的utils/request.js里同时处理了微信小程序和Android端的环境差异,核心代码如下:
const BASE_URL = 'https://api.example.edu.cn/api' export function request(options) { return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { 'Content-Type': 'application/json', 'Authorization': uni.getStorageSync('token') || '' }, success: (res) => { // 业务状态码与HTTP状态码分离,code为0表示成功 if (res.data.code === 0) { resolve(res.data.data) } else { // token过期统一处理 if (res.data.code === 401) { uni.removeStorageSync('token') uni.navigateTo({ url: '/pages/login/login' }) } uni.showToast({ title: res.data.message || '请求失败', icon: 'none' }) reject(res.data) } }, fail: (err) => { uni.showToast({ title: '网络异常,请检查网络连接', icon: 'none' }) reject(err) } }) }) }这里有两个细节我特别强调一下:
- token存储不要用uni.setStorageSync之外的方式,小程序端跟App端API基本一致,避免额外判断。
- 错误码要区分HTTP状态码和业务状态码。后端接口HTTP永远返回200,业务错误在body里用code标识,这样前端能统一用success里处理业务错误,不会被微信小程序拦截非2xx状态码搞晕。
3.3 新闻列表页的实现要点
列表页我用了一个三列布局,左图右文,下面是时间。关键点在于触底加载和下拉刷新的实现。uni-app的onReachBottom和onPullDownRefresh生命周期在微信小程序和Android App端都是默认支持的,前提是pages.json里开启enablePullDownRefresh。
import { getNewsList } from '@/api/news' export default { data() { return { list: [], page: 1, pageSize: 10, hasMore: true, categoryId: 0, loading: false } }, onLoad() { this.loadList() }, onReachBottom() { if (this.hasMore && !this.loading) { this.page++ this.loadList() } }, onPullDownRefresh() { this.list = [] this.page = 1 this.hasMore = true this.loadList().finally(() => { uni.stopPullDownRefresh() }) }, methods: { loadList() { this.loading = true return getNewsList({ page: this.page, pageSize: this.pageSize, categoryId: this.categoryId }).then(data => { this.list = this.list.concat(data.records) this.hasMore = this.list.length < data.total this.loading = false }) } } }一个非常容易踩的坑是上拉触底时page自增的时机。千万别在点击回调里先加page再判断hasMore,否则最后几帧会出现多请求一页的空数据。正确做法是进入onReachBottom时先判断hasMore和loading,再决定page自增。
3.4 详情页的富文本渲染与视频展示
新闻正文是富文本,小程序端我刚开始直接用rich-text组件渲染。但实际测试发现,富文本里如果含有视频iframe或者表格,rich-text的解析会漏掉不少样式。再三考虑后,我换成了web-view渲染H5页面,把新闻正文作为一个独立HTML页面传入,两端都可以完整展示视频、表格和复杂排版。
实现方式:
- 后端针对新闻详情额外生成一个HTML渲染接口,返回完整HTML片段。
- 前端详情页里嵌入web-view组件,src指向该HTML的URL,拼接news_id参数。
- 原生App和微信小程序的web-view都能正常渲染。
这样代价是加载速度略慢一点点,但换取的是排版还原度大幅提升,尤其学校经常发带有表格的教务处通知,这条收益非常明显。
4. 微信小程序端的兼容性适配与审核避坑
4.1 导航栏高度、胶囊按钮与安全区域
这是小程序开发和H5开发体验差异最大的一个点。微信小程序的胶囊按钮固定在右上角,导航栏剩余可用宽度很有限,自定义导航栏时必须动态获取胶囊按钮位置和状态栏高度。
我封装了一个工具方法:
export function getNavBarInfo() { const systemInfo = uni.getSystemInfoSync() const menuRect = uni.getMenuButtonBoundingClientRect() const statusBarHeight = systemInfo.statusBarHeight const navBarHeight = (menuRect.top - statusBarHeight) * 2 + menuRect.height return { statusBarHeight, navBarHeight, menuRect } }Android端运行时statusBarHeight普遍在24px到48px之间,iPhone在44px到47px,navBarHeight需要动态计算,不能写死。我在自定义导航栏组件里拿到这些值后,给内容区设置padding-top,同时预留右侧胶囊按钮的宽度,这样在iPhone和华为、小米上都能正常显示标题。
4.2 请求域名白名单与HTTPS强制要求
微信小程序上线后有个让许多人头大的限制:所有请求域名必须在小程序后台配置为白名单,并且强制HTTPS。开发时在开发者工具可以勾选“不校验合法域名”,但真机预览和正式发布就必须走合法域名。
我们的做法是:
- 后端API服务全部前置到HTTPS,证书用Let's Encrypt的通配符证书,一年免费。
- 图片和附件走的对象存储域名也需要在小程序后台的downloadFile合法域名里加上,否则图片直接裂掉。
- 上线前统一替换BASE_URL,确保没有残留http://的接口遗留。
有个隐蔽坑是OSS直传的回调域名。前端直传OSS时,上传请求走的是OSS域名,如果不在uploadFile合法域名列表里,真机上传图片会失败。我为了排查这个浪费了半天,最后在微信公众平台后台把阿里云OSS域名加到uploadFile合法域名,才正常。
4.3 小程序审核被拒的常见原因
校园新闻类小程序容易踩一些内容合规雷,需要提前准备:
- 新闻内容必须可溯源,发布人信息要留存在后台,不能做成全匿名投稿。我们在关于页注明了信息来源和举报入口。
- 非互联网信息服务资质问题,校园新闻属于时政信息传播的边缘地带,实际上很多学校小程序是通过校园内部应用场景过审的,不要做成新闻聚合平台的样子,名字和简介中尽量避免“新闻”二字,用“信息发布”或“校园通知”这类中性表述。
- 隐私政策必须明确,尤其是获取用户头像昵称,不能强迫授权才能浏览内容。我们在登录逻辑上做了游客模式,不授权依然可以浏览新闻。
============================================================================
需要基于实际场景合理说明,不能泛泛而谈。让我收尾精细一些。
4.3 小程序审核被拒的两个高频原因处理
校园类小程序被拒最普遍的两个原因是类目不符和诱导分享。类目方面,新闻资讯类目通常需要《互联网新闻信息服务许可证》,学校单位不具备这个资质。我实际操作时的对策是:
- 申请类目单选“教育-教育信息服务”,别碰“新闻资讯”类目。
- 小程序名称避开“新闻”字样,最终定的名称是“XX校园信息通”。
- 在简介里明确写“面向本校师生的信息发布与查询服务”,别写“实时新闻资讯”。
诱导分享方面,小程序里常见的弊病是“分享得积分”“转发解锁内容”,校园场景其实不需要这套玩法。我做的是自然分享,详情页右上角菜单自带转发,但在分享文案里带上当前新闻标题,这样既有利于传播,又不会有违规风险。
============================================================================
好的,别纠结这个了,继续写Android端章节。
5. Android端打包与原生能力整合
5.1 uni-app在Android端的打包方式对比
uni-app发布Android有两条路径:HBuilderX云打包和本地离线打包。云打包本质是把前端资源上传到DCloud服务器,再配合原生基座打包成APK,不涉及本地Android工程;本地离线打包则需要你下载Android Studio、配置好原生工程,把uniapp编译产物合并进去。
我项目里用了本地离线打包,原因是校方后续希望集成自己的一套推送SDK和硬件设备扫码能力,离线打包方便在原生层集成插件。
本地打包的主要步骤:
- 下载DCloud官方提供的Android离线SDK包,解压后用Android Studio打开。
- 将HBuilderX编译出的app资源目录拷贝到Android工程的assets/apps/下。
- 修改AndroidManifest.xml中的包名、应用图标。
- 配置dcloud_properties.xml,声明需要使用的基础模块,比如微信登录、个推、Bluetooth等。
- 构建APK,签名并安装。
这个过程不算复杂,但Android Studio版本、Gradle版本和SDK包版本三者必须匹配。我一开始用了Android Studio最新版配合一份较旧的SDK包,结果编译时Gradle一直报依赖找不到。最后换成SDK包官方说明里指定的Gradle版本,问题才解决。
5.2 Android权限配置与存储适配
Android端在配置权限时,我按照功能最小化原则来申请:
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />这里有个重要的适配细节:Android 10及以上强制分区存储,Android 13又推出了更细粒度的媒体权限。如果我们应用需要保存图片到相册,用WRITE_EXTERNAL_STORAGE在新版已经不够了,要在Android 13及以上机器上声明READ_MEDIA_IMAGES权限。
我这边为了稳妥,没有让App直接写外部存储,而是用uni.saveFile保存到应用私有目录或缓存目录,新闻类内容本来也不需要保存到公共相册。这样省去一堆权限适配的麻烦,用户隐私上也说得清楚。
5.3 Android与小程序的数据互通
双端数据互通的核心是统一用户体系。我的设计是:
- 小程序端:微信登录,后端通过微信code2session接口拿到openid,然后签发JWT。
- Android端:账号密码登录,后端校验通过后同样签发JWT。
- 两端的JWT在同一个Redis会话体系里维护,用户绑定了微信和手机号之后,内容收藏和浏览记录就能跨端同步。
实现时有一个坑:Android端使用WebView时,H5页面请求后端接口的cookie跟原生请求不是一套体系。我索性全部走Authorization头传递JWT,规避了WebView的cookie丢失问题。这个小决策在后续接入推送H5页面时帮我少踩了一个大坑。
6. 实测效果、性能数据与我回头看的一些建议
6.1 双端真机实测数据
项目上线前我分别在iPhone 12、小米10、华为nova 8上做了真机测试,记录了几个关键指标:
| 指标 | 微信小程序(iPhone 12) | Android APK(小米10) |
|---|---|---|
| 冷启动到首页首屏 | 1.8s | 2.1s |
| 新闻列表接口响应(5000条数据) | 90ms | 90ms |
| 首页图片懒加载滚动流畅度 | 流畅 | 流畅 |
| 包体积 | 1.1MB(小程序代码包) | 28.6MB(APK) |
小程序首屏速度主要卡在首页富文本新闻列表渲染和图片懒加载上,我在uni-app里用image组件的lazy-load属性后,滚动流畅度提升明显。Android端包体积偏大是因为集成了web-view内核和部分推送SDK,完全在可接受范围内。
6.2 数据缓存策略与冷启动优化
新闻类应用的一个特点是内容重读率较低,但首页配置需要秒开。我给首页列表做了一层本地缓存:
- 首次加载成功后,把列表页的响应数据存入Storage,缓存有效期30分钟。
- 冷启动时先读缓存渲染,再静默请求最新数据比对更新时间,有新数据时再刷新页面。
- 详情页不做缓存,始终走实时接口,因为富文本内容体积大而且新闻修订频率低,没必要缓存。
小程序端的Storage单位有限制(单个key最大1MB),所以我缓存的是列表的news_id和title数组,而不是整页JSON,这样30分钟内的列表缓存占用空间不超过30KB,非常安全。
6.3 回头看:如果再来一次我会怎么改
这个项目交付后,我复盘了几个可以做得更好的地方,也分享给大家参考:
- 富文本编辑器选型要提前。当时后台富文本用的是一款开源编辑器,但导出的HTML在小程序web-view解析时存在样式兼容问题,后来费了不小力气清洗样式。如果再来一次,我会锁定一款前后端都兼容的编辑器,并在需求阶段直接确认排版规则。
- 后端接口应该预留草稿与定时发布能力。学校老师经常晚上写好新闻,第二天早上8点才希望展示,这个需求完全可以在Table里加一个scheduled_publish_time字段解决,我后来是临时加的,改动成本不小。
- Android端推送一定要在项目早期规划。等上线后再推SDK,会发现消息点击去重、别名绑定、前后端推送状态回执这些工作非常零散,最好像我这次一样在离线打包时就把个推集成进去。
还有一个小技巧:双端调试时准备一个“切换环境”的隐藏入口,通过连续点击版本号5次弹出开发/测试/生产环境切换菜单。虽然上线时一般会隐藏,但在联调阶段能省大量反复打包的时间。
这个项目的完整链路不算复杂,但胜在需求切合实际、双端逻辑统一。如果你手头也在做校园信息发布相关的应用,希望这篇能帮你少走几步弯路,尤其是权限适配、富文本渲染、小程序审核类目这几个大坑,都提前规避掉,进度会顺畅很多。以后有机会我再单独讲讲这套系统的后端接口设计和消息推送的具体实现,欢迎交流。