乐于分享
好东西不私藏

没有window!搞懂小程序逻辑层App Service

没有window!搞懂小程序逻辑层App Service

各位产品大大、运营同学,还有深夜在开发者工具里敲 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 中一些能力都无法使用,如 windowdocument等。

这意味着——

  • 想改界面?别抓节点,走 this.setData(...)(数据驱动)

  • 想用 windowlocationhistorylocalStorage(Web 版)?没有,也别幻想 polyfill 出一个假 DOM/BOM 来硬跑

  • 想用 jQuery / Zepto(依赖 DOM/BOM)?基本跑不了,请把它们从依赖里请出去

这不是阉割,是把 UI 所有权从 JS 手里收走,逼你走更可维护的数据驱动通道。短期疼一下,长期少大量"界面状态打架"的 Bug。


二、微信在 JS 基础上"加了什么":入口、全局、页面、API

同样来自官方文档,框架在 JavaScript 基础上扩出来的"小程序专属装备",核心就这几样:

(1)App():总控室(app.js 必须且只能调一次)

js

// app.jsApp({  globalData: {    userInfonull,    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 中一些能力都无法使用,如 windowdocument等。

把它落到日常开发习惯上,就是下面这张对照表(我把它当"红线清单"贴在团队 wiki 首页):

(1)你从 Web 带来的习惯 → 小程序现实 → 正确姿势

  • document.getElementById / querySelector→ ❌ 不存在 → 用 {{ }}setData驱动视图

  • window / location / history→ ❌ 没有这些 BOM 对象 → 用 wx.navigateTo / switchTab / redirectTo等路由 API

  • jQuery / 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 写法"是哪一个?documentwindow.location?还是某个 NPM 包直接报 DOM 缺失?

评论区留一句你当年最疼的"跨环境踩坑",我挑几条最典型的,下篇用真实重构对比帮你改成"App Service 正确姿势"。想拿《小程序逻辑层合规与性能 Checklist(防踩雷版)》的,喊一声,我整理完发~