ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

UniApp同城O2O项目独立部署与二次开发实战解析

UniApp同城O2O项目独立部署与二次开发实战解析 先说结论用 UniApp 做同城 O2O 项目最舒服的不是“能一套代码跑三端”这个噱头而是它把“独立部署”和“二次开发”这两件事的门槛压得很低。我去年到今年一直在维护一套跑在同城配送场景的 UniApp 项目从微信小程序、H5 到 Android/iOS App 都在跑期间经历了完整的上架、支付联调、地图接入、扫码、视频播放、WebView 混合通信这些环节。今天这篇就把这套项目里“独立部署”和“二次开发”涉及到的关键节点全部拆开讲包括 manifest 配置、多域名切换、支付差异、扫码体验、地图适配、组件样式穿透、底部导航闪烁、麦克风权限这类高频问题尽量做到你拿着就能用。这篇东西适合谁适合已经会一点 Vue 但还是第一次用 UniApp 接真实 O2O 业务的前端也适合后端同学想自己把管理端和用户端部署起来做个闭环演示。我会尽量避免只讲 API 怎么调重点讲这些 API 在真实场景里为什么会这样设计、有哪些坑你必须提前知道。毕竟同城 O2O 这种项目核心不是页面多好看而是“定位、支付、订单流转、消息触达”这条链路不能断。1. 独立部署的整体思路与工程选型1.1 为什么同城 O2O 更适合独立部署很多人第一次接触 UniApp 时习惯把它理解成“小程序工具”实际在落地同城 O2O 项目时你会发现业务方往往同时要求“小程序先上线H5 也要能打开App 后续也要有”。这时候如果依赖第三方 SaaS 平台或者云开发业务数据、用户体系、支付商户号都会被平台绑死后续想改业务逻辑非常难受。独立部署的意思是前端代码打包成各端产物之后静态资源放在自己的服务器或 CDN 上后端接口完全由自己控制域名、证书、数据库、对象存储全部自己管。这么做的直接好处有三个数据主权在自己手里用户订单、支付流水、配送轨迹这类敏感数据不会被第三方平台约束。二次开发空间大从首页装修、营销活动到配送路由算法都可以直接改代码而不是等平台开放接口。成本可控初期可以只租一台低配云服务器跑后端静态资源放 CDN后续再逐步扩容。但独立部署也意味着所有问题都得自己扛。尤其是 H5 端的域名校验、小程序的 request 合法域名配置、App 端的打包证书这些环节环环相扣任何一个配置不对线上就直接白屏。后面我会把每一步都列出来。1.2 技术选型为什么是 UniApp 而不是原生或纯 Web我承认原生小程序和纯 Vue Web 也都能做但同城 O2O 这种“一鱼三吃”的业务形态下UniApp 的性价比确实最高原因是它把三个关键痛点在编译期就处理掉了第一多端条件编译。同一套代码通过#ifdef MP-WEIXIN、#ifdef H5、#ifdef APP-PLUS可以写出差异化的平台逻辑。比如 App 端支付需要调用 plus.payment小程序端用 uni.requestPaymentH5 端则走 JSSDK 或者后端跳转这些差异写在同一个文件里维护成本比维护三套工程低得多。第二组件生态和原生能力的桥接。UniApp 提供了大量 uni.* API像 uni.getLocation、uni.scanCode、uni.login 这些跨端能力底层会自动抹平差异。O2O 项目里最频繁用到的就是定位和扫码实测下来小程序端和 App 端的表现虽然还有细微差别但业务可接受度很高。第三独立部署友好。UniApp 的 H5 构建产物就是纯静态资源随便扔到 Nginx 就能跑不需要特殊运行时。小程序端发布到微信公众平台App 端打好包上传到应用市场全部流程都是标准化的。当然选 UniApp 也要接受它的限制。比如视频播放性能不如原生复杂动画流畅度一般还有个别原生插件需要自己写离线打包。我在项目里就碰到过“App 端录屏防止”这类需求纯 UniApp 层做不到系统级最后是靠原生插件才搞定。所以选型时不要盲目追求“一套代码走天下”而是要在熟悉它边界的前提下把优势用足把劣势绕开。1.3 项目目录设计与多端产物规划同城 O2O 项目一般涉及用户端、骑手端、商户端三个角色如果全部塞进一个 UniApp 工程pages.json 会膨胀到没法维护。我目前用的是“一个主工程 多套页面分包”的方式结构大致是这样src/ ├── pages/ # 用户端主包页面 ├── pages_merchant/ # 商户端分包 ├── pages_rider/ # 骑手端分包 ├── components/ # 公共组件 ├── api/ # 接口请求统一封装 ├── store/ # Pinia/Vuex 状态管理 ├── utils/ # 工具函数 └── static/ # 静态资源pages.json 里通过 subPackages 配置分包每个角色包独立成包首屏加载只下载主包。独立部署时每个角色其实对应不同的登录态和权限所以路由拦截也要分开写。这个设计一开始多花了半天规划后面省了非常多反复改 pages.json 的时间。产物规划方面我是这样约定的H5 端构建到dist/build/h5小程序端用微信开发者工具导入dist/build/mp-weixinApp 云打包时直接选发行 原生App-云打包。每次发版前固定跑一遍三端构建保证产物都能正常出。注意分支管理开发分支打测试包master 分支打正式包再用 CI/CD 自动推送 H5 到服务器这是独立部署比较舒服的一条流水线。2. manifest 配置与多域名适配2.1 manifest.json 里最容易忽略的配置项manifest.json 是 UniApp 项目的门面除了应用名称、AppID、版本号这些基本项同城 O2O 项目里还有几个配置项直接影响上架和运行必须逐个过一遍。uni-app 应用标识微信小程序端需要在 mp-weixin 节点填上自己的 AppID否则开发者工具里预览会报错。App 端在云打包时需要在 DCloud 开发者中心创建应用拿到 appid 填回来。App 模块配置O2O 项目几乎必选定位、地图、支付、推送、摄像头这几项。这里要注意云打包时模块不是默认全开的没勾选对应模块调用相关 API 会直接失败。我第一次打包就是忘了勾选定位模块结果在真机上经纬度一直是 0。H5 端跨域配置devServer 的 proxy 可以解决本地开发跨域但线上还是建议后端处理好 CORS或者用 Nginx 反向代理/api路径。这点不配好H5 发布后接口全部报跨域排查起来很头疼。App 图标与启动图安卓应用市场上架时对图标、截图、隐私政策都有硬性要求manifest 里的图标配置只是第一步真正的合规检查在应用市场后台。另外同城项目里经常会出现“一套代码对应多个客户”的情况每个客户的 appid、包名、推送证书都不一样这时候建议用环境变量配合 manifest 的 condition 编译模式来管理不要每次发版都手改 manifest。我见过不少团队就是因为这个环节靠人手改结果把客户的微信小程序 AppID 给串了发布到线上才发现回滚成本非常高。所以独立部署到多个环境时manifest 必须纳入版本管理并且要在 CI 里用脚本动态生成或替换对应配置段。2.2 多域名环境本地、测试、生产的切换策略同城 O2O 项目涉及的环境至少三个本地开发环境、测试环境、生产环境。每个环境的接口域名、文件上传域名、WebSocket 地址都不一样。如果全部写死在代码里每切换一次环境就要全局搜索替换效率低且容易漏。我的方案是建一个config/index.js按环境导出配置对象// config/index.js const env process.env.NODE_ENV || development const config { development: { baseUrl: https://dev-api.example.com, uploadUrl: https://dev-upload.example.com, wsUrl: wss://dev-ws.example.com }, test: { baseUrl: https://test-api.example.com, uploadUrl: https://test-upload.example.com, wsUrl: wss://test-ws.example.com }, production: { baseUrl: https://api.example.com, uploadUrl: https://upload.example.com, wsUrl: wss://ws.example.com } } export default config[env]然后在请求封装里统一用config.baseUrl拼接不要在页面里直接写https://开头的硬编码地址。这样本地开发、测试联调、生产发版各走各的域名互不干扰。打包的时候通过uni build --mode test这种形式注入环境变量实测比直接改 manifest 里的 h5.router.base 更合理因为域名只跟接口有关H5 的 base 路径是另一回事。还有一个容易踩坑的地方微信小程序端有“request 合法域名”校验每次新增一个二级域名都要去微信公众平台的后台配置。如果你们测试环境和生产环境域名不一样记得把两个域名都加进去否则测试环境一切正常换到生产环境就白屏原因只是域名没加白名单。App 端没有这个限制但 H5 端如果要跨域请求必须后端配合 CORS或者本地开发时在 devServer 里配置 proxy。2.3 H5 端两个域名的玩法同城多商户隔离搜索热词里有“uniapp 封装h5如何指向2个域名”这在同城 O2O 里其实很常见。比如一个平台下有多个城市分站每个分站绑定独立域名但前端代码是同一套。用户访问杭州分站时接口域名是 hangzhou-api.example.com访问上海分站时接口域名是 shanghai-api.example.com。这个需求本质上是“运行时动态决定接口域名”而不是“打包时静态指定”。我的做法是在 H5 端启动时用脚本读取当前 window.location.hostname匹配出对应的城市编码和 API 域名写入全局 store。这样用户打开哪个域名API 就指向哪个域名对应的服务集群。后端只需在网关层按城市编码做数据隔离前端不用维护多套代码。实现上需要在 index.html 里加一小段同步脚本否则白屏阶段请求就已经发出去了script (function () { var host window.location.hostname var city default if (host.indexOf(hangzhou) -1) city hangzhou if (host.indexOf(shanghai) -1) city shanghai window.__CITY__ city })() /script然后在请求封装初始化时读取window.__CITY__替换 baseUrl 的城市前缀。注意 App 端没有 window.location所以要单独配置默认城市。这个方案处理完以后一套 H5 代码同时服务多个城市域名部署时只需要在不同 Nginx 配置里指向同一份静态资源即可非常省事。3. 工程级特性与二次开发实战3.1 ESLint 与代码规范多端项目必须从第一天做起UniApp 项目默认生成出来的代码风格比较自由变量命名、组件导入、样式写法都没有强约束。但一旦进入二次开发阶段多人协作的问题就暴露了有人用分号、有人不用有人写var有人写const有人把业务请求直接写在页面里有人封装到 api 目录。这些混乱在 O2O 这种长生命周期项目里会滚雪球我后来花了两个晚上把所有文件过了一遍 ESLint才把局面稳定下来。具体操作是在项目根目录安装eslint、eslint-plugin-vue新建.eslintrc.jsmodule.exports { root: true, env: { browser: true, node: true, es6: true }, extends: [plugin:vue/recommended, eslint:recommended], parserOptions: { ecmaVersion: 2020, sourceType: module }, rules: { vue/max-attributes-per-line: off, no-console: process.env.NODE_ENV production ? warn : off, no-unused-vars: [warn, { argsIgnorePattern: ^_ }] } }配套的.prettierrc建议统一 2 空格缩进、单引号、行尾不加分号。HBuilderX 内置的格式化工具和 Prettier 偶尔会打架可以直接用命令行npm run lint强制校验。注意 UniApp 项目开启 ESLint 后HBuilderX 里要确保工具链用的是项目本地依赖而不是内置的旧版本否则提示规则对不上。还有个坑是UniApp 的模板里会包含大量平台条件编译注释ESLint 默认解析器可能不认识#ifdef这种写法需要把常见平台指令作为注释处理。实测下来 ESLint 不会报错但编辑器偶尔会把#ifdef高亮成错误语法不影响编译但有点烦可以在 EditorConfig 里把 .vue 文件的注释配置调整一下。3.2 生命周期适配页面、应用、组件三层UniApp 的生命周期看起来跟 Vue 很像实际上有三层需要分清应用生命周期App.vue、页面生命周期onLoad、onShow、onHide、组件生命周期Vue 标准生命周期。同城 O2O 项目里最容易出问题的就是页面销毁和重新进入时的状态恢复尤其是订单列表页、地图选点页这种带“当前状态”的页面。举一个实际例子用户在首页点击一个商品进入详情页然后返回首页首页需要刷新购物车角标用户从订单详情页点“再来一单”回到首页后首页榜单数据需要重新拉取。如果只在 onLoad 里请求数据页面从页面栈恢复时不会触发 onLoad数据就停留在旧状态。所以我一般在首页用 onShow 配合一个 lastUpdateTime 字段只在必要时强制刷新onShow() { if (Date.now() - this.lastUpdateTime 30000) { this.fetchHomeData() } }App.vue 的 onLaunch 适合做全局登录态检查、版本更新提示、全局异常监听。注意小程序端 App.vue 里不能访问 DOM也不能使用uni.setStorageSync之外的同步存储做太重的事情否则首屏会有卡顿感。组件生命周期则完全遵循 Vue 的 created、mounted、beforeDestroy但要注意 UniApp 在 App 端和 H5 端的组件渲染时机略有差异。特别是地图组件、视频组件这类原生组件mounted 里立刻去操作地图实例可能会拿不到最好配合 nextTick 或者延迟 100ms 再执行。3.3 微信小程序、H5、App 三端的支付流程差异支付是同城 O2O 的核心环节也是跨端差异最大的地方。搜索热词里有人问“uniapp打包app支付和微信小程序支付时支付流程和参数是否相同”答案很明显有差别而且差别很大。如果不搞清楚差异点服务端很容易设计出一套“万能支付接口”但实际各端根本无法复用。微信小程序走的是uni.requestPayment参数里需要 provider: wxpay订单信息由后端调用微信支付接口生成 prepay_id然后经过签名后返回给前端 timeStamp、nonceStr、package、signType、paySign 这些字段前端直接透传给 requestPayment 就行。App 端也调用uni.requestPayment但 provider 可以填 wxpay且依赖 manifest 里配置的微信支付模块和开放平台的应用。这里不同的点在于App 端调起微信支付时需要先在微信开放平台创建移动应用并绑定包名和签名否则真机上会提示“微信未安装”或者调不起支付。整个流程里后端统一下单时使用的 appid 必须是开放平台应用的 appid不能拿小程序 AppID 去下单否则 App 端支付会失败。H5 端的支付就相对麻烦。微信 H5 支付需要后端返回一个 mweb_url 链接前端用window.location.href跳转过去支付完成后再跳回回调地址。这个回调地址需要和后端约定好一般是window.location.origin /#/pages/order/detail?idxxx这种带参数的路由否则支付完成后用户无法回到正确的订单页。另外 H5 支付只能在微信浏览器里用微信支付普通浏览器里得用支付宝或者拉起重定向。我的建议是 H5 端把微信支付和支付宝都接上根据uni.getSystemInfoSync().platform判断当前浏览器环境再决定用哪种支付方式。支付回调后的订单状态更新前端不要信任自己的业务逻辑判断一切以后端回调为准。前端只负责根据支付结果跳转到“支付成功”或“支付失败”页面页面上要提供“刷新订单状态”按钮避免用户卡在中间态。3.4 自定义分享小程序好友分享与 App 分享同城 O2O 的拉新裂变场景里自定义分享是刚需。小程序端可以用uni.showShareMenu和onShareAppMessage最常用的是把商品的标题、图片、路径分享给微信好友。这里有一个细节分享出来的卡片要能正确打开对应商品详情路径里的参数必须经过 encodeURIComponent 处理商品标题里有特殊字符时尤其要注意。onShareAppMessage() { return { title: this.goods.name, path: /pages/goods/detail?id${this.goods.id}fromshare, imageUrl: this.goods.cover } }App 端分享跟小程序完全不一样需要调用 plus.share 模块而且要预先在 manifest 里配置分享 SDK。微信分享需要申请微信开放平台并配置 Universal LinksiOS和包名签名Android。做完这些之后代码里通过uni.share这个封装可以统一调用但底层是否配置成功必须在真机上用微信小号实测开发者工具里模拟不了。H5 端的分享就比较弱微信内置浏览器可以通过微信 JS-SDK 自定义分享标题和缩略图普通浏览器只能靠复制链接或生成海报。我给项目加了一个“生成分享海报”的通用组件用 canvas 绘制商品图、价格、二维码用户保存图片后再去微信/朋友圈发布这种“图片分享”的方式在 O2O 场景里比纯链接转化率高很多。3.5 扫码能力从商品码到门店码再到一码多扫同城项目里的扫码场景特别多扫码核销优惠券、扫码开门、扫码骑车上锁、扫码点餐。UniApp 扫一扫用的是uni.scanCode小程序端会自动拉起微信的扫码界面App 端则是调用系统相机扫码。如果只是扫普通二维码这套 API 完全够用但同城项目里经常会遇到“扫码不清”的问题尤其是骑手在户外扫车身码、门店码光线暗或者码面破损时识别率下降明显。这时有几个实操经验可以分享扫码前检查相机权限被拒绝时要引导用户去系统设置开启权限。如果业务码是固定的像门店码这种长期不变的可以在服务端生成高清码并统一尺寸不要用 App 动态生成的低码率二维码去打印。如果扫出来的内容不是 URL 而是纯业务字符串比如o2o://shop/1001?tokenxxx原生识别器也能识别但要注意内容里的特殊字符建议二维码内容用 URL 编码后再生成否则某些扫码引擎会截断。uni.scanCode在 App 端有一个需要注意的问题扫码成功后回调里的 result 字段在部分安卓机型上取到的不是完整二维码内容而是自动跳转后的 URL。这是因为一些 ROM 的扫码组件会自作主张地对二维码内容做识别并跳转。解决办法是不要直接在扫码回调里做业务逻辑把结果带回页面后先正则校验不符合预期格式就给用户一个“无法识别”的提示不要静默失败。另外小程序端的扫码结果如果是普通链接微信会默认跳转而非返回 result。这个时候可以在 scanCode 成功回调里用resultStr判断如果是以 http 开头的内容就复制到剪贴板并提示用户自行打开或者直接中转到一个 H5 解码页再带回小程序。3.6 天地图接入微信小程序、H5、App 三端处理同城 O2O 项目里地图选点和轨迹展示非常核心但国内商用地图对小程序端的支持一直不算顺畅。热词里提到的“uniapp接入天地图适配微信小程序、h5、app”其实是对接中国地图服务商的地图能力做统一封装。天地图的好处是合规且免费额度相对充足但它的 JS API 也是标准的 Web API在小程序里不能直接用需要通过 WebView 嵌入 H5 页面来加载。我的做法是写一个 MapContainer 组件内部用 WebView 承载一个 H5 地图页面。页面之间通过 postMessage 通信小程序的web-view和 App 里的plus.webview都支持向内部页面发消息。地图页面包装了天地图 JS API支持搜索、选点、逆地理编码、轨迹回放对外暴露统一接口给 UniApp 层调用。这个方案在 H5 端最省心因为本身就是网页直接引入天地图 JS API 即可。小程序端受限于域名校验要求 web-view 承载的 H5 页面域名必须配置为业务域名。App 端限制相对少但要处理 webview 返回时的页面栈问题。天地图定位精度其实还可以但要把它跟手机原生定位结合先用uni.getLocation拿到经纬度再把经纬度传给地图组件去反查地址。不要指望地图组件自己去定位因为 WebView 里的浏览器定位权限在各端上的策略不一样精度和成功率都不稳定。3.7 视频播放与展示商品展示视频的自动播放和预加载同城 O2O 的很多商品比如美甲、宠物、家厨这类服务型商品文字描述很难打动人视频的重要性很高。UniApp 里视频播放用的是video组件基础用法不复杂但 O2O 场景里有两个需求会用到视频懒加载和自动播放。热词里提到“uniapp 商品展示视频”“uniapp视频自动播放”“uniapp视频预播放”都是围绕这几点展开的。小程序端和 App 端对自动播放的限制不一样小程序端 iOS 上 video 组件要用户手动触发才能带声音播放Android 上静音自动播放相对宽松App 端用 plus.video 封装可以实现更多控制。我的方案是列表页的视频全部用图片盖层模拟用户滑动到视口内时再动态设置 video 的 src 并调用 play 方法。避免一次性加载大量视频地址否则内存占用会直接拉爆中低端安卓机。预加载方面video 组件本身没有预加载 API但可以在用户即将看到视频前先渲染一个隐藏的 video 并设置preloadauto。这里要注意小程序端的 preload 属性目前支持仍有限所以更可靠的做法是只设置 poster让系统在进入页面时自动拉取第一帧实际播放时再加载完整视频源。不要在小程序里对 video 做太激进的预加载策略微信对这类操作有性能审计严重的会有告警。H5 端相对自由一点可以在列表滚动时用 IntersectionObserver 判断视频进入视口再动态创建 video 元素播放。但每个网页同时播放多个视频会互相抢音频焦点所以要注意同时只保留一个活动播放器其余全部 pause。4. 高频问题排查与避坑实录4.1 WebView 返回处理与 H5 重新加载UniApp 里的 WebView 返回行为跟普通页面的 uni.navigateBack 不是一回事。热词里专门有人问“uniapp webview的页面返回方式跟常规页面返回不太一样怎么处理”我深有同感。因为 WebView 内部有自己的历史栈用户在内嵌 H5 页面里点返回通常应该优先返回 H5 的上一个历史记录而不是直接退出整个 WebView 页面。我封装的返回策略是这样的在 WebView 页面里用plus.webview.currentWebview()拿到原生 webview 对象监听 onBackButton然后调用canBack()判断内部是否有历史记录。如果有执行back()如果没有再执行uni.navigateBack。小程序端用web-view的时候情况比较简单小程序菜单里自带的“返回”按钮会直接退出 web-view 页面H5 内部的历史路由没法接管。所以小程序端的内嵌 H5 页面必须自己处理好单页路由保证用户从 H5 的任何子页面返回时都能退到“退出 H5”这个入口。否则用户会卡死在 H5 内部体验非常差。另外H5 端重新加载当前页面也是一个常见需求。支付完成或者登录态变化后经常需要清理页面状态。普通 Vue 页面里可以用uni.redirectTo重定向到当前页但在 WebView 页面里不行最好让内嵌 H5 监听 postMessage 收到 refresh 事件后执行location.reload()。我在前端请求拦截器里加了一个forceRefresh标记登录过期时自动通知 WebView 刷新页面实测能解决很多“为什么我登录了还显示未登录”的诡异 bug。4.2 底部导航栏切换闪烁处理如果你用 uni-app 的 tabBar 管理 O2O 项目的首页、订单、我的这些主模块大概率会遇到切换 tab 时底部导航闪烁一下的问题。我在多个机型上复现过这个问题原因是 TabBar 在切换时页面进入动画和新页面资源加载同时进行底栏被重绘视觉上就像闪了一下。我的解决思路分为两个层面第一减少页面白屏时间。每个 tab 页的首屏请求尽量做缓存比如首页的 banners、分类、推荐商品列表可以缓存到 storage设置 5 分钟过期。用户切换 tab 时先渲染缓存数据再后台刷新新数据视觉上就不会有一闪而过的白屏感。第二对 TabBar 的 icon 资源做特殊处理。尽量控制 icon 文件体积不要用几 MB 的大图推荐用 81x81px 的 PNG压缩后单张在 20KB 左右。icon 加载过慢也会导致 tabBar 重绘时闪烁。这个问题在高分辨率安卓机上尤其明显。如果闪烁已经影响了正常的开发调试可以先开uniapp的--watch模式看是编译导致的界面重载还是运行时导致的如果快速切换 tab 时 console 里有大量 WebView 重建日志那大概率是 App 端在复用一个 webview 容器时处理不当可以尝试关闭“切换动画”或者把 tabBar 的animation设置为 false。4.3 安卓手机打包后缺少麦克风权限这是一个典型的“云打包后权限清单发生变化”的问题。很多 O2O 项目需要语音输入、语音通话或者录音评价本地调试时 uni.getRecorderManager 一切正常但打包安装到小米手机上设置里根本没有麦克风权限选项导致录音完全没声音。原因在于 UniApp 云打包时原生权限是按模块动态生成的。如果你的工程里没有实际调用录音相关 API或者对应的原生插件没有声明麦克风权限云打包时就不会把 RECORD_AUDIO 加进 AndroidManifest.xml。解决办法是在 manifest.json 的“App 模块配置”里手动勾选“Recorder(录音)”模块有的版本还需要在“权限配置”里显式添加android.permission.RECORD_AUDIO。这里有一个很容易忽略的细节如果 App 端只是引入了录音组件但从未调用云打包的时候树摇优化可能把相关代码当成无用代码剔除权限也就不会被识别。建议在代码里加一个权限检查函数打包前先在真机上跑一次最小录音流程再确认权限申请弹窗是否正常。如果还是没有权限就要考虑用自定义基座调试而不是用标准基座因为标准基座包含的权限集合跟自定义基座不一定完全一致。另外权限申请时机也很重要。不要在 App 启动时一次性申请所有权限用户会反感和拒绝。按功能模块分步申请比如用户点“录音评价”时才申请麦克风权限点“扫码”时才申请相机权限。App 端可以用 plus.android.requestPermissions 主动申请参数里要带上权限说明说明写得越具体用户接受率越高。4.4 弹出层打开时底部页面滚动穿透同城项目里经常用弹出层展示地址选择、优惠券、服务说明。在 H5 和小程序里弹层打开时底部页面依然可以滚动这就是“滚动穿透”。解决办法网上很多常规的是在弹层打开时给页面加overflow: hidden但小程序里页面根节点是 page这个方案有时不生效。我后来用了一招比较稳的弹层打开时在页面最外层容器上加一个 class设置height: 100vh; overflow: hidden弹层关闭时移除这个 class。注意在小程序里要把page的高度设置为100%否则100vh在部分安卓微信版本里会有底部安全区计算问题。App 端如果遇到穿透可以直接用plus.navigator.setStatusBarStyle配合原生层拦截或者把弹层做成半透明遮罩并监听 touchmove 事件执行 preventDefault。但要注意不要给 document 全局绑定 touchmove preventDefault这次会同时封杀掉弹层内部的滚动比如优惠券列表就没法滑了。正确做法是只在遮罩层上监听用事件来源判断是否允许滚动。一个容易踩的坑Vue 的 transition 动画会使遮罩层的 touchmove 监听在动画结束后才生效所以动画期间底部页面可能会“抖一下”。我的经验是把 touchmove 的监听放在打开弹层的同一帧就绑定而不是等 transition 的 afterEnter 回调再绑定。4.5 父子组件样式穿透与 scoped 处理UniApp 的 Vue 单文件组件默认支持 scoped 样式但它跟普通 Web 项目一样遇到需要修改子组件内部样式时会受限于 scoped。同城项目里最典型的是第三方组件库的样式定制比如要给一个城市选择弹层改主题色或者给支付面板调整按钮圆角。常规做法是使用::v-deep别名deep选择器。但 UniApp 在不同编译端对深度选择器的支持程度不一样H5 端和小程序端表现良好App 端在 Vue2 版本里偶尔需要写成/deep/。我觉得最简单的规避方式尽量给子组件预留样式变量或 className 插槽不要在父组件里强行穿透三层。比如自己开发的组件props 里加一个customClass组件根节点拼接这个类这样既优雅又不会因为编译平台不同导致样式失效。如果必须穿透可以这样写style langscss scoped .parent-class :deep(.child-class) { color: #ff6600; } /style注意:deep()括号里不要带父级前缀否则编译到小程序端会出现层级过深的问题。另外H5 端在部分浏览器上:deep()编译出来的选择器优先级不够必要时可以通过!important兜底但不要滥用。样式穿透是典型的“能不用就不用”的方案能用组件属性就优先组件属性。4.6 sqlite、本地缓存与离线能力O2O 项目对网络依赖很高但用户在地下停车场、地铁里经常断网。同城骑手端和配送端尤其需要离线能力热词里提到“uniapp sqlite”这更多出现在 App 场景。UniApp 的 App 端可以在 plus.sqlite 的 API 上封装一些轻量本地存储但说实话对于大多数中小型 O2O 项目用uni.setStorageSync配合 JSON 存储就够了。我在骑手端做过一个“离线订单缓存”的功能骑士接单后把订单的核心字段订单号、取货地址、配送地址、预计送达时间写入本地 storage。网络断开时骑手仍然能查看这些订单点“拨打电话”或者“上报异常”时先入本地队列网络恢复后统一上报。这里不能用 sqlite因为 SQLite 的 API 在 UniApp 里属于 plus 扩展H5 端没对应能力。用 storage 做 KV 存储配合队列发送全端兼容逻辑还更简单。小程序端有 wx.setStorage 的大小限制单个 key 最大 1MB但同城项目的订单列表对象通常只有几十 KB完全够用。只要注意不要把一个 list 数组无限追加塞进同一个 key 就行按订单 ID 拆分存储定期清理过期数据。这个方案在 H5 端也可以直接用 localStorage行为一致。当然如果你要处理的数据量真的很大比如骑手一天几千条轨迹点那么存储格式就得换。此时建议把轨迹点先聚合成批量数据每分钟或每十秒写入一次 storage而不是每秒钟写一次。轨迹压缩做一层抽稀只保留关键拐点否则用户的手机存储会被日志吃空这也是很多地图轨迹组件容易忽略的性能问题。4.7 小米等安卓应用市场上架的合规要点上架安卓应用市场是不少新手最容易卡壳的一环。你以为云打包出来一个 APK 就能上传实际上各应用市场对隐私政策、权限声明、备案要求都有严格规定。同城 O2O 项目必然会申请定位、相机、存储、麦克风权限这些都属于敏感权限应用市场要求你提供真实的隐私政策地址并且在 App 内展示和可撤回。我踩过的坑主要有三个应用市场要求“隐私政策弹窗”必须在 App 首次启动时展示用户同意后才能进入主界面和调用相关系统能力。所以 App.vue 的 onLaunch 里不要直接去获取定位或申请权限先弹隐私政策授权窗用户点了同意再初始化地图定位、推送、统计 SDK。小米应用市场要求 App 必须具备“账号注销”功能入口要清晰注销流程要真的能走通不能只放一个客服电话。如果 App 里接了支付宝或微信支付要确保支付功能在审核期间可以被测试到否则审核人员可能以“功能不可用”为由拒绝。我一般会在隐私政策弹窗之后加一个“游客浏览模式”允许审核人员先看到核心页面而不是被登录页挡死。这些合规细节跟代码逻辑无关但完全决定你的包能不能过审。独立部署到各个安卓市场时要针对每个市场的规范做差异化调整。应用市场审核有时就像开盲盒但基本要求前置做足后面就能节省大量来回修改的时间。5. 打包发布与持续交付经验5.1 H5 部署Nginx 配置、history 路由与缓存策略H5 打包产物部署到 Nginx有几个细节必须处理。首先是h5.router.base配置如果你的站点部署在根目录base 设为/如果部署在/app/子路径则需要设置为/app/否则打包出来的 index.html 引用 JS/CSS 的路径全部是绝对路径导致 404。路由模式建议用 history而不是默认的 hash。history 模式 URL 更干净但需要 Nginx 做 try_files 配置location / { root /var/www/o2o-h5; index index.html; try_files $uri $uri/ /index.html; }否则用户直接在浏览器地址栏访问/pages/order/detail刷新后会 404。这是 H5 独立部署最常遇到的白屏原因。静态资源缓存策略也很关键。同城业务迭代快活动页经常改不合理的缓存策略会导致用户永远看到老版本。我的做法是给带 hash 的资源JS/CSS设置Cache-Control: max-age31536000, immutable给 index.html 设置Cache-Control: no-cache确保每次发版后用户打开首页立即拉到最新入口文件。Nginx 配置片段location /static/ { expires 1y; add_header Cache-Control public, immutable; }5.2 小程序发布与版本管理微信小程序的发布流程很简单但版本管理需要严谨。开发者工具导入dist/build/mp-weixin后要做的第一件事不是预览而是先确认manifest.json里的微信小程序 AppID 是否正确。提交代码前跑一遍npm run build:mp-weixin确保产物是最新的否则就会出现“开发工具里代码改对了上传包还是旧代码”的笑话。小程序的体验版、审核版、线上版是三个独立的版本。同城项目一般会先在体验版上放给业务同事验收然后再提交审核。审核期间线上版本保持不变所以小程序线上出问题回滚方案是重新提交一个旧版本代码包而不是像 H5 那样直接改服务器静态资源。这个回滚流程要提前跟后端约定好线上问题的定位优先级也要和后端分开否则两边互相等事故时间就被拉长了。5.3 App 云打包与自定义基座调试UniApp 的云打包很方便但调试阶段强烈建议使用“自定义调试基座”。原因很简单自定义基座包含了你选择的全部原生模块地图、支付、推送、录音等真机运行时可以直接调用这些原生能力。如果只依赖标准基座部分模块不可用导致“运行时 undefined”的情况。云打包正式包时注意证书的选择。Android 用 keystore 签名iOS 用 p12 证书和描述文件。证书的别名和密码必须记录好忘了密码只能重新生成证书但重新生成证书后已经在应用市场上架的老应用无法直接覆盖安装需要做升级适配。这个坑我见过不止一次一定要把证书文件纳入团队密码管理。另外云打包时“广告标识”和“推送通道”这些配置如果不是业务必须建议不要勾选否则打包产物会变大还可能会收到平台推送 SDK 的合规提醒。O2O 项目如果需要消息推送我建议优先使用厂商通道小米、华为、OPPO、vivo加个自建推送服务的组合而不是依赖打包平台内置的推送这样在离线送达率和自定义消息上更可控。结尾一点实际体感从项目启动到现在我最大体感是UniApp 的“独立部署”和“二次开发”能力在同城 O2O 这个品类里其实是超出预期的。小程序、H5、App 三端之间真正需要大改的通常只是支付、地图、分享这类原生能力调用段而业务逻辑、页面交互、接口封装这些核心代码几乎可以做到一套走完。但反过来说UniApp 不会替你处理业务上的复杂状态比如订单状态机、配送超时、骑手和用户的实时位置同步这些还是得老老实实按照传统前端的工程规范来做。还有一个小经验分享给后来者在启动这种多端项目时一定要先把 manifest 和 config 这套“环境基础设施”梳理清楚。我见过很多团队在项目初期图省事域名写死在各个页面里打包时靠全局替换来切换环境结果到了多城市部署时彻底失控回滚一次要花半天。如果你打算长期维护一套同城 O2O 系统第一天就做好多环境配置管理绝对是最值得花的时间。最后再补充一个技巧发布 H5 之前务必检查一遍h5.router.base以及静态资源路径里是否带上了 CDN 前缀。很多时候“换个域名部署就白屏”的问题根本不是代码问题而是资源路径四舍五入写死了。把 base 和 CDN 路径抽象成环境变量之后再遇到迁移服务器或者新增城市节点你只需要改一行配置剩下的交给构建流水线就好。
返回列表