十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

UniApp助眠小程序实战:跨端开发、音频支付与上架全记录

UniApp助眠小程序实战:跨端开发、音频支付与上架全记录 做这个助眠小程序之前我其实纠结了很久要不要直接用原生微信小程序开发。后来项目上线、迭代、被用户催着加功能再回头看当初选择 UniApp 这个决定真的是救了大命。这篇文章我就把这个项目的完整思路、技术实现、踩坑记录全盘托出来涉及从架构设计到支付对接、从自定义分享到安卓上架的各种细节适合准备做微信小程序或者正在用 UniApp 跨端开发的同行参考。先说清楚这个项目是干嘛的。这是一款基于微信小程序的助眠小程序核心解决的是当代人入睡困难、睡眠浅、睡前焦虑这几个问题。产品形态上它包含了场景音播放雨声、海浪、篝火、白噪音、睡眠数据记录、冥想引导音频、定时关闭等模块同时接入了微信支付做会员订阅。整套代码用 UniApp 开发一套代码同时编译成微信小程序、H5 和 Android App这也是我最终选 UniApp 的最主要原因。我当时设想的用户画像很清晰晚上躺床上刷手机、越刷越清醒的年轻人以及长期出差、认床、需要环境音掩盖噪音的上班族。这篇文章我不会只讲“怎么调用 uni.createInnerAudioContext”这种 API 层面的事我会把每个关键决策背后的“为什么”也讲清楚。比如为什么播放器不用多个音频实例、为什么分享逻辑要单独抽出来、为什么 iOS 支付要格外小心。有些经验是普通文档里查不到的都是真金白银换来的教训。1. 项目定位与整体技术选型为什么是 UniApp 而不是原生1.1 助眠小程序的用户需求与核心功能拆解助眠类产品看着简单好像放几个白噪音就行实际上要做成一款可以持续运营的小程序功能盘子需要仔细设计。我当时把需求拆成了三层第一层是基础工具属性也就是场景音播放、定时关闭、连续播放这是用户每天都会用的高频功能第二层是数据与个性化比如记录入睡时间、睡眠时长生成周报曲线让用户看到自己的改善第三层是商业化闭环包括会员订阅、精品课程解锁、打赏主播如果是 UGC 音频的话。这个拆解直接影响了我后面的技术架构。工具层要保证音频切换零卡顿所以音频播放器必须用单例模式管理数据层要兼顾实时展示和离线可用所以本地存储用了 SQLite图表展示引入了 ECharts商业化层则牵扯出微信支付、订单回调、会员权益校验一系列逻辑。三个层次对应的是完全不同的技术难点如果一开始不分层全部写在一个页面里后期基本就是重构的命。1.2 为什么选 UniApp一套代码多端复用的实际账本我见过不少团队坚持用原生开发微信小程序理由无非是“原生性能最好”“生态最成熟”。这话没错但前提是你的项目只做微信小程序一个端。我这个项目从一开始就考虑要做 App 和 H5——微信小程序受限于平台规则某些诱导分享、虚拟支付功能容易被限制一旦小程序违规导致支付功能暂时无法使用你连退路都没有。有一段时间我们的小程序就碰上了支付暂不可用的问题因为 iOS 虚拟支付政策调整我们连夜把 App 端的支付链路顶了上去才没有导致会员收入断崖。这就是 UniApp 的核心价值。你只需要写一套 Vue 代码通过不同的编译配置输出到微信小程序、App、H5 三端。尤其是 HBuilderX 的云打包能力让我一个没有 Mac 的开发者也能打出 iOS 包这在原生开发时代是不可想象的。当然跨端不是零成本后面我会详细讲哪些 API 需要做条件编译、哪些坑只有写小程序端才会遇到。1.3 目录结构与工程初始化要点我建议你在新建 UniApp 项目之后第一时间把目录结构按业务模块重新整理不要用默认的 pages 堆积所有页面。我的项目实际结构是这样拆的/pages只放微信小程序端页面比如pages/index/index、pages/sounds/detail/components公共组件包括自定义导航栏、音频播放悬浮条、隐私政策弹窗/api接口请求统一封装区分 wx.request 和 uni.request/utils工具函数包括时间格式化、音频管理单例、路由参数解析/storeVuex 或 Pinia 状态管理用户信息、会员状态、当前播放状态放这里/static静态资源音频文件推荐放 CDN不塞进包里这个结构的好处是你后续加一个“睡眠报告”模块只需要新增页面接口组件三个维度不会污染现有逻辑。特别是音频播放这种全局状态如果放在页面 data 里一旦页面被销毁播放就断了用户会骂娘的。音频状态必须放在全局 store页面的onUnload只负责暂停 UI 状态不销毁播放实例。2. 核心功能模块的设计与实现2.1 场景音播放与音频生命周期管理场景音是助眠小程序最核心的功能也是技术细节最多的模块。很多新手会把每个音频都new一个InnerAudioContext这是大忌。多个音频实例同时存在会互相抢占资源切换时经常出现上一个声音还在响、新的已经播了的情况体验极其糟糕。我在项目里用一个audioManager.js单例统一管理内部只维护一个innerAudioContext实例每次切换音频时先stop()再重新src 新地址。这里有一个关键参数obeyMuteSwitch。默认情况下微信小程序的音频播放会受手机静音键控制但助眠场景要求用户锁屏后音频还要继续播放就必须在音频初始化时把obeyMuteSwitch设为false。我记得第一次真机测试时用户反馈“锁屏就没声了”排查了很久才发现是这个参数默认值的问题。实测在 iOS 和 Android 上必须区分处理部分 Android 机型即使设置了obeyMuteSwitch也不完全生效还需要配合后台播放能力。定时关闭功能是我自己最常用的功能实现逻辑不复杂但要注意 setTimeout 在 App 切后台后可能被系统冻结。我在项目里用setTimeout 本地时间戳双重校验用户设定 30 分钟后关闭真正执行时先判断当前时间是否超过设定的时间戳防止定时器提前或延后触发。另外关闭动作要放在前台确保onHide期间不会误关音频用户切出去回个消息回来发现音乐停了一样要骂人。2.2 睡眠数据采集与图表展示SQLite 与 ECharts 的组合睡眠数据模块我最初想直接调后端接口每次进页面实时拉取。后来发现一个很现实的问题用户凌晨睡不着打开小程序很多场景是断网的或者不想开流量如果数据全部依赖后端体验会很差。所以我采用了本地优先的策略每一次入睡、醒来、翻身通过加速度计粗略判断的关键时间点先写入本地 SQLite再在 WiFi 环境下批量同步到服务端。UniApp 的 SQLite 能力在新版本中是通过plus.sqlite提供的App 端和wx.setStorage模拟的小程序端。微信小程序本身没有真正的 SQLite我是用 key-value 存储模拟表结构比如sleep_record_202501存一个月的数据。这里要提醒一下小程序本地存储有 10MB 上限如果睡眠数据每天都记一年下来可能撑爆。我建议只保留最近 3 个月的原始记录更早的数据聚合后上传服务器本地只留统计结果。图表展示我用的是 ECharts 的echarts-for-weixin方案。 UniApp 中使用 ECharts 要注意渲染层和逻辑层的分离小程序端不能直接用 DOM必须通过canvas渲染而 App 端和 H5 端可以直接使用完整版 ECharts。条件编译是必须的// #ifdef MP-WEIXIN import * as echarts from ../../components/echarts/echarts.min // #endif // #ifndef MP-WEIXIN import * as echarts from echarts // #endif如果你不写条件编译在微信小程序里就会报window is not defined这是我被官方文档坑过一次的地方后来老实了——跨端项目凡是遇到浏览器专属 API第一反应就是条件编译。2.3 自定义分享与路由参数传递的隐藏陷阱助眠小程序天然适合做分享裂变用户分享一段“雨声伴你入眠”的卡片给朋友新用户点进来直接播放对应声景。这就要说到onShareAppMessage了。这个 API 必须在每个需要分享的页面单独定义而我在项目里为了统一处理分享文案和图片写了一个全局 mixin 里定义onShareAppMessage结果发现页面自定义的分享方法会被 mixin 中的全局方法覆盖导致不同场景分享出去的内容全部一样用户分享了一个雨声场景好友点进来却到了首页。排查这个问题花了半天。最终的解决方案是全局 mixin 里只定义默认配置不写死path页面内通过onLoad读取路由参数后在onShareAppMessage中动态生成分享路径。这里有个细节分享路径中的参数要拼接当前关键词比如pages/sounds/detail?sceneId123。好友点进来后在小程序冷启动和热启动两种场景下取参方式不同热启动用onShow的 options冷启动要用onLaunch的 query 或者onLoad的 options。我在实际测试中发现不少开发者只处理了onLoad导致用户在聊天窗口点分享卡片进入时参数正常但小程序已经打开再点分享卡片时参数就丢了一半。稳妥做法是同时监听onLoad和onShow并用一个变量缓存最新参数。还有一个小点分享图片不要太大。微信限制分享图不超过 5MB但实际超过 2MB 就经常出现图片不显示的情况。我建议先压缩到 800x800 以下控制在 500KB 以内分享成功率会显著提高。3. 平台能力与商业化细节支付、隐私与权限3.1 微信支付 v3 对接与 requestPayment 的完整流程助眠小程序如果要卖会员、精品课程微信支付是绕不开的。我项目用的是微信支付 v3 接口整体流程分成两段客户端发起下单请求到自己的后端后端调用微信支付下单接口拿到预支付交易会话标识prepay_id然后后端用商户私钥对参数进行签名返回给小程序端timeStamp、nonceStr、package、signType、paySign五个参数小程序端再调用uni.requestPayment拉起收银台。最容易踩坑的是签名算法。微信支付 v3 对签名的要求是先用商户私钥对“请求方法请求路径时间戳随机串请求体摘要”构造签名串再用 SHA256-RSA2048 进行加签。很多新手拿 Java 后端的签名代码硬套 Node.js 后端结果全部报签约验证失败。我建议签名逻辑尽量放在后端完成客户端只负责透传参数。如果后端返回的paySign一直签不对优先检查私钥是不是 PKCS8 格式微信支付后台可以下载不同格式的私钥格式不匹配的报错很隐蔽。另一个亲身经历的坑是“小程序违规支付功能暂时无法使用”。我们曾经有一段时间被平台限制了支付能力原因是在 iOS 上销售虚拟商品会员订阅属于虚拟支付。微信小程序在 iOS 端是不允许直接走虚拟支付购买虚拟内容的只能通过公众号或 App 内购。这个政策非常严格如果你的助眠会员在 iOS 端还要拉起微信支付大概率会被判违规。我的解决方案是iOS 端隐藏会员购买入口引导用户到公众号内购买或下载 App 购买。代码里通过uni.getSystemInfoSync().platform判断平台platform ios时就走替代支付流程。如果你现在正在做这类产品一定要提前做好这个设计否则审核驳回、支付限制都会找上门。3.2 隐私政策弹窗与用户不同意退出的处理微信小程序上线前隐私政策弹窗是必须的而且从 2023 年之后平台对隐私接口的管控越来越严。如果用户的隐私权限未被授权你不能调用任何涉及用户隐私的接口比如获取手机号、位置、相册等。助眠小程序虽然不像外卖那样必须拿位置但我们想做基于地理位置的“天气助眠音”推荐就绕不开位置权限。我采用的实现方案是启动时先自定义一个隐私弹窗组件弹窗展示隐私政策和用户协议底部有两个按钮“同意并继续”和“不同意退出”。如果用户点“不同意”我需要调用uni.exitMiniProgram让用户退出小程序。这里官方要求是必须提供退出机制很多开发者以为点不同意就只是停用相关功能其实平台审核时会看你的弹窗逻辑如果没有退出功能审核很容易被拒。具体代码如下methods: { agree() { this.showPrivacyModal false uni.setStorageSync(privacyAgreed, true) // 初始化需要隐私权限的功能 }, disagree() { uni.showModal({ title: 提示, content: 需要同意隐私政策才能继续使用本小程序, showCancel: false, success: (res) { if (res.confirm) { uni.exitMiniProgram() } } }) } }一个细节是uni.exitMiniProgram在 App 端不生效只支持微信小程序端。如果你的项目也要编译成 App需要在该方法外层做平台判断App 端可以改成“退出应用”或者禁用某些功能。我在真机测试时发现部分安卓手机调用exitMiniProgram会有延迟所以最好在弹窗文案上提示用户“请手动关闭小程序”体验会好很多。3.3 自定义导航栏高度与安全区域适配助眠小程序为了设计感首页做了一个沉浸式夜间星空背景顶部不能使用默认的白色导航栏必须自定义。这里就涉及一个高频问题顶部导航栏高度计算。很多小白用uni.getSystemInfoSync().statusBarHeight拿到状态栏高度之后就直接在导航栏上加上 44px结果不同机型上差异巨大有的机型胶囊按钮和标题重叠有的机型底部出现黑条。正确的做法是在onLoad里用uni.getMenuButtonBoundingClientRect()获取胶囊按钮的位置和尺寸微信小程序独有 API然后动态计算导航栏高度。胶囊按钮的 top 就是状态栏底部到胶囊顶部的距离胶囊到屏幕顶部的距离和底部留白有时候不对称我直接取胶囊按钮的 top 加上胶囊高度、再加上一个安全余量作为自定义导航栏的整体高度。因为自定义导航栏组件会在所有页面复用我把计算逻辑放在组件内部的created钩子里把结果缓存到全局 store避免每次进入页面重复计算。还有一个容易被忽略的适配点是 iPhone X 系列以上的底部安全区。播放悬浮条、上滑手势区域会被系统底部横条遮挡需要在App.vue全局样式中加入环境变量padding-bottom: constant(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom);这俩属性必须都写上constant兼容 iOS 11.0-11.2env兼容 iOS 11.2 以上。虽然是老生常谈但我见过很多上线的助眠 App 在 iPhone 14 上底部按钮被横条盖住的就是因为漏了这行样式。4. 从开发到上架调试、打包与常见问题实战4.1 HBuilderX 运行到微信开发者工具的故障排查用 HBuilderX 开发 UniApp 项目最常用的调试姿势是“运行 - 运行到小程序模拟器 - 微信开发者工具”。但这个链路经常出问题最常见的是点了运行没反应、微信开发者工具不自动打开。我排查过几轮原因通常集中在以下几点第一微信开发者工具的服务端口没开。需要在微信开发者工具的“设置 - 安全设置”里开启服务端口否则 HBuilderX 无法推送编译结果过去。第二项目路径不能有中文和特殊字符微信开发者工具对中文路径的兼容性极差有时候不报错但就是不刷新。第三HBuilderX 的编译版本和微信基础库版本不一致用最新版 HBuilderX 编译出来的代码可能用了旧基础库不支持的语法运行时会白屏。如果运行时提示“不是开发者”大概率是你的微信号没有在项目成员列表里去微信公众平台的“成员管理”里把当前微信加进去或者使用“测试号”方式临时调试。团队协作时这一点特别容易踩坑A 同事能正常运行B 同事刚接手项目因为没加为项目成员每次打开都是空白页非常浪费时间。4.2 页面交互细节滚动穿透、软键盘遮挡和视频自动暂停助眠小程序里经常用一个半屏弹窗让用户选择当前心情焦虑、平静、疲惫弹窗出来之后底层页面还能滚动这就是典型的滚动穿透问题。微信小程序的catchtouchmove在老项目中是好用的但在popup自定义组件里需要给弹窗根部加catchtouchmovetrue否则安卓机上依然会穿透。我在项目中遇到了一个更隐蔽的情况用uni-popup组件弹窗内部的 scroll-view 滚动时底层的页面跟着滚动。最终解决是监听弹窗的开关状态打开时给page添加overflow: hidden关闭时恢复。手机软键盘遮挡查询内容的痛点助眠小程序里其实也有——用户在搜索“雨声”时键盘弹出来会挡住底部的搜索按钮。uni-app中常见的做法是设置adjust-positionfalse然后监听键盘高度onKeyboardHeightChange手动调整。实测下来adjust-position在部分 Android 机的 WebView 上失效过所以最稳的方案是用 CSSenv(safe-area-inset-bottom)加底部 padding给键盘预留空间。不过这个方案在 iOS 上更可靠Android 端还是要拿到键盘高度动态计算。场景音列表里我放了一些带视频封面的引导内容冥想视频这就涉及另一个高频问题视频列表嵌套视频组件时如果一个视频在播放用户滑动到下一个旧的还在响。我的实现思路是在每个视频项上绑定videoContext页面滚动时用IntersectionObserver监测视频是否还在可视区域内一旦滑出就立刻调用videoContext.pause()。在微信小程序里可以用uni.createIntersectionObserver监听实测性能会比onPageScroll里频繁计算位置更好不会造成滑动卡顿。4.3 打包上架 Android 应用市场签名与审核注意事项这套代码要复用到 App 端HBuilderX 云打包非常方便但 Android 应用市场上架比微信小程序复杂得多。首先要准备签名证书UniApp 云打包支持使用自有证书如果没有也可以在云端生成一个测试证书。华为、小米、OPPO、vivo 这些应用市场对签名要求不同部分市场要求使用自己的签名文件不然换包之后无法覆盖安装。我在上传应用市场时遇到的一个坑是 targetSdkVersion 版本太低。早期打出来的包 targetSdkVersion 停留在 26华为市场直接拒绝上架提示必须适配 Android 12 及以上的隐私和安全要求。HBuilderX 新版可以指定 targetSdkVersion 和 minSdkVersion记得在manifest.json- App 模块配置里检查这两个参数。另外一个提示就是隐私政策网址不能留空应用市场的审核员会先访问你的隐私政策链接如果没有或者无法访问第一轮就被打回。我当时把隐私政策页面做成了小程序内的一个页面但应用市场要求网页版链接所以专门单独部署了一个静态网页。这里也涉及 iOS 审核的“用户不同意隐私政策退出”逻辑应用市场审核同样会看重代码逻辑和小程序端类似但 App 端退出使用plus.runtime.quit()退出应用。5. 反编译防范与小程序的自我保护现在互联网上存在不少针对微信小程序的反编译工具用它们可以还原出小程序的代码包括页面结构、接口地址、AppSecret如果开发时不慎写在代码里等敏感信息。助眠小程序虽然业务逻辑不算重但包含会员支付、用户隐私数据一旦被反编译出关键接口和签名逻辑攻击者可以仿造请求刷量、绕过支付校验后果很严重。还是要从上游做好防范。第一编译后的代码中不要硬编码敏感密钥。AppSecret、商户号、加密 key 等绝不能出现在前端代码里这是底线。反编译出来的代码能直接看到请求地址和参数结构如果 AppSecret 也暴露攻击者可以直接冒充后端调用微信支付接口。第二启动时对请求做签名校验每个请求带上一个由服务端派发的动态 token并且 token 过期时间缩短到 15 分钟以内。这样即使别人拿到代码也很难重放请求。第三可以适当利用微信小程序的“分包”机制把核心逻辑放到分包中提高扒代码的成本。第四代码混淆不是绝对安全但它能显著增加阅读成本市面上有小程序代码加固服务如果产品足够重要值得考虑。5.1 关于蓝牙与智能硬件的扩展联想后续版本我还计划加入蓝牙连接智能枕、智能手环的能力通过低功耗蓝牙读取用户的体动数据、心率变化辅助生成更精准的睡眠报告。UniApp 在 App 端提供uni.openBluetoothAdapter系列 API微信小程序端也有对应的蓝牙接口但两者 API 略有差异。实际开发中我发现小程序端的蓝牙 API 在 Android 兼容性上比 App 端更敏感尤其是扫描附近设备时部分机型需要开启定位权限。做智能硬件联动时权限设计一定不能想当然。助眠小程序的功能边界未来可以延伸得很远。除了智能硬件还可以做“睡前故事”UGC 社区、与天气数据结合的场景推荐、甚至接入线下助眠体验馆的预约。但不管功能怎么加技术骨架的稳定性最重要这也是我坚持用 UniApp 单工程多端发布的理由——小程序快速试错App 沉淀核心用户H5 用来做渠道投放三个端口共享一套代码运营成本能省下很大一块。6. 常见问题速查表与最终实操心得我把自己和团队在开发、测试、审核过程中查过的问题整理成了一张表希望帮你少走弯路问题现象可能原因解决方案运行到微信开发者工具没反应服务端口未开启、路径有中文设置 - 安全设置 - 开启服务端口移动项目到纯英文路径iOS 端支付拉不起来虚拟支付限制或签名错误检查是否在 iOS 上销售虚拟商品核查签名格式onShareAppMessage返回内容不一致全局 mixin 覆盖页面方法全局只做兜底配置页面内用重写方式定义分享卡片点进来参数丢失冷启动/热启动取参不通同时监听onLoad和onShow缓存参数弹窗出现后底层页面滚动滚动穿透未处理弹窗打开时page加overflow: hidden或用catchtouchmove键盘遮挡底部搜索按钮adjust-position失效监听键盘高度动态调整位置或预留底部安全距离视频列表滑动后依然播放未监测视频位置用IntersectionObserver监听滑出可视区即暂停自定义导航栏高度不准只用了statusBarHeight结合getMenuButtonBoundingClientRect计算打包后应用市场上架失败隐私政策缺失或 targetSdkVersion 低部署可访问的隐私政策网页升级 targetSdkVersion用户不同意隐私政策后应用未退出退出逻辑缺失或不生效小程序用exitMiniProgramApp 用plus.runtime.quit()开发这类跨端项目我的体会是千万不能抱着“一个 API 用到老”的心态。微信小程序、App、H5 三端对同一功能的支持程度差异巨大大到 API 名称小到一个 CSS 属性是否生效。所以编码第一原则就是区分 Web 与原生环境的差异凡是涉及浏览器 API 或原生 API 的地方第一时间条件编译不要等测试环境出 bug 再去补救。最后再分享一个藏得比较深的技巧。如果你在小程序里用到了音频播放一定要处理好onHide和onShow的生命周期。微信小程序切后台比如用户去回了一条微信再回来播放状态有时候会错乱定时关闭的剩余时间也会走不准。我的解决办法是在onHide时记录当前播放进度和剩余时间戳onShow时先判断是否超过关闭时间超过就自动停止并更新 UI没超过则从记录的进度恢复播放。有了这层保障用户晚上挂着雨声睡觉第二天早上醒来发现手机还在播放这种尴尬事基本就不会再发生了。
返回列表