
各位产品、运营、还有深夜对着红屏报错「App() must be called…」的攻城狮朋友们,大家好。我是做小程序架构的老工程狮 👋
你有没有遇到过这种场面:测试同事换了个入口场景——比如从分享卡片、扫码、或通知中心再次唤起小程序——结果页面一闪白,控制台多了一行刺眼的提示,逻辑像断了线:登录态没续上、全局数据清零、埋点重复计数。追到最后,根子都在 app.js:要么没注册,要么注册了不止一次,要么把“初始化”当成了“随缘异步”。
微信官方文档把这件事讲得很硬,但很关键:
每个小程序都需要在 app.js 中调用 App() 方法注册小程序实例,绑定生命周期回调、错误监听和页面不存在监听函数等;并且 App() 必须在 app.js 中调用,必须且只能调用一次,不然会出现无法预期的后果。
今天咱们不搞背诵,把这行话拆开:它到底在“注册”什么、生命周期怎么跟用户前台/后台对应、onError/onPageNotFound 怎么兜底、以及我见过最毁稳定性的几种写法怎么改。
一、App() 不是“随便执行个函数”,而是把程序交出去
(1)小程序只有一个“全局实例”,入口不是 main(),是 App()
网页里你可以 N 个 script 标签、N 次 new Vue(),谁先ready谁说了算。
小程序逻辑层是一份 JS 文件从头跑(更像 ServiceWorker),框架要求你在 app.js 里显式声明:
js
// app.jsApp({onLaunch(options) {},onShow(options) {},onHide() {},onError(msg) {},globalData: { token: '', userInfo: null }})
这里的 App() 不是帮你“创建对象”,而是注册程序:框架读到 app.js 执行到这一句,才真正把小程序全局实例挂起来,生命周期管线才开始转。
所以它才咬死两条:
必须在 app.js(不让你把“程序注册”藏到 pages/ 里某个文件、或按需懒加载)
只能一次(不是“建议单例”,是硬性约束——二次调用直接给你“未定义行为”警告)
(2)为什么“只能一次”不是小题大做
如果允许多次注册,就会变成:
A 页逻辑 require 了别的模块,模块侧又偷偷 App();热重载/分包加载路径再一抖——全局实例被覆盖、生命周期重复绑、globalData 被重置……用户侧表现就是:切后台再回来状态丢了、onLaunch 突然跑两遍、甚至白屏。
所以这条铁律本质是在保护一件事:
全局状态与全局管线,只能有一条,且必须在框架能找到的地方(app.js)钉死。
二、生命周期:不是“页面加载”,是“程序活着”的三段节奏
很多团队把 onLaunch 当“页面 onLoad 的放大版”,结果把接口、埋点、一次性重算全塞进去,导致首屏被拖慢。看清语义,比背时机更重要。
(1)onLaunch:全局只一次,干“一次性基建”
官方定义:小程序初始化完成时触发,全局只触发一次。
参数 options里你最该关注的不是“用户是谁”,而是入口场景:scene / query / path / referrerInfo——它回答了“这趟小程序是怎么被打开的”(扫码?分享卡?通知?)。
我建议的纪律:
✅ 轻量探测:scene 分发、灰度开关、版本/渠道记录、隐私协议是否已同意
❌ 别在这里写“必须等到接口回来才让首页渲染”的同步锁——你正在拖冷启动
js
App({onLaunch(options) {// 正确姿态:记录入口信息,而不是卡住世界this.globalData.launchScene = options.sceneif (!wx.getStorageSync('PRIVACY_AGREED')) {// 进阶建议:这里只做“事实记录”,真正弹窗/跳转最好交给首页 onShow 判断}},globalData: { launchScene: 0, token: '' }})
(2)前台/后台的真实定义:不是“关了”,是“离开微信前台”
文档给的前后台定义很实用:关闭/Home 键离开微信并不意味着销毁,常见是进入后台;只有在后台太久或资源高才真正销毁重来。
对应到回调就是:
onShow:启动 / 从后台回到前台 → 适合做“每次可见都要拉”的轻量刷新
onHide:前台切后台 → 适合做“离开前保存/停止定时器/结束不必要轮询”
老工程狮经验:
别在 onShow 里无脑 wx.request刷一大坨不变数据,否则用户每次切回来都白等一轮;先把“是否需要刷新”的条件想清楚(时间戳阈值/脏标记/对比版本号)。
三、onError + onUnhandledRejection:别让线上事故“静默”
(1)onError(msg):脚本错误 / API 调用报错的统一出口
官方说明:小程序发生脚本错误或 API 调用报错时触发,并带上错误信息。
它最适合做的事只有一件:把它变成证据——打本地日志/写到上报队列,而不是弹框吓用户。
js
App({onError(msg) {// 1) 本地先留痕(环形队列防爆)try {const logs = wx.getStorageSync('_err_q') || []logs.push({ t: Date.now(), msg: (msg||'').slice(0,800) })if(logs.length > 20) logs.shift()wx.setStorageSync('_err_q', logs)} catch(e) {}// 2) 异步上报(别用 await 阻塞生命周期)wx.request({ url: 'https://log.xxx.com/err', method:'POST', data:{ msg } })}})
(2)onUnhandledRejection:Promise 的“最后防线”
从基础库 2.10.0 起支持:Promise 拒绝但没人 catch 时,会到这里。
它往往比 onError 更隐蔽:代码没语法炸,但逻辑“悄悄失败”(比如接口封装成 Promise 后忘记 then/catch)。
进阶建议:
这两个钩子定位是“兜底日志/告警”,别在里面再跑一套会抛错的复杂链路,不然日志系统自己就循环了。
四、onPageNotFound:把“404”变成可控的降级,而不是用户一脸懵
官方:小程序要打开的页面不存在时触发(比如活动页下线、路径拼错、动态 url 里 page 写错)。
js
App({onPageNotFound(res) {// res.path / res.query 能看到它想去哪// 如果是 tab 页,要用 switchTab;普通页才 redirectTowx.redirectTo({ url: '/pages/home/index' })}})
进阶建议(建设性):
对业务而言,onPageNotFound 的价值是保护转化路径——让用户掉到首页/兜底页,而不是直接卡死白屏。同时把 res.path上报,你才能发现:到底是代码发布的路径改漏了,还是运营分享卡里带了过期页面参数。
五、globalData 与 getApp():能碰,但得定规矩
(1)globalData 是“全局可见储物柜”,不是“随便塞的杂物间”
适合放:登录态 token、脱敏后的 userId、环境开关、launchScene。
不适合放:整页列表数据、表单草稿、UI 状态(这些应在页面/组件 data)。
(2)getApp() 的两条红线(官方也点到为止)
官方提醒过:
不要在 App() 内定义的函数里、或 App 调用前 调用 getApp()
拿到实例后,不要私自调用生命周期函数
人话版:
getApp()用来读全局状态/工具函数就够了;别把它当“跨页通信总线”到处 getApp().globalData.xxx=yyy然后指望别人知道你改了啥。要通信就走事件/状态管理/显式函数,别暗箱。
六、最常见的“毁约”写法(以及怎么收回来)
翻车 1:把 App() 包进条件/异步里
有人写:
js
// ❌ 错误氛围if (someFlag) { App({...}) }// 或setTimeout(() => App({...}), 0)
结果框架在“预期注册期”没见到实例,后续行为就看运行时脸色了。
✅ 收回来:App() 放 app.js 顶层同步调用,内部再按条件走分支。
翻车 2:在 app.js 里引了某个 page 级模块,模块又偷偷依赖 App 已存在
形成“先有鸡还是先有蛋”的脆弱链。
✅ 收回来:app.js 只做全局初始化;页面级逻辑不反向依赖 app.js 的执行时序,依赖只通过 getApp()读 globalData/方法。
翻车 3:onLaunch 里塞重同步任务,导致首屏被拖
表现为:低端机白屏时间明显变长。
✅ 收回来:把“必须刷新的数据”做成可等待令牌/脏标记,首页 onShow 自己决定要不要刷,onLaunch 只负责写标记。
七、结束语
App() 看起来就一行,却是小程序整个生命周期管线的唯一开关:它在 app.js、只能一次、它绑定 onLaunch/onShow/onHide 的节奏、它也给你 onError/onUnhandledRejection/onPageNotFound 的兜底口子。
你守好它的纪律——不二次调、不同步锁死、不暗箱全局写——全局状态就稳,首屏就轻,用户从入口到成交的路径就不容易“莫名其妙断线”。
参考文献
[1] 微信团队. 逻辑层 App Service——注册小程序(每个小程序都需要在 app.js 中调用 App 方法…)[EB/OL]. (更新日期见页面底部版本信息)[2024-06-16 引用确认]. https://developers.weixin.qq.com/miniprogram/dev/framework/app-service/app.html.
[2] 微信团队. App(Object object)(App() 必须在 app.js 中调用,且只能一次;参数:onLaunch/onShow/onHide/onError/onPageNotFound/onUnhandledRejection…)[EB/OL]. (更新日期见页面底部版本信息)[2024-06-16 引用确认]. https://developers.weixin.qq.com/miniprogram/dev/reference/api/App.html.
[3] GB/T 25000.51-2016, 系统与软件工程 系统与软件质量要求和评价(SQuaRE) 第51部分:就绪可用软件产品(RUSP)的质量要求和测试细则 [S]. 北京:中国标准出版社,2017.
💬 互动话题
你们项目里 onError / onPageNotFound 现在有没有接上报?还是说至今还是 console.log躺平版?评论区留一句你见过最“安静爆炸”的线上事故(白屏/状态清零/分享卡打不开),我挑几个典型,顺着 App() 的时序给你画一张「从入口场景→onLaunch→onShow→兜底」的落地 Checklist,照着补齐,入口稳定性基本就锁住了👇
夜雨聆风