
你有没有遇到过这种情况——听小程序里的课程音频,按右上角叉叉切去回微信消息,结果声音还在哗哗响;或者深夜把手机切成暗黑模式,再切回小程序,界面还是亮瞎眼的大白底;又或者用户从朋友圈点开你分享的旧版本卡片链接,路径早就改了,结果小程序直接白屏,你后台监控啥也没捞到。
这三个"翻车现场",根子都在同一个地方:app.js 里 App() 的那几个生命周期回调,你没用对或用漏了。微信官方文档说得很明白——"App() 必须在 app.js 中调用,必须调用且只能调用一次",它接受一个 Object 参数,里面能挂的生命周期回调正是 onLaunch / onShow / onHide / onError / onPageNotFound / onThemeChange 这六个 。今天我们把这六个兄弟挨个拆开,讲清各自什么时候触发、该干什么、不该干什么。
一、六个回调的角色分工
小程序启动到销毁的整个过程里,这六个回调各管一段。先给一张总览表,心里有个数:
回调 | 触发时机 | 次数 | 最低版本 |
|---|---|---|---|
| 小程序初始化完成时 | 全局仅一次 | — |
| 小程序启动,或从后台进入前台 | 至少一次 + 每次切前台 | — |
| 小程序从前台进入后台 | 每次切后台 | — |
| 脚本错误或 API 调用报错 | 出错时 | — |
| 要打开的页面不存在 | 命中时 | 1.9.90 |
| 系统切换主题(浅色/深色) | 切主题时 | 2.11.0 |
数据来源:微信开放文档 。
(1)onLaunch:应用出生证,全局唯一一次
触发时机:小程序初始化完成时触发,全局只触发一次。启动参数也可以通过 wx.getLaunchOptionsSync() 获取,跟 onLaunch 的参数是同一份 。
这是整个小程序运行期最早执行的那段 JS。它该干的事就一条标准:全局初始化、且只做一次。
// app.jsApp({onLaunch(options) {console.log('启动路径:', options.path)console.log('场景值:', options.scene) // 1011=扫码, 1007=会话, ...console.log('启动参数:', options.query)// ① 云开发初始化wx.cloud && wx.cloud.init({ env: 'your-env' })// ② 登录态初始化this.initLogin()// ③ 读取本地缓存的用户信息,挂到 globalDataconst userInfo = wx.getStorageSync('userInfo')if (userInfo) this.globalData.userInfo = userInfo},async initLogin() {const { code } = await wx.login()const res = await wx.request({url: 'https://api.example.com/auth/login',data: { code }})this.globalData.token = res.data.tokenwx.setStorageSync('token', res.data.token)},globalData: { token: '', userInfo: null }})
📌 进阶优化建议:
onLaunch里别塞太多同步重逻辑。小程序启动到首屏渲染有时间要求(性能评分里"首屏渲染耗时"是硬指标),狂await多个接口会拖慢首屏。登录态这种必须的可以放这,但非阻塞的(启动日志上报、配置预拉取)建议拆出去用Promise并行或延后处理。
(2)onShow:每次"露脸"都触发,比 onLaunch 勤快
触发时机:小程序启动,或从后台进入前台显示时触发 。注意"从后台切回前台"也会触发——所以 onShow 的触发次数 ≥ onLaunch。
它适合放"每次应用可见都要刷新"的逻辑:
检查登录态是否过期,过期则刷新
token重新拉取用户最新信息(昵称头像可能在别处改了)
重启在
onHide里暂停的定时器、音视频播放
App({onShow() {this.checkTokenExpiry() // 每次切前台都检查this.resumeAudio() // 续播}})
💡 判断窍窍:"只做一次"的放 onLaunch,"每次露脸都要"的放 onShow。新人常犯错误是把"检查更新""拉用户信息"放到 onLaunch,结果小程序切后台再回来,用户信息改了但界面没同步——该放 onShow 的逻辑错放到了 onLaunch。
(3)onHide:切后台的善后
触发时机:小程序从前台进入后台时触发 。
这里的"后台"不是 wx.navigateTo 跳去别的页面(那是页面级 onHide),而是整个小程序切到后台——用户按了右上角胶囊的"关闭"、切去微信聊天、切去别的 App。
onHide 里该干的,跟页面 onHide 是同一个思路——"不可见就要停"的资源:
App({onHide() {clearInterval(this.timer) // 清定时器this.timer = nullwx.pauseBackgroundAudio() // 暂停背景音wx.setStorageSync('lastActiveTime', Date.now()) // 存状态防销毁丢失}})
⚠️ 一个关键认知:小程序"切后台"不等于"被销毁"。官方机制是进入后台后并不会立即销毁,而是存活一段时间(资源紧张或长时间不回才会真销毁)。所以 onHide 里别做"清空 globalData"这种激进操作——否则用户切回来 onShow 时数据全丢,页面上数据全空。
(4)onError:全局错误兜底,线上监控总入口
触发时机:小程序发生脚本错误或 API 调用报错时触发 。
这是很多项目最容易忽略、但线上价值极大的一个回调。新手 app.js 里基本不写 onError,结果用户侧脚本一报错,开发者完全无感——直到应用商店飘来一星差评"点 XX 功能就白屏"。
App({onError(msg) {console.error('全局错误:', msg)// 上报到监控平台wx.request({url: 'https://log.example.com/report',method: 'POST',data: {error: msg,route: getCurrentPages().pop()?.route || 'unknown',time: Date.now(),systemInfo: wx.getSystemInfoSync()}})}})
📌 进阶优化建议:onError 是应用级错误兜底,但有两个局限:① 它只能拿到
String error(包含堆栈的错误信息),粒度有限;② 未捕获的 Promise 拒绝它管不到——这个要交给onUnhandledRejection(基础库 2.10.0+)。生产环境建议双管齐下:
App({onError(msg) { reportError('script_error', msg) },onUnhandledRejection(e) { reportError('unhandled_rejection', e.reason) }})
(5)onPageNotFound:路由兜底的"安全网"
触发时机:小程序要打开的页面不存在时触发(基础库 1.9.90 开始支持,低版本需做兼容处理)。
典型场景:分享卡片的 path 指向 /pages/old-detail?id=123,但某次发版你把路由改成了 /pages/goods-detail,老用户点开分享卡片 → 目标页不存在 → 触发 onPageNotFound。
正确用法是在回调里同步重定向(异步无效):
App({onPageNotFound(res) {const { path, query, isEntryPage } = resconsole.warn('页面不存在:', path, query)// 同步重定向到兜底页if (isEntryPage) {wx.redirectTo({ url: '/pages/index/index' })} else {wx.redirectTo({ url: '/pages/not-found/not-found' })}}})
⚠️ 三个硬规则:① 重定向必须是同步的,
setTimeout里异步wx.redirectTo无效;② 如果重定向到的页面也不存在,会推入微信原生"页面不存在"提示页,且不再回调 onPageNotFound——所以兜底页一定要存在;③ tabBar 页面要用wx.switchTab而不是wx.redirectTo。
📌 进阶优化建议:把
onPageNotFound当成"路由层的安全网",所有动态 path(分享卡片、二维码扫码、模板消息跳转)都要在这里做兼容——老路径映射到新路径、非法参数重定向到首页。这是线上"白屏率"优化最划算的一笔投资。
(6)onThemeChange:暗黑模式跟随的系统钩子
触发时机:系统切换主题(浅色/深色)时触发(基础库 2.11.0 开始支持)。
这是六个回调里最年轻的一个,也是让小程序"看起来很专业"的关键——用户手机切暗黑模式,你的小程序界面跟着切,体验立刻拉满。
App({onThemeChange(res) {console.log('当前主题:', res.theme) // 'dark' | 'light'this.globalData.theme = res.theme// 通知当前页面刷新主题const pages = getCurrentPages()const currentPage = pages[pages.length - 1]currentPage && currentPage.onThemeChange && currentPage.onThemeChange(res.theme)}})
配合 CSS 变量做主题切换:
/* app.wxss */page {--bg-color: #ffffff;--text-color: #333333;}page.dark {--bg-color: #1a1a1a;--text-color: #e0e0e0;}
💡 一个完整方案:在
onShow里用wx.getSystemInfoSync().theme读当前主题初始值,在onThemeChange里监听变化,在页面onLoad时根据globalData.theme给页面根节点加dark类。三处配合,暗黑模式就稳了。
⚠️ 注意:
onThemeChange需要基础库 2.11.0+ ,低版本小程序里不会触发,要做兼容:低版本用wx.getSystemInfoSync().theme读初始值,配合wx.onThemeChange(带 on 的全局监听)兜底。
二、六个回调的时序全景
把六个回调放在"小程序运行机制"里看,会更清晰:
用户点击打开小程序↓onLaunch (全局唯一一次)↓onShow (第一次露脸)↓┌──────────────────────────────────┐│ 小程序在前台运行 │└──────────────────────────────────┘↓ (用户切去微信聊天 / 按关闭)onHide (进入后台,并未销毁)↓ (用户再次切回)onShow (从后台回前台)↓┌──────────────────────────────────┐│ 循环:onShow ↔ onHide │└──────────────────────────────────┘↓ (系统切暗黑模式)onThemeChange ('dark')↓ (脚本报错)onError (msg)↓ (打开不存在的页面)onPageNotFound (res)↓ (系统资源紧张 / 长时间未回)小程序被销毁(无应用级 onUnload)↓ (用户再次打开)onLaunch 再次触发(新一轮生命周期)
几个关键点:
onLaunch 不是"每次打开都触发",而是"每次小程序初始化(冷启动)触发一次";热启动(从后台切回)只走 onShow。
小程序"销毁"后再次打开,会重新走 onLaunch——所以 onLaunch 里的初始化逻辑必须幂等(多次执行不产生副作用)。
onHide 不等于销毁:小程序进入后台后并未立刻终止,而是进入后台状态存活一段时间,资源紧张或长时间未打开才会真销毁。所以 onHide 里别清 globalData。
三、把 App 生命周期放回知识网
串一下前面几篇的线索:
App 实例与 getApp 篇:
App()在这里注册出那个"全局唯一实例",getApp()在页面里拿到它——本篇讲的是 App() 内部六个回调怎么用。页面生命周期篇:页面有 onLoad/onShow/onReady/onHide/onUnload 五个,应用有 onLaunch/onShow/onHide/onError/onPageNotFound/onThemeChange 六个——应用级 onShow/onHide 和页面级 onShow/onHide 是两套独立触发的:切后台时先触发页面 onHide 再触发应用 onHide;切回前台时先触发应用 onShow 再触发页面 onShow。
setData 篇:App 实例上没有
setData(setData 是 Page/Component 的方法),App 里改全局状态直接this.globalData.xxx = yyy就行,不需要走那座桥。
参考文献
[1] 微信开放文档. App(Object object)[EB/OL]. https://developers.weixin.qq.com/miniprogram/dev/reference/api/App.html.
[2] 微信开放文档. 小程序运行机制[EB/OL]. https://developers.weixin.qq.com/miniprogram/dev/framework/runtime.html.
六个回调各管一段:onLaunch 做一次性初始化、onShow 做每次露脸的刷新、onHide 做切后台的善后、onError + onUnhandledRejection 兜底全局错误、onPageNotFound 兜住路由异常、onThemeChange 跟随系统主题切换——这套组合拳吃透,小程序的应用级架构就有了稳固的地基。下次有人问"切后台音乐为啥还在播",你知道该去 onHide 里 pause 了。
互动话题 🎯
你们项目的 app.js 现在把六个回调都用上了吗?是只写了空 onLaunch 的"毛坯房",还是已经把错误上报、页面不存在兜底、暗黑模式跟随都经营起来的"精装房"?有没有踩过 onLaunch 里塞太多同步逻辑拖慢首屏、或者 onPageNotFound 里异步重定向无效这种坑?评论区晒一下,抽两位朋友送一份我整理的《App 生命周期最佳实践模板》(含错误上报 + 路由兜底 + 暗黑模式全套代码)📋。
夜雨聆风