乐于分享
好东西不私藏

切后台音乐还在播?App 生命周期六个回调讲透

切后台音乐还在播?App 生命周期六个回调讲透

你有没有遇到过这种情况——听小程序里的课程音频,按右上角叉叉切去回微信消息,结果声音还在哗哗响;或者深夜把手机切成暗黑模式,再切回小程序,界面还是亮瞎眼的大白底;又或者用户从朋友圈点开你分享的旧版本卡片链接,路径早就改了,结果小程序直接白屏,你后台监控啥也没捞到。

这三个"翻车现场",根子都在同一个地方:app.js 里 App() 的那几个生命周期回调,你没用对或用漏了。微信官方文档说得很明白——"App() 必须在 app.js 中调用,必须调用且只能调用一次",它接受一个 Object 参数,里面能挂的生命周期回调正是 onLaunch / onShow / onHide / onError / onPageNotFound / onThemeChange 这六个 。今天我们把这六个兄弟挨个拆开,讲清各自什么时候触发、该干什么、不该干什么。


一、六个回调的角色分工

小程序启动到销毁的整个过程里,这六个回调各管一段。先给一张总览表,心里有个数:

回调

触发时机

次数

最低版本

onLaunch

小程序初始化完成时

全局仅一次

onShow

小程序启动,或从后台进入前台

至少一次 + 每次切前台

onHide

小程序从前台进入后台

每次切后台

onError

脚本错误或 API 调用报错

出错时

onPageNotFound

要打开的页面不存在

命中时

1.9.90

onThemeChange

系统切换主题(浅色/深色)

切主题时

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()    // ③ 读取本地缓存的用户信息,挂到 globalData    const 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.token    wx.setStorageSync('token', res.data.token)  },  globalData: { token''userInfonull }})

📌 进阶优化建议: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 = null    wx.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,        routegetCurrentPages().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 } = res    console.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 生命周期最佳实践模板》(含错误上报 + 路由兜底 + 暗黑模式全套代码)📋。