
各位产品大大、运营同学,还有深夜在开发者工具里敲 console.log(window)结果拿到 undefined的攻城狮们,大家好。我是做小程序底层架构与性能优化的老工程狮。
先问一句:你第一次写小程序时,有没有下意识想用 document.getElementById抓个节点,结果报了个让你怀疑人生的错?
我见过最经典的"新人翻车现场":把 H5 项目的核心动效用 $('#id').animate()搬进来,最后页面不动、控制台飘红——然后对着空荡荡的全局对象发呆半天。
其实不是你手生了,而是你正站在一个关键分界线上:小程序的逻辑层,根本不在浏览器里跑。今天咱们把这块"大脑"剖开,讲清楚它到底在什么环境、拿什么当武器,以及怎么用它写出稳、快、合规的代码。
一、它在哪跑:类似 ServiceWorker,但不是浏览器页
微信官方文档对逻辑层的定位写得非常直白:
小程序开发框架的逻辑层使用 JavaScript 引擎,为小程序提供 JS 代码的运行环境以及微信小程序的特有功能。逻辑层将数据进行处理后发送给视图层,同时接受视图层的事件反馈。开发者写的所有代码最终将会打包成一份 JavaScript 文件,并在小程序启动的时候运行,直到小程序销毁。这一行为类似 ServiceWorker,所以逻辑层也称之为 App Service。
关键结论只有一句——
渲染层(View) 用 WebView 负责画界面(WXML + WXSS),它更像"浏览器页面",但也不是让你随便进 DOM 厨房乱翻。
逻辑层(App Service) 跑在 JavaScript 引擎(iOS 常见 JSCore,Android 对应宿主实现)里,没有浏览器的 window / document / DOM。
(1)"类似 ServiceWorker"这句比喻在说什么?
ServiceWorker 给人的直觉是:常驻、独立于页面 DOM、充当服务进程角色。小程序逻辑层也是这个气质:
它是"全局服务中心"(App 级生命周期在这里)
它不画 UI,但决定 UI 应该长什么样的数据(state)
它把数据"下发"给渲染层;渲染层把"用户点了哪"的事件"上传"回来
你可以把它理解为:一个专门为小程序定制的、带微信原生能力的"无界面 JS 服务进程"。
(2)所以 window和 document去哪了?
官方直接给了这句话:
注意:小程序框架的逻辑层并非运行在浏览器中,因此 JavaScript 在 web 中一些能力都无法使用,如
window,document等。
这意味着——
想改界面?别抓节点,走
this.setData(...)(数据驱动)想用
window/location/history/localStorage(Web 版)?没有,也别幻想 polyfill 出一个假 DOM/BOM 来硬跑想用 jQuery / Zepto(依赖 DOM/BOM)?基本跑不了,请把它们从依赖里请出去
这不是阉割,是把 UI 所有权从 JS 手里收走,逼你走更可维护的数据驱动通道。短期疼一下,长期少大量"界面状态打架"的 Bug。
二、微信在 JS 基础上"加了什么":入口、全局、页面、API
同样来自官方文档,框架在 JavaScript 基础上扩出来的"小程序专属装备",核心就这几样:
(1)App():总控室(app.js 必须且只能调一次)
js
// app.jsApp({globalData: {userInfo: null,token: ''},onLaunch(options) {// 小程序初始化:全局只一次// 进阶建议:适合做轻量探测(是否已登录/设备信息收集)// 别塞同步重计算或大循环,会拖慢首屏},onShow() { /* 从后台切回前台 */ },onHide() { /* 切到后台 */ },onError(msg) { console.error(msg) }})
要点就两个:
App()必须在app.js中调用,且只能调用一次,不按规矩来会有"未定义行为"你挂在
globalData上的东西,就是小程序级别的"共享仓库"
进阶建议(别把 globalData 用成垃圾场): 只放真的需要跨页共享且可序列化的状态(登录态、少量配置)。业务数据尽量留在页面/组件作用域内;跨页通信优先用事件/状态管理,而不是"随便哪个页面都去 getApp().globalData.xxx = yyy"。
(2)Page():每个页面的剧本
js
// pages/index/index.jsPage({data: {motto: 'Hello MINA'},onLoad(query) {// query = 打开本页的参数(扫码/分享带过来的 scene 等)},onShow() {},handleTap() {this.setData({ motto: 'Hi WeChat' })}})
规则很清晰:
data是本页真相源界面绑定
{{motto}};你改就走setData,别去"抓节点改样式"生命周期(
onLoad / onShow / onReady / onHide / onUnload)把"页面什么时候活着"给你管明白
(3)getApp():拿全局实例(读多写少原则)
官方给出 getApp()用来拿你在 App()里注册的总实例:
js
const app = getApp()console.log(app.globalData.token)
进阶建议: 用 getApp().globalData读配置/读登录态,没问题;但尽量少在页面深处写 getApp().globalData.xxx = yyy(写操作最好收敛到一两个 service 模块里),否则调试时很难追溯:到底谁把状态改崩了?
(4)getCurrentPages():页面栈的 X 光片(看,但慎动手)
官方明确给出:getCurrentPages()可获取当前页面栈实例数组,按顺序排,最后一项就是当前页;并提示:仅用于展示/读取,勿修改页面栈。
js
const pages = getCurrentPages()// pages[pages.length - 1] === 当前页实例(可读取其 route / data)
进阶建议(很重要): 别拿它做"跨页强制赋值"(例如 pages[pages.length-2].setData(...)依赖隐式栈关系当日常通道),否则页面栈一变逻辑就脆。更稳的跨页通信:让目标页在 onShow里自己去读"待消费消息",而不是别人隔着栈去掀被子。
(5)wx.*API:逻辑层真正的特权通道
提供丰富的 API,如微信用户数据、扫一扫、支付等微信特有能力。
因为这些能力涉及硬件/系统级调用、资金/身份敏感操作、隐私合规边界,所以它们不走 DOM,而是通过 wx.*统一收口——要经过微信客户端(Native)这层网关。
合规提醒(人话版): 用到权限的 API(位置/相机/麦克风等)时,必须按微信当时的权限与隐私协议流程走:先告知、再授权、最小必要、别偷偷传。逻辑层没有 DOM 可藏污,但日志/上报链路要是设计糙了,一样会踩《个人信息保护法》相关红线。
三、为什么"没有 window/document"反而是保护你
官方这句话很冷,但很负责任:
注意:小程序框架的逻辑层并非运行在浏览器中,因此 JavaScript 在 web 中一些能力都无法使用,如
window,document等。
把它落到日常开发习惯上,就是下面这张对照表(我把它当"红线清单"贴在团队 wiki 首页):
(1)你从 Web 带来的习惯 → 小程序现实 → 正确姿势
document.getElementById / querySelector→ ❌ 不存在 → 用{{ }}+setData驱动视图window / location / history→ ❌ 没有这些 BOM 对象 → 用wx.navigateTo / switchTab / redirectTo等路由 APIjQuery / Zepto(依赖 DOM/BOM)→ ❌ 基本跑不了 → 用框架数据绑定 + 组件化
NPM 包里"只要用到 DOM/BOM"的部分 → ❌ 别硬上 → 选小程序适配版本,或换纯逻辑库(
lodash-es核心、dayjs等通常 OK)
一句话定性: "没有 window/document"不是限制你的创造力,是把 UI 所有权从 JS 手里收走,让你没法写出"状态散落在十个 DOM 节点上"的面条代码。
四、进阶心法:让 App Service 又快又稳的三条纪律
(1)onLaunch 别塞"重活"
onLaunch是小程序最敏感的启动期。在这里做同步重计算、大循环、阻塞式操作,会直接拖慢首屏。进阶建议:onLaunch里只做轻量初始化;需要异步拉的配置/登录态探测,用 Promise或事件解耦,别让启动链等它。
(2)globalData 不是全局变量垃圾桶
能放 globalData的只有两类东西:
全局配置(主题/环境标识)
登录态凭证(token / userInfo 脱敏后的)
其他的,该是页面 state 就放页面,该是组件 state 就放组件。
(3)模块化走 CommonJS,别走全局副作用
官方明确说了每个文件有独立作用域,变量/函数只在该文件有效;不同文件声明同名变量不会互相污染。
js
// utils/format.jsfunction formatPrice(cents) {return (cents / 100).toFixed(2)}module.exports = { formatPrice }// pages/index/index.jsconst { formatPrice } = require('../../utils/format')
这才是干净的模块化。别在文件顶层写一堆直接执行的副作用脚本,把 app.js搞成"万物起源"。
五、合规与标准的底线
从 GB/T 25000.51-2016 的角度,软件产品的"功能性"与"可靠性"要求你的运行时行为可预期、可审计。逻辑层没有 DOM 这个"不可控变量",其实帮你更容易做到这点——前提是你的状态管理别又全塞回 globalData当野草原。
而从 《中华人民共和国个人信息保护法》 的角度,逻辑层所有涉及用户数据的处理(哪怕是间接的,比如日志上报里带了 openid/手机号哈希),都必须满足合法、正当、必要、透明。App Service 的隔离结构是好起点,但别让它变成"因为看不到 UI 就忽略隐私设计"的盲区。
六、结束语
小程序的逻辑层 App Service,本质就是一个不带浏览器的、被微信管控的 JS 运行时:它用 App()/Page()组织生命周期与状态,用 setData向下发数据,用事件向上收反馈,用 wx.*调微信原生能力,并且坚决不给你 DOM 这支"会走火的枪"。
你越早把它当"服务进程"而不是"页面脚本",你的架构越干净,性能越可控,审核与合规也越稳。
参考文献
[1] 微信团队. 逻辑层 App Service [EB/OL]. (2024-05-01)[2024-06-16]. https://developers.weixin.qq.com/miniprogram/dev/framework/app-service/index.html.
[2] 微信团队. App(object) 参考文档 [EB/OL]. https://developers.weixin.qq.com/miniprogram/dev/reference/api/App.html.
[3] GB/T 25000.51-2016, 系统与软件工程 系统与软件质量要求和评价(SQuaRE) 第51部分:就绪可用软件产品(RUSP)的质量要求和测试细则 [S]. 北京:中国标准出版社,2017.
[4] 全国人民代表大会常务委员会. 中华人民共和国个人信息保护法 [Z]. 2021-08-20.
💬 互动话题
你最早转小程序时,最想当然的"Web 写法"是哪一个?document?window.location?还是某个 NPM 包直接报 DOM 缺失?
评论区留一句你当年最疼的"跨环境踩坑",我挑几条最典型的,下篇用真实重构对比帮你改成"App Service 正确姿势"。想拿《小程序逻辑层合规与性能 Checklist(防踩雷版)》的,喊一声,我整理完发~
夜雨聆风