Vue3+uni-app跨端开发报警小程序:定位、视频上传与隐私合规实战
去年接了一个项目,要做一个基于 Vue 3 和 uni-app 的互联网警报警小程序平台。用户在微信里打开小程序就能上报警情,系统自动带上定位,支持图文、短视频和语音补充现场信息,后台接警后完成调度、处置和进度反馈。整个项目从立项到上架,几乎把 uni-app 跨端开发能踩的坑都踩了一遍:路由传参、地图定位、视频播放兼容、隐私合规、支付对接、安卓/iOS 打包上架……这篇文章就以这个项目为主线,把关键技术和实战细节完整拆解一遍,给正在做或准备做跨端业务系统的朋友一个可参考的完整方案。
1. 项目定位与整体技术方案选型
1.1 报警类小程序的核心业务闭环
报警类小程序跟电商、内容类小程序有个本质区别:用户的操作时间极端,流量却可能突发。用户进来只关心三件事——我在哪、发生了什么、怎么把证据交上去。整个产品闭环围绕这三件事展开:打开小程序、一键报警或视频报警、自动定位、填写描述并上传现场图片/视频、提交工单、后台接警调度、用户随时查看处理进度。
技术侧随之拆成几个核心模块:定位采集、媒体上传、消息推送、进度查询,外加账号体系和安全鉴权。这决定了前端架构不能做得太重,页面跳转要短平快,所有核心操作尽量在 3 步以内完成。我见过不少同类项目把表单做得很长,用户报警还要填十几个字段,这在真实场景里是致命的。所以我们在前端把所有非必要字段后置,第一次提交只需要“位置 + 类型 + 一句描述”,其余信息进入后台后由接警员电话核实时再补充。
1.2 为什么选择 uni-app 而不是三端各写一套
需求一提出来就有一个硬性约束:微信小程序必须首发,Android 和 iOS App 也要在两个月内跟上;H5 作为一个轻量入口最好也能复用。三端各写一套人力完全不允许,原生小程序 + 原生 App 的组合也会让后续维护成本翻倍,所以跨端框架成为唯一选择。
我对比了当时主流的几个方案,核心考虑点如下:
| 维度 | uni-app | 原生微信小程序 | Flutter |
|---|---|---|---|
| 多端支持 | 小程序/App/H5 一套代码 | 仅微信端 | App 为主,小程序需额外适配 |
| 学习成本 | 熟悉 Vue 即可上手 | 需要熟悉小程序语法 | 需要学习 Dart 和自绘 UI |
| 原生能力 | uni 封装了大量原生 API,支持插件 | 需原生配合 | 通过 platform channel 二次封装 |
| 生态成熟度 | uni_modules 插件丰富 | 微信原生能力最全 | 国内生态仍在补全 |
| 包体积 | 可 Tree-shaking,控制得当 | 相对较小 | 相对偏大 |
最终选了 uni-app,原因很直接:团队核心成员都是 Vue 背景,Vue3 组合式 API 的写法写起来太舒服了;uni-app 把大量原生能力统一封装成了
uni.xxx
接口,定位、相机、地图这些报警高频能力都有现成封装;社区里还有不少政府项目、应急项目的现成插件可以直接抄作业。
当然它的限制也要提前说清楚:一些偏底层的原生能力仍然绕不开离线 SDK 和原生插件,比如高德地图深度定制、视频流硬解码这些,前端层只能做桥接。 如果你的项目 90% 以上是表单、列表、详情页,uni-app 能帮你省掉至少三分之一的工作量;如果涉及大量原生渲染和计算,还是要认真评估插件生态够不够。
1.3 接口安全与签名机制
报警类小程序的接口安全级别比普通业务系统高得多。我们不能只依赖 HTTPS 和 token,服务端必须能识别请求是否被篡改、是否有重放攻击的可能。
我们最终采用的是比较标准的签名方案:前端请求时除了登录态 token,还要带上
timestamp
、
nonce
(随机字符串)和
sign
三个参数。
sign
的生成规则是:把所有业务参数按 key 字典序排列,拼接成 key=value&key=value 格式,再拼接上密钥,做 HMAC-SHA256,转成大写。服务端收到请求后做三件事:校验 timestamp 是否在 5 分钟内、校验 nonce 是否在 Redis 中已存在、用相同规则重新计算 sign 比对。
// utils/sign.js
import CryptoJS from 'crypto-js'
const SECRET_KEY = 'your-secret-key'
export function generateSign(params, timestamp, nonce) {
const keys = Object.keys(params).sort()
const str = keys.map(key => `${key}=${params[key]}`).join('&')
const raw = `${str}&key=${SECRET_KEY}×tamp=${timestamp}&nonce=${nonce}`
return CryptoJS.HmacSHA256(raw, SECRET_KEY).toString().toUpperCase()
}
这套机制实际运行下来有几个额外收益:nonce 缓存天然防止了重复提交,用户连点两次“一键报警”不会产生两条重复工单;timestamp 校验让抓包重放请求的窗口期被压缩到 5 分钟以内。注意点就是前端要把时钟同步好, 设备时间不准会导致签名被服务端误杀,我们最初上线时没处理这个,反馈“总是提示请求过期”的用户几乎都是手机时间不对,后来在客户端加了时间校准逻辑才解决。
2. 环境搭建与工程初始化
2.1 Vue3 + Vite 搭建 uni-app 项目
uni-app 官方推荐两种创建方式:HBuilderX 可视化创建和 CLI 命令创建。既然用 Vue3,我直接选择了 CLI 方式,方便配合 Git 管理,也便于后续写自动化构建脚本。
# 创建项目
npx degit dcloudio/uni-preset-vue#vite my-alarm-app
cd my-alarm-app
# 安装依赖(推荐用 npm,pnpm 在 uni-app 某些依赖上会有 hoist 问题)
npm install
# 运行到微信小程序
npm run dev:mp-weixin
# 构建微信小程序
npm run build:mp-weixin
CLI 创建的项目和 HBuilderX 创建的项目本质上是一样的,核心依赖都在 package.json 里。我在项目中维护了三个环境变量文件:
.env.development
、
.env.test
、
.env.production
,分别对应本地联调、测试服、生产服三套接口地址。这个习惯强烈推荐,后面你会感谢自己一开始就做了环境隔离。
需要注意 Node 版本。我一开始用的 Node 18,跑起来没问题,但同事用 Node 14 装依赖时某个插件直接编译报错。
建议统一 Node 16+,并且在 package.json 里不做
engines
强约束的话,团队文档里一定要写清楚。
项目里我建议配一个全局请求封装,基于
uni.request
做一层 wrapper,统一处理 token 注入、签名、错误码弹窗和登录态过期跳转。开发阶段再包一层前置拦截器,打印完整请求和响应,排查问题效率会高很多。
2.2 manifest.json 中的关键配置与权限申请
manifest.json 是 uni-app 的“总开关”,但它也是新手最容易出问题的地方。先说小程序端,需要填上自己的微信小程序 appid;App 端要重点检查模块权限配置。
小程序端和 App 端的权限体系是两套逻辑。小程序权限在
pages.json
里声明,而 App 端定位、相机、麦克风这些原生能力,必须在 manifest 的可视化界面勾选对应模块,否则运行到 App 时对应能力会直接不可用或者直接报 no permission。我当时就被坑过一次:
uni.chooseImage
在 H5 和微信小程序都好使,但打出来的 Android 包一选相册就闪退,查了半天发现问题在 App 模块配置里没勾选 Camera 和 Gallery。
{
"name": "一键报警平台",
"appid": "__UNI__XXXXXXX",
"versionName": "1.0.0",
"versionCode": "100",
"app-plus": {
"usingComponents": true,
"modules": {
"Geolocation": {},
"Camera": {},
"VideoPlayer": {}
},
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>"
]
},
"ios": {
"privacyDescription": {
"NSLocationWhenInUseUsageDescription": "需要获取您的位置信息用于报警定位",
"NSCameraUsageDescription": "需要访问相机拍摄现场照片和视频",
"NSMicrophoneUsageDescription": "需要访问麦克风录制现场语音"
}
}
}
}
}
这里有个 iOS 隐私描述的细节:苹果审核会检查你的 App 是否在 Info.plist 里声明了所有涉及隐私权限的用途说明, 描述文字要明确告知用户“为什么需要这个权限”,笼统的“用于App功能”可能会被拒审。 我们的写法是“需要访问相机拍摄现场照片和视频”,具体到用途,审核基本一次过。
2.3 开发代理与真机联调
本地开发最大的痛点是跨域。微信小程序里
uni.request
的域名必须在小程序后台配置白名单才算合法,开发阶段可以勾选“不校验合法域名”绕过,但真机预览时 HTTPS 证书问题还是经常冒出来。
H5 端的跨域比较好解决,在
vite.config.js
里配置 devServer 代理即可:
// vite.config.js
import { defineConfig } from 'vite'
import uni from '@dcloudio/vite-plugin-uni'
export default defineConfig({
plugins: [uni()],
server: {
proxy: {
'/api': {
target: 'https://test-backend.example.com',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '')
}
}
}
})
小程序端的联调稍微麻烦一点。我们的测试服接口是 HTTPS,但证书是自签名的,微信开发者工具里预览倒还好,真机预览就经常报
request:fail
。后来统一在微信开发者工具右上角“详情-本地设置”勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”,真机体验版再用开发版二维码扫码测试。这个坑几乎每个做小程序的人都会遇到,不用慌,就是环境校验问题。
如果遇到更复杂的请求问题,比如签名对不上、参数被改,可以用抓包工具看实际发出的请求内容。微信开发者工具自带的 Network 面板已经可以满足 90% 的排查需求;必要时候再借助抓包工具做更完整的链路分析。重点不是“能不能抓到”,而是抓到之后能不能对比出前端发出的参数跟服务端期望的参数哪里不一致,大部分联调问题都出在参数名大小写、空值处理和编码格式上。
3. 报警核心功能模块实现
3.1 一键报警与定位信息采集
报警平台最重要的是让用户“一键就能说清位置”。
uni.getLocation
是最直接的方案,只能定位到经纬度,但我们还需要把经纬度转成文字地址。做法是拿到经纬度后,通过后端接口做逆地理编码,返回省市区和详细地址;前端直接展示给用户确认。
// 报警页获取定位
async function getLocation() {
return new Promise((resolve, reject) => {
uni.getLocation({
type: 'gcj02',
isHighAccuracy: true,
highAccuracyExpireTime: 4000,
success: (res) => {
// 请求后端逆地理编码
uni.request({
url: '/api/geo/reverse',
data: {
lat: res.latitude,
lng: res.longitude
},
success: (geoRes) => {
resolve({
latitude: res.latitude,
longitude: res.longitude,
address: geoRes.data.data.address
})
},
fail: () => {
resolve({
latitude: res.latitude,
longitude: res.longitude,
address: ''
})
}
})
},
fail: (err) => {
reject(err)
}
})
})
}
坐标系是个必须注意的细节。微信小程序端
uni.getLocation
的
type
传
gcj02
,得到的是火星坐标系;如果后台地图数据用的是 GCJ-02,直接存就行。但如果后端接的是 GPS 原始坐标或者高德的坐标系,一定要在服务端做坐标系转换,否则地图上标注的位置会偏几十米到几百米。
报警场景里位置偏 500 米可能就是另一条街,这个精度差异在技术上不可接受。
还有一道“防呆”设计:用户如果拒绝授权定位,界面不能卡死,要兜底到手动选择地址。我们做了一个三级联动选择器 + 地图选点的降级方案,从实际使用数据看,大多数用户还是会选择点击定位按钮自动定位,但兜底逻辑让“拒绝权限”的用户也能完成报警。
3.2 图片/视频/语音报警上传与 m3u8 播放兼容处理
报警现场的证据往往是图片、视频、语音,挑选和上传是核心链路。我们用的是
uni.chooseImage
、
uni.chooseVideo
和
uni.chooseMedia
。在微信小程序端要注意
chooseMedia
的
mediaType
可以同时选图片和视频,App 端低版本兼容性一般,建议分场景调用。
上传用
uni.uploadFile
。这里有个坑:
大视频容易超时。
微信小程序默认超时时间比较短,报警现场录的视频动辄几十 MB,加上弱网环境,上传失败率很高。我们的方案是客户端先做压缩:图片走
uni.compressImage
;视频限制时长(30 秒内),超过的提示用户现场录制,不走相册大文件;上传时把
timeout
加长到 30 秒以上,同时在后端做分片上传接口,前端实现断点续传。分片上传代码量不小,但稳定性的收益非常大。
视频回放的问题更隐蔽。后台会生成两类视频地址:一类是用户上传的原始文件(一般是 MP4),另一类是实时监控流的回放地址(一般走 HLS,也就是 m3u8 格式)。
<video
:src="videoUrl"
:autoplay="false"
controls
object-fit="contain"
style="width: 100%; height: 220px;"
></video>
m3u8 在三个端的兼容情况差异很大:
- 微信小程序:基础库 2.4.0+ 原生支持 HLS 播放,但前提是地址必须是 HTTPS。同一个地址,HTTP 的 m3u8 直接黑屏,换成 HTTPS 就好了。
- App 端:uni-app 的 video 组件在 App 端封装的是原生播放器,m3u8 支持很好,实测 iOS 和 Android 都没问题。
-
H5 端:大部分浏览器不支持 m3u8,原生 video 播放不了。方案是用
hls.js做 polyfill,或者用 renderjs 在视图层解析。但 renderjs 的兼容环境比较特殊,操作临时文件路径也跟普通逻辑层不一样,容易出问题。
关于“renderjs 手机录的 mp4 无法播放”这个问题,我排查下来的结论是: 手机录制的视频编码很多是 HEVC( H.265 ),而 renderjs 里通过 video 标签播放临时路径时,浏览器内核不一定支持 H.265 解码。 到服务端统一下发 M3U8 地址,或者在客户端录制时统一成 H.264 编码,问题就迎刃而解。 这个经验也适用于所有视频播放模块:遇到花屏、黑屏、有声音无画面的问题,第一个排查方向永远先看编码格式。
3.3 路由传参与全局分享的“坑”与解法
路由传参在 uni-app 里是最基础但也最容易踩坑的地方。简单场景用
uni.navigateTo
在 url 后面带 query,页面里
onLoad(options)
接收,常规操作:
// 页面 A
uni.navigateTo({
url: '/pages/alert/detail?id=123&type=emergency'
})
// 页面 B
onLoad(options) {
console.log(options.id, options.type)
}
但报警工单里有时需要传递整个对象,比如用户选择的地址信息
{latitude: 39.9, longitude: 116.4, address: 'xx街道'}
。直接拼在 url 里会有一堆转义问题,而且 url 长度有限制。这种情况我的建议是两种处理方式:一是把对象 JSON.stringify 后
encodeURIComponent
再拼到 query 参数里,接收端
decodeURIComponent
后再
JSON.parse
;二是用
eventChannel
做页面间通信。
// 页面 A
uni.navigateTo({
url: '/pages/alert/preview',
success: (res) => {
res.eventChannel.emit('alertData', {
latitude: 39.9,
longitude: 116.4,
address: 'xx街道'
})
}
})
// 页面 B
onLoad(options) {
const eventChannel = this.getOpenerEventChannel()
eventChannel.on('alertData', (data) => {
console.log(data)
})
}
eventChannel
的好处是不用做编解码,适合传对象、数组这种复杂结构;坏处是事件通信是单向的、调试起来没法一眼看到参数,建议只在传对象时用。
再说分享,这个项目里用户分享报警案例、求助信息是刚需。小程序默认的
onShareAppMessage
只能分享当前页面,但不同页面有不同的分享标题和图片,所以很多团队用全局 mixin 统一处理。
问题就在这:mixin 里的
onShareAppMessage
会被页面内自定义的同名钩子直接覆盖,不会自动合并。
页面一旦自己写了
onShareAppMessage
,全局分享配置全失效。
我们的解法是把公共分享逻辑抽成一个工具函数,页面里保留自己的分享钩子,再主动调用公共函数合并参数:
// utils/share.js
export function buildShareConfig(pagePath, customData = {}) {
return {
title: customData.title || '一键报警,快速求助',
path: customData.path || `/${pagePath}`,
imageUrl: customData.imageUrl || '',
success: (res) => {
console.log('分享成功', res)
}
}
}
// 页面内
import { buildShareConfig } from '@/utils/share'
onShareAppMessage() {
const config = buildShareConfig('pages/index/index', {
title: `紧急求助:${this.currentCaseId}`,
path: `pages/index/index?shareCaseId=${this.currentCaseId}`
})
return config
}
这样既保底又有页面个性化,还能通过分享路径里的参数做来源统计。
3.4 地图导航与轨迹回放
接警端最需要地图能力:报警位置展示、出警路线规划、现场轨迹回放。小程序端我们用
map
组件显示地图,结合
marker
标出报警点;用户点击某个报警记录时,可以调用
uni.openLocation
直接拉起微信内置地图查看位置。
uni.openLocation({
latitude: 39.908823,
longitude: 116.39747,
name: '报警地点',
address: '北京市东城区某街道',
scale: 16,
success: () => {
console.log('打开地图成功')
},
fail: (err) => {
console.log('打开地图失败', err)
}
})
uni.openLocation
唯一的限制是它只展示位置,不能自定义路线和导航轨迹。如果要做出警车辆轨迹回放,需要用到
map
组件的
polyline
。后端每隔几秒上报一次车辆坐标,前端拉取坐标数组后绘制轨迹线:
<map
:latitude="centerLat"
:longitude="centerLng"
:markers="markers"
:polyline="polyline"
style="width: 100%; height: 300px;"
></map>
const polyline = [{
points: this.trackPoints, // [{latitude, longitude}, ...]
color: '#FF0000',
width: 4,
arrowLine: true
}]
高德地图的深度能力在 uni-app 里使用有几种方式:官方 map 组件、高德原生插件、或 Web View 形式嵌入。考虑到阿里云要定期续费、原生插件要更新兼容,我们直接采用官方 map 组件实现 90% 的需求,导航这块交给
uni.openLocation
,避免引入大量原生依赖导致 App 包变大和审核复杂度上升。
4. 隐私合规与支付相关扩展
4.1 隐私政策弹窗与“不同意就退出App”的实现
App 上架到安卓应用市场和苹果 App Store,隐私合规是硬指标。 用户首次启动必须弹出隐私政策弹窗,并且用户有权选择“不同意并退出”。 苹果对这块尤其严格,没有弹窗或者弹窗文案不清,审核直接打回。
我们做了一个自定义弹窗,App 启动时读取本地是否已同意,没同意过就弹窗;用户点击“同意”后写入本地
Storage
并继续初始化;点击“不同意”直接退出。
// App.vue 或启动页
function checkPrivacyAgreement() {
const agreed = uni.getStorageSync('privacy_agreed')
if (agreed) {
initApp()
return
}
this.showPrivacyModal = true
}
function onAgreePrivacy() {
uni.setStorageSync('privacy_agreed', true)
this.showPrivacyModal = false
initApp()
}
function onDisagreePrivacy() {
// App 端
if (plus && plus.runtime) {
plus.runtime.quit()
}
}
这里面有几个细节要注意:
- 不要只点“不同意”隐藏弹窗 ,审核人员会反复测试,发现还能继续用就会视为不合规。
-
退出逻辑在 iOS 上要小心处理
。
plus.runtime.quit()在 Android 上可以真正退掉进程,但在 iOS 上苹果并不推荐直接调用退出,有时候只是回到桌面。更稳妥的做法是:弹窗上“不同意退出”按钮调用plus.runtime.quit(),同时在系统层清除本地状态,让用户下次打开时重新看到弹窗。 - 用户协议和隐私政策的链接必须是可点击的、能正常打开的 ,我们踩过链接域名不在备案白名单导致点击打不开的坑,审核被打回一次。
微信小程序端的隐私政策要求又不一样。小程序后台要配置“用户隐私保护指引”,前端在收集用户信息前也要弹窗授权。 小程序没有“退出小程序”的 API,用户不同意时只能做引导页,提示用户关闭小程序,或者提供“放弃使用”按钮跳转到客服会话。
4.2 微信支付v3 对接流程(作为增值服务接入参考)
报警本身是免费的,但平台后续会接一些便民缴费类功能,比如事故处理费用、违规罚款代缴之类,所以支付能力要提前预留。我们直接对接了微信支付 v3 接口,跟老一代 v2 相比,v3 的接口设计更规范,全部走 RESTful API,证书和签名机制也更清晰。
v3 的核心概念先理清楚:
- 商户号 :申请微信支付后分配的唯一标识。
- API v3 密钥 :用于解密回调通知中的敏感字段,自己设置的 32 字节字符串。
- 商户 API 证书 :包含商户私钥和证书序列号,用于生成请求签名。
- 平台证书 :微信支付平台自己的证书,用于验证微信回调的签名。
前端拉起支付的流程比较简单,核心逻辑都在后端:
// 后端 Node.js 简化版统一下单
const axios = require('axios')
const crypto = require('crypto')
const {
MCH_ID,
APP_ID,
API_V3_KEY,
PRIVATE_KEY_PATH
} = process.env
// 构造签名
function buildSign(method, url, timestamp, nonce, body) {
const message = `${method}\n${url}\n${timestamp}\n${nonce}\n${body}\n`
const privateKey = fs.readFileSync(PRIVATE_KEY_PATH, 'utf8')
const sign = crypto.createSign('RSA-SHA256').update(message).sign(privateKey, 'base64')
return sign
}
// 发起 JSAPI 下单
async function createOrder(orderNo, amount, openid) {
const url = '/v3/pay/transactions/jsapi'
const timestamp = Math.floor(Date.now() / 1000)
const nonce = crypto.randomBytes(16).toString('hex')
const body = JSON.stringify({
appid: APP_ID,
mchid: MCH_ID,
description: '便民缴费',
out_trade_no: orderNo,
notify_url: 'https://backend.example.com/api/pay/notify',
amount: {
total: amount, // 分为单位
currency: 'CNY'
},
payer: {
openid
}
})
const sign = buildSign('POST', url, timestamp, nonce, body)
const response = await axios.post(`https://api.mch.weixin.qq.com${url}`, body, {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `WECHATPAY2-SHA256-RSA2048 mchid="${MCH_ID}",nonce_str="${nonce}",signature="${sign}",timestamp="${timestamp}",serial_no="${SERIAL_NO}"`
}
})
return response.data // 返回 prepay_id
}
前端拿到
prepay_id
后,通过
uni.requestPayment
拉起微信支付:
uni.requestPayment({
provider: 'wxpay',
timeStamp: paymentParams.timeStamp,
nonceStr: paymentParams.nonceStr,
package: paymentParams.package,
signType: 'RSA',
paySign: paymentParams.paySign,
success: (res) => {
console.log('支付成功', res)
},
fail: (err) => {
console.log('支付失败', err)
}
})
v3 最容易出错的地方就三个:
金额单位必须为分
,传元进去会直接报参数错误;
时间格式必须是 RFC3339 格式
,即
2024-01-01T12:00:00+08:00
;
回调通知里的 resource 字段需要用 API v3 密钥 AES-256-GCM 解密
,不能直接用明文。
回调验签和解密,后端要做两件事:先用微信平台证书验证签名,再解密 resource 拿到支付成功的订单号、金额和 openid。这个环节如果验签失败,微信服务器会重试通知,不要急着在日志里骂微信,先检查你的平台证书是否更新了。
5. 打包上架与真机调试避坑
5.1 微信小程序打包与安卓应用市场上架
微信小程序打包最简单:执行
npm run build:mp-weixin
,产物在
dist/build/mp-weixin
目录,微信开发者工具导入这个目录,确认没问题后点“上传”,然后在小程序后台提交审核。
小程序审核有一些需要注意的坑位:
- 类目选择 。报警类小程序建议选择“政务民生”或“工具 > 信息查询”下的合适类目,并准备对应的资质文件,不然审核可能以“涉及公安业务需提供特殊资质”为由拒绝。
- 用户隐私保护指引必须完整 。后台要配置收集了哪些信息,比如位置信息、相机、相册,没有对应说明也会被拒。
- 分享文案不要夸大 。“一键解决所有问题”这种诱导性标题会被判违规。
安卓应用市场上架前,需要在 manifest 的 App 模块里配置好签名证书。生成 Android 签名证书的命令:
keytool -genkey -alias myapp -keyalg RSA -keysize 2048 -validity 36500 -keystore myapp.jks
然后使用 uni-app 云打包,勾选使用自有证书,填上 keystore 文件和别名、密码。云打包完成后会生成 apk 或 aab。各大安卓应用市场(华为、小米、OPPO、vivo、应用宝)上架时的要求大同小异,核心是: 必须有隐私弹窗、权限说明清晰、应用内不得有诱导点击 。有些市场还要求提供软著或备案号,建议提前准备好。
5.2 iOS上架与隐私协议审核
iOS 上架用的是另一套证书体系。现在都推荐在 App Store Connect 后台注册 App ID,生成
Distribution
证书和描述文件,再交给 uni-app 云打包。iOS 打包需要两样东西:
.p12
证书文件和
.mobileprovision
描述文件。
iOS 审核最严格的地方就在隐私和权限描述。前面说的 Info.plist 里的
NSLocationWhenInUseUsageDescription
、
NSCameraUsageDescription
、
NSMicrophoneUsageDescription
文案必须写好。
苹果审核人员会启动 App,模拟用户拒绝权限、拒绝隐私协议等操作,如果你的 App 在这些情况下直接崩溃或者白屏,会被直接打回。
我们被迫补了很多防御代码:弹窗按钮的 loading 状态、拒绝权限后的引导页、网络异常的兜底提示。
另外,苹果要求 App 内必须有“投诉/反馈”的入口,报警类 App 尤其要有一个用户联系后台的渠道。我们做了一个“帮助与反馈”页面,接入了客服会话功能,审核时也算是一个加分项。
5.3 常见调试问题速查表
整理一些项目里高频出现的问题,开发时遇到可以直接对照排查:
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 小程序软键盘遮挡查询内容 | 键盘弹出后页面没有自动滚动 |
页面开启
adjust-position="false"
,手动监听键盘高度并滚动容器
|
| 下拉刷新与页面滚动冲突 | 页面启用了下拉刷新,scroll-view 里也绑定了刷新 |
二选一:页面级刷新用
enablePullDownRefresh
,局部刷新用
scroll-view
的
refresher-enabled
,不要同时用
|
| 打开 webview 页面有过渡白屏 | web-view 加载需要时间 |
在 web-view 内嵌 html 中设置背景色并加 loading 层,用
onPageFinished
隐藏 loading
|
| 扫码扫出来是一串数字 | 普通二维码内容本身就是字符串 |
先
JSON.parse
判断格式,再决定是直接展示还是跳转处理;不要默认扫码结果一定是小程序码
|
| renderjs 播放手机录的 MP4 无法播放 | 视频编码为 HEVC(H.265),浏览器不支持 | 服务端转码为 H.264 或改用 m3u8 播放 |
| App 端勾选了权限模块仍无法定位 | manifest 修改权限后没有重新打包 | manifest 变更必须重新打包才生效,热更新不行 |
| 小程序页面分享标题没生效 |
页面内自定义
onShareAppMessage
覆盖了全局 mixin
| 按 3.3 的方案把公共分享逻辑抽成工具函数合并 |
| H5 端 m3u8 视频黑屏 | 浏览器不支持 HLS | 接入 hls.js 或让后端起 MP4 地址 |
这里展开说两个我觉得特别值得注意的。
第一个是小程序软键盘遮挡输入框的问题。微信小程序里,输入框如果被软键盘挡住,用户根本看不到自己打了什么,体验极差。常规做法是把输入框页面把
adjust-position
设为
false
,然后监听键盘高度,手动把页面往上推一段距离。但这里有个细节:监听键盘高度的回调在不同的基础库里可能会有兼容性问题,我在代码里加了平台判断,基础库低于某版本时用
wx.onKeyboardHeightChange
,高版本时用
uni.onKeyboardHeightChange
,两个 API 都封了一层。
第二个是 webview 白屏。我们页面里嵌了不少后台处理流程的 H5 页面,最开始打开时总有一两秒白屏,用户以为是打不开。后来在前端入口页加了一个首屏 loading 动画,再配合 web-view 的
@onPageFinished
事件控制隐藏,基本感受不到白屏了。
记住,webview 白屏不是代码 bug,是网络加载的固有延迟,重点是给用户视觉反馈,不要让用户以为卡死了。
最后分享一点自己的体会
这几个月的开发和上线过程中,我最大的一个感受是:报警类小程序的功能亮点不是“技术有多炫”,而是“用户在关键时刻能用起来”。UI 再好看,如果定位转圈、上传失败、按钮点了没反应,一切都是零。所以我把大部分时间花在了异常流程上:定位权限被拒绝时给出手填地址的兜底,上传失败时自动重试而不是直接报错,弱网环境下给用户清晰的进度反馈。
还有一个深刻的教训是: 上线前一定要做权限拒绝测试和弱网测试 。真机连着 Wi-Fi 跑得太顺,不代表用户在地下车库、地铁里也能跑得顺。报警场景尤其不能赌网络质量,客户端必须在弱网下给出足够明确的等待反馈和重试入口。
这个项目做完之后,我对 uni-app 的跨端边界也有了更清醒的认识。它能帮你省掉至少三分之一的重复开发,但原生能力和平台差异仍然要靠工程手段去补平。对于一个业务系统而言,选好框架是起点,把每个平台的细节打磨到位才是真正的交付。如果你也在做类似的便民服务类小程序,希望这篇分享能帮你少走几步弯路。
火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。
更多推荐
所有评论(0)