跳转到内容

热更新实现原理

vpt 的微信热更新不是浏览器 HMR 的直接移植。它提供三种开发更新模式:

  • devtools 把原生 JavaScript 补丁写入 dist/wx,借助开发者工具重新执行 Page 入口,更新原有 Taro/React 页面;
  • interpreter 通过 Vite 已有 WebSocket 推送源码,由 App 中的 Sval 安装模块实现,不重新注册 Page;
  • rebuild 不交付增量补丁,每次有效源码变化都让 Rolldown 写出完整原生项目并重启 App。

devtools 与 interpreter 共享补丁序号、模块图更新、React Refresh、样式事务、确认和完整构建恢复,只改变补丁如何到达并安装到存活的 App 运行时。一次开发服务器只选择一种模式,更新路径中没有逐补丁模式判断。

使用方法和常见问题参见开发热更新。本页解释内部设计。

微信开发期间会发生三种容易混淆的事情:

变化被替换的内容状态结果
模块热更新App 内存中的部分 JavaScript 模块App 和兼容的 React 组件状态保留
开发者工具重新执行 Page 入口(仅 devtools)Page JavaScript 注册代码glass-easel 保留原生 Page 实例,不重放页面生命周期
完整构建整个 dist/wx 代码基线和 App 运行环境所有运行时状态重置

devtools 的成功更新通常同时包含前两项,但重新执行入口不等于销毁并重建原生页面。interpreter 只执行第一项。rebuild 对每次有效变化都直接执行第三项。

在补丁模式中,完整构建是恢复手段。只要局部替换无法证明安全,vpt 就生成新基线并允许开发者工具重启 App。

新代码需要环境允许的执行机制

Section titled “新代码需要环境允许的执行机制”

小程序不能把网络收到的源码交给 eval() 或 Function()。devtools 因此先把补丁写入 dist/wx,由微信开发者工具编译为原生 JavaScript。interpreter 则把源码当作数据交给 Sval 的解析器和解释执行器,不调用动态代码生成 API。

解释器只存在于开发运行时。生产构建仍然全部是开发者工具可编译的静态项目文件。

devtools:改了哪个文件,决定重载多大范围

Section titled “devtools:改了哪个文件,决定重载多大范围”

开发者工具根据发生变化的文件及其已有依赖关系决定如何重载:

  • 根入口 app.js 或 app.wxss 变化,会重建整个 App 运行环境;
  • Page 已经直接依赖的 JavaScript 文件变化,可以只重新执行 Page;
  • 页面开始重载后才动态发现的新依赖,不能反过来改变这次重载范围。

所以,每个 Page 入口必须从第一次构建起就包含一个字面量依赖:

require('../../hmr/patches.js')

路径按页面位置生成,但所有页面最终指向同一个 hmr/patches.js。

React 状态只能存在于没有被销毁的树中

Section titled “React 状态只能存在于没有被销毁的树中”

React Refresh 不会把状态序列化后重建。它只能更新仍然存活的 React 树。vpt 必须避免普通更新重启 App,并把模块运行时放在 App 共享的微信全局环境中,而不是放在会被重新执行的 Page 中。

部分职责
Rolldown 开发引擎监视源码,按所选模式生成模块补丁或完整输出
vpt 开发主机串行处理构建结果、样式、累计补丁、交付和运行时报告
DevTools 交付把累计补丁渲染为 hmr/patches.js
Interpreter 交付通过 Vite WebSocket 推送新发布的累计日志
App 模块运行时保存模块图、缓存、热更新边界和已应用序号
Sval(仅 interpreter)解析并解释 Rolldown 模块注册程序与更新后的模块实现
React Refresh判断组件边界是否兼容,并更新现有 React 树
平台 Page 注册策略(仅 devtools)WX 直接保留配置和生命周期;其他平台使用既有交接实现

微信开发模式复用 Vite 已经解析好的打包开发配置,并创建一个直接写入 dist/wx 的 Rolldown 开发引擎。不会再创建第二个文件监听器或第二张模块图。

第一次完整构建会建立以下约定。

开发文件名不包含内容哈希。App、Page、普通代码块和补丁模式的 HMR 文件都能在后续完整构建中覆盖同一路径,开发者工具也可以持续观察这些文件。

补丁模式中,每次成功并交付给开发者工具的完整构建都有一个新的 buildId。主机把它和已认证的 Vite WebSocket 地址写入:

hmr/info.js
module.exports = {
buildId: '本次完整构建的唯一标识',
endpoint: 'ws://127.0.0.1:<port>/__vpt_hmr__?token=<vite-token>'
}

App 入口在业务模块执行前读取该文件并初始化共享模块运行时。旧构建延迟到达的补丁或报告会因为 buildId 不匹配而被忽略。

初始 hmr/patches.js 不包含更新:

module.exports = undefined

每个 Page 入口仍然会同步读取它:

__rolldown_runtime__.applyPatches(
require('../../hmr/patches.js'),
'pages/example/index'
)

第一次执行什么也不会发生,但开发者工具已经记录了 Page 到补丁文件的直接依赖。以后只需替换这个文件,就能触发所需的 Page 重载。

解释器模式不给 Page 添加补丁依赖,也不生成 hmr/patches.js。App 连接 hmr/info.js 中的 WebSocket,源码发布时主机直接广播累计补丁。

点击开发者工具的「编译」会创建新的 App 运行环境,但不会自动重启 Vite 或更新磁盘上的完整代码基线。此时 hmr/patches.js 可能只剩旧 App 尚未确认的后缀,新 App 不能跳过缺失的前缀直接执行它。

Socket 连接建立后,小程序发送 { kind: 'startup', buildId },通知开发服务自己刚刚启动。如果当前构建还没有产生过热更新补丁,就直接继续运行;如果已经发布过补丁,开发服务就根据最新源码重新生成完整代码,并让小程序重新加载。即使旧补丁仍然齐全,也采用完整重建,而不是在启动时逐个重放。遇到不完整的旧补丁时,小程序会跳过它们,等待完整代码生成,不会强行执行。

Socket 打开前跳过 Page 补丁执行,不暂存补丁或 ACK;由启动报告让主机决定是否需要完整构建。启动报告不是应用确认,不会裁剪历史。正常更新若应用失败,共享运行时先发送完整构建请求,再关闭 Socket;后续补丁因没有可用连接而跳过,不保存失败原因或额外的失败标记。正常 HMR 仍在成功应用后报告真实序号。Socket 引用只在 onOpen 后可用于发送,关闭或报错时清除,不维护额外的连接状态或事件队列。

rebuild 不生成 hmr/info.js 或 hmr/patches.js,也不注册 Rolldown 补丁客户端。开发引擎使用 rebuildStrategy: 'always',有效源码变化完成增量分析后会直接产生一次完整输出。主机在完整代码和样式落盘后最后更新带唯一标记的 App 样式入口,保证开发者工具把这一代文件作为 App 级刷新处理。

该模式复用原生模块运行时来执行开发代码块并满足 Vite 生成代码中的 import.meta.hot 接口,但不初始化补丁 Socket、入口依赖或 Page 交接。

模块缓存、模块引用关系、热更新边界和补丁序号都保存在 App 共享的运行时中。devtools 的 Page 重新执行时取得同一个实例;interpreter 的 WebSocket 也由该实例持有。两种补丁模式都把新实现应用到现有模块图,而不是从空状态开始。

开发构建还会使用应用实际的 Taro 模块实例,并在 React 渲染器运行前安装 React Refresh 所需的全局 Hook。这里不能另行导入一份 Taro,否则 DevTools 页面交接和解释器更新都会面对与应用不同的模块实例。

保存源码 ─┬─ rebuild: Rolldown 完整输出 → App 样式构建标记 → App 重启
└─ 补丁 → 各入口 WXSS → 累计补丁日志
├─ devtools: patches.js → Page 重载 → 原生工厂
└─ interpreter: Vite WebSocket → Sval 安装源码
↓
共享运行时验证并一次应用整个批次
↓
React Refresh → WebSocket 报告已应用序号

rebuild 在完整输出后结束本次更新。devtools 随后重新执行 Page 注册,WX glass-easel 不重放生命周期;interpreter 的 Page 从未被重新注册。下面主要按两种补丁模式的共享顺序和差异展开。

每次源码变化会得到以下结果之一:

结果含义
无变化没有需要发布的运行时更新
模块补丁包含变化模块的新注册程序,可以尝试局部替换
完整构建变化不能表示为安全的局部更新

语法错误等中间状态不会立刻触发完整构建。vpt 记录错误并继续运行上一份有效代码;下一次保存恢复有效语法后,开发引擎可以继续产生正常补丁。

连续保存时,vpt 会短暂等待这一轮编译结果结束,再合并成一次磁盘通知。这段等待只减少文件写入次数,不会丢弃中间补丁:后一个补丁是基于前一个补丁产生的增量,必须按原顺序全部保留。

一次补丁发布遵守固定顺序:

  1. 根据最新模块图更新 App 和页面 WXSS;原生 CSS 转换或压缩失败时只报告错误,继续使用上次成功的样式;
  2. 让所选模式发布累计补丁:devtools 原子替换文件,interpreter 通过 Vite WebSocket 广播源码;
  3. 按补丁顺序通知 Rolldown,这些补丁已经可交付。

所有构建结果、样式、补丁、运行时报告和完整构建切换都经过同一条串行队列。交付成功只推进 Rolldown 的“已发布”前沿;App 通过 WebSocket 成功应用序号后,日志才推进“已应用”前沿。

devtools 文件使用“同目录临时 .txt 文件 + rename”替换。开发者工具忽略临时文件,只会看到完整的最终内容。interpreter 不写 JavaScript 文件,也不复制第二份发布状态;每次广播直接序列化本次累计补丁发布。

3. devtools 补丁文件保留尚未确认的完整序列

Section titled “3. devtools 补丁文件保留尚未确认的完整序列”

hmr/patches.js 不是“最新补丁”,而是“当前 App 尚未确认应用的全部补丁”。例如,App 已应用到序号 3:

主机产生 4 → 文件包含 [4]
Page 尚未读取
主机又产生 5 → 文件包含 [4, 5]
Page 这时读取 → 一次得到 [4, 5]
App 报告已应用 5 → 主机删除不大于 5 的历史

这解决了开发者工具合并或错过中间文件事件的问题。即使 Page 没有观察到只含 [4] 的版本,下一版仍携带从它当前状态到最新状态所需的所有注册程序。

补丁文件大致如下:

module.exports = {
buildId: 'current-build',
patches: [
{
seq: 4,
changedIds: ['src/report.tsx'],
factory: () => {
// Rolldown 生成的新模块图和模块实现
}
},
{
seq: 5,
changedIds: ['src/chart.tsx'],
factory: () => {
// 下一次增量
}
}
]
}

补丁文件本身只导出数据,不会擅自修改运行时。Page 入口明确把它交给 App 模块运行时。

4. devtools Page 在业务代码之前应用补丁

Section titled “4. devtools Page 在业务代码之前应用补丁”

开发者工具发现 hmr/patches.js 变化后,会重新执行依赖它的存活 Page。Page 入口先调用 applyPatches(),之后才继续加载页面业务模块。因此本次重新执行看到的是新模块状态。

多个 Page 可能依次读取同一份补丁。App 运行时使用序号跳过已经应用的部分,但仍会为每个被开发者工具替换的路由建立独立的页面交接。

4b. interpreter 接收 WebSocket 源码并安装

Section titled “4b. interpreter 接收 WebSocket 源码并安装”

主机把当前尚未确认的补丁 { buildId, patches: [{ seq, changedIds, code, ... }] } 作为 Vite 自定义事件发送。Sval 在一个 App 级沙箱作用域中解释每个 code;代码只登记模块图和模块工厂,应用模块仍由共享运行时稍后统一执行。

成功应用后,App 通过同一个 WebSocket 报告序号。解释器源码只在新发布时发送。每份源码携带构建身份;构建替换和主机关闭都会向旧 App 发送终止消息。

运行时先确认 buildId 属于当前 App,再按顺序读取补丁。对每一项:

  1. 已应用过的序号直接跳过;
  2. 新序号必须等于当前期待的下一个序号;
  3. 通过检查后,才执行这一项的模块注册程序并继续检查下一项。

补丁是增量,序号不能缺失或重复。若批次中途发现错误,前面连续部分的注册程序可能已经登记了新实现,但应用模块尚未重新执行,序号也不会被确认;运行时会立即请求完整构建,而不是继续使用这份不完整状态。

6. 一次切换到批次中的最新实现

Section titled “6. 一次切换到批次中的最新实现”

通过序号检查后,运行时先执行批次中所有补丁的注册程序。这些程序只登记新模块图和新模块实现,不立即运行应用模块。

随后运行时合并整个批次的变化模块,只做一次模块替换。连续保存 [4, 5, 6] 不会让 React 依次渲染三个中间版本;所有实现登记完成后,页面直接从当前版本切到序号 6 的版本。

对于每个已经执行过的变化模块,运行时沿“谁导入了它”向上查找:

  • 遇到调用过 import.meta.hot.accept() 的模块,就把它作为更新边界;
  • 尚未执行过的变化模块无需立即处理,新实现会在第一次导入时生效;
  • 如果走到顶层仍找不到更新边界,局部替换不安全;
  • 如果传播路径形成循环,运行时也放弃局部替换。

React 项目的更新边界通常由 @vitejs/plugin-react 生成,vpt 不另造组件边界。

找到边界后,运行时:

  1. 确认每个受影响模块都有可执行的新实现;
  2. 保存旧边界注册的接受回调;
  3. 在执行任何新模块之前,一次性清除整个受影响集合的旧缓存;
  4. 用最新实现重新执行边界;
  5. 把最新导出交给旧边界的接受回调。

统一清除缓存可以防止一个新边界读到另一个受影响模块的旧导出。遍历成本与本次实际触及的模块和引用关系成正比,即 O(V + E)。

只有模块传播、模块执行和全部接受回调都成功后,运行时才更新“已应用序号”,并发送:

{
kind: 'applied',
buildId: string,
seq: number
}

主机收到后只删除该序号覆盖的补丁前缀。把“模式已经发布”和“App 已经应用”分开,才能在 Page 尚未处理文件事件或解释器尚未收到 WebSocket 消息时继续安全发布后续补丁。

组件签名、组件家族和边界兼容性仍由 @vitejs/plugin-react 与 React Refresh 判断。vpt 只适配它们对浏览器环境的假设:

  1. 为 React Reconciler 添加 Refresh 运行时的静态导入,在渲染器注册前完成初始化;
  2. 在 Refresh 运行时模块末尾调用 injectIntoGlobalHook(globalThis),把 React DevTools Hook 安装到 JavaScript 全局对象;
  3. 通过仅在小程序开发模式启用的 define,把 Refresh 的四个 window.* 协议属性映射到 globalThis.*。运行时与生成的组件边界共享同一个全局对象,不移除边界的前置检查。

前置初始化编译后为:

injectIntoGlobalHook(globalThis);
globalThis.$RefreshReg$ = () => {};
globalThis.$RefreshSig$ = () => (type) => type;

映射仅覆盖 $RefreshReg$、$RefreshSig$、__registerBeforePerformReactRefresh 和 __getReactRefreshIgnoredExports。VPT 不会创建或整体替换 window;普通 window、typeof window、window.document 以及局部声明的 window 保持原有语义。生产构建和 H5 不启用这些映射。

边界兼容时,React Refresh 在原有 React 树上更新组件,所以 Hook 状态得以保留。组件类型、Hook 顺序或导出形状不兼容时,Refresh 可以重新挂载局部组件;如果边界主动使本次模块更新失效,运行时会请求完整构建。

vpt 不读取或序列化 React 内部的渲染树,不复制 Hook 状态,也不创建第二棵 React 树。

WX 使用 glass-easel。开发者工具热更新会重新执行对象形式的 Page(...) 注册,但保留当前原生 Page 实例,不重新调用其数据工厂,也不重放 onUnload、onLoad、onShow。这与真正的页面卸载、重新进入不同。

共享的 injectPageHmr 通过普通生命周期包装记录已挂载的原生 Page。再次注册时检查该实例的 groupUpdates 方法:glass-easel 提供此方法,exparser 不提供。检测到 glass-easel 后直接返回配置,不抑制生命周期,不复制 Page 或 CustomWrapper 快照,也不设置等待清理的重注册标记。React Refresh 继续更新原有 React 树,Taro 继续连接原来的原生页面。

不能在热更新时设置一个依赖后续 onShow 清除的标记:该回调没有发生时,标记会泄漏到之后的真实导航,误跳过真正的卸载和加载,令新页面继续显示旧快照却无法接收 React 更新。用定时器或微任务猜测清理时机同样不是 WX 的生命周期约定。

prerender: true 的 Page({ data: () => ... }) 工厂仅在创建新原生实例时执行。热注册不会新建实例;真实导航仍执行工厂和普通 Taro 生命周期。VPT 的 createVptPage 仅在开启预渲染时把原始配置的 data 替换为工厂,通过闭包保留对象形式种子,不复制配置。原生壳不再调整数据形式,只把胶囊导出的配置交给 HMR helper 后调用 Page(pageConfig)。

该判断基于每个已挂载 Page 的运行时能力,而不是 WX 目标、App 配置或 componentFramework 编译器选择,因此可区分同一 App 中使用不同框架的页面。尚未挂载或已经卸载的配置仍直接返回;没有该能力的页面保留既有的生命周期和原生快照交接实现。各平台使用同一个 HMR helper,不再传入平台专用的 Page 注册策略。

WXSS 内容不通过 JavaScript 模块的更新边界交付。完整构建和增量补丁共用一个 WX 样式插件和同一种样式事务:

  1. Tailwind 扫描器把已有候选文件和编译依赖登记到 Rolldown;相关文件变化时,Rolldown 重新转换对应 Tailwind 入口;
  2. 入口复用自己的增量生成器,得到新的浏览器 CSS 和同一代原始类名集合;编译依赖变化时才重建生成器;
  3. Vite 继续执行 PostCSS、CSS Modules 和预处理器转换,vpt 在内置 vite:css-post 序列化浏览器 HMR 模块前记录最终模块 CSS;
  4. 按 build.cssCodeSplit 投影 Rolldown 当前模块图:开启时分别选择 App 和每个页面仍然可达的 CSS,页面排除 App 已拥有的样式;关闭时按 App、配置页面顺序合并所有可达样式并去重,页面样式保持为空;
  5. 分别转换、压缩 App 与页面 WXSS,仅在 App 样式中加入 HTML 显示默认值;
  6. 用所有存活入口的 Tailwind 类名并集转换同一事务中的最终 JavaScript;
  7. 逐个原子替换转换成功且内容发生变化的 assets/global.wxss 和页面 WXSS,写入全部成功后再发布 JavaScript 补丁。

Tailwind 生成器只负责编译入口,不拥有补丁发布。它在入口存活期间保留增量扫描缓存;候选文件变化由 Rolldown 触发入口转换,新增和删除类名都会更新同一个权威集合。vpt 不在发布事务中重新扫描项目,不重复执行 Vite CSS 预处理,不绕过 Vite 读取物理样式文件,也不从 WXSS 反向解析类名。

增量更新不会改写根目录的 app.wxss。它始终导入 assets/global.wxss;开启 build.cssCodeSplit 时,App 样式更新只替换后者,页面样式更新则写入对应的页面 WXSS;关闭时,所有应用样式更新都写入全局文件。vpt 先写入转换成功的 WXSS,再发布已经用当前类名集合转换过的 JavaScript 补丁。发布前仅把补丁中的 Vite 浏览器 CSS 字符串置空;模块工厂、CSS Modules 导出、changedIds 和补丁序号保持不变。每个文件分别比较最终字节,未变化的文件不会重写;删除页面样式导入会写入空内容,清除旧规则。若后续文件写入失败,JavaScript 不会交付;重试跳过已成功写入的文件。

HMR 的原生 CSS 转换或压缩失败时,vpt 只报告输出文件名和错误,不抛出该 CSS 错误,也不发布失败或部分处理的 CSS;对应文件继续使用上次成功的样式,有效 JavaScript 和其他正常样式继续交付。若尚无成功结果,则跳过该文件。失败的源 CSS 不会被缓存为成功结果,下次更新仍会重试;修复后恢复正常转换和发布。JavaScript 仍使用当前 Tailwind 类名集合,因此修复前可能暂时出现样式缺失或不匹配,但不会因丢弃已分配序号的补丁而触发序号缺口和状态重置。此策略不吞掉 Vite / Tailwind 编译错误、JavaScript 转换错误或文件写入错误。

完整构建仍严格拒绝任何转换或压缩错误;它在最终输出阶段执行同一种图投影,用类名并集处理 JavaScript,并输出 App 和页面 WXSS。平台骨架不再生成空的页面样式占位文件;原生自定义组件的样式随后独立输出,始终保持不透明。

rebuild 对每次有效源码变化执行完整构建。两种补丁模式只在以下情况无法继续局部替换时执行:

  • Rolldown 明确返回完整构建;
  • 已执行的变化模块找不到接受边界;
  • HMR 传播路径形成循环;
  • 补丁序号缺失或重复;
  • 受影响模块缺少新实现;
  • 新模块执行或接受回调抛错;
  • 热更新边界调用 invalidate(),包括 React Refresh 判定边界失效的情况。

补丁模式的完整构建完成后,主机按以下顺序建立新基线:

  1. 协调本次完整输出的 App 与页面 WXSS;
  2. 生成新的 buildId,停止使用旧 Rolldown 客户端身份;
  3. 通知旧 App 关闭 WebSocket,再旋转补丁日志身份并清空待确认补丁;devtools 同时清空补丁文件;
  4. 写入带有新身份和认证 WebSocket 地址的 hmr/info.js;
  5. 最后写入带有新 buildId 标记的 app.wxss。

最后一步是有意的 App 级文件变化。开发者工具随后重启 App,新运行时从序号 0 开始读取已经写好的匹配身份。旧 App 延迟发送的报告会被忽略。

rebuild 没有前四项补丁状态,只在完整输出和最终 WXSS 已落盘后写入新的 App 样式构建标记。两条路径都不尝试在完整构建后恢复 React 内部状态;新的磁盘基线和新的 App 运行环境就是恢复边界。

补丁模式都通过同一个 Vite WebSocket 自定义事件发送运行时报告:

type AppliedReport = {
kind: 'applied'
buildId: string
seq: number
}
type RebuildReport = {
kind: 'rebuild'
buildId: string
reason: string
}

applied 允许主机删除已经成功应用的补丁历史;rebuild 请求完整构建。每份报告都与补丁交付和构建切换按顺序处理,不存在单独的 HTTP 报告协议。

interpreter 的源码发布、构建切换和关闭由主机通过 WebSocket 主动推送。

维护这套实现时,最重要的不是某个具体构建钩子,而是以下顺序:

收齐且保留全部 Rolldown 增量
→ 发布内容有变化的 App / 页面 WXSS
→ 通过所选模式发布累计补丁
→ 按序确认补丁已交付给 Rolldown
→ 等待 App 报告实际应用序号
写入完整输出
→ 协调最终 App / 页面 WXSS
→ 通知旧 App 关闭 WebSocket
→ 创建新 buildId
→ 重置所选交付
→ 写入 info.js
→ 最后更新 app.wxss,让 DevTools 重启 App
写入完整输出
→ 协调最终 App / 页面 WXSS
→ 最后更新 app.wxss 的唯一构建标记,让 DevTools 重启 App

破坏这些顺序会让页面看到混合构建、让主机过早删除补丁,或让新 App 读取旧身份。

  1. 一次服务器只运行一种开发更新模式,模式选择不进入增量热路径;
  2. rebuild 的有效更新只发布完整输出,不创建补丁文件、日志、客户端身份或运行时连接;
  3. 补丁模式的普通模块更新不能改写 App 入口或其他会重启 App 的根文件;
  4. devtools 的每个 Page 从初始构建起依赖同一个 hmr/patches.js;
  5. 每个补丁模式 App 堆只创建并保留一条 Vite WebSocket,不重建连接,也不使用轮询定时器;
  6. 补丁模式的 App 模块运行时必须比 Page 活得更久;
  7. 补丁日志必须保留所有尚未确认应用的连续序号;
  8. 整个受影响模块集合必须在任一新边界执行前统一清除缓存;
  9. WX devtools 的 Page 热注册不得包装真实生命周期或创建新的原生页面连接;
  10. React Refresh 必须复用存活的 App React 根;
  11. 新构建身份暴露给 App 前,所选交付必须已经重置;
  12. 转换成功的样式必须先于对应 JavaScript 补丁发布;HMR 原生 CSS 转换失败时只报告错误并保留上次成功的样式,不丢弃有效补丁;
  13. 任何无法证明连续且安全的补丁状态都必须回到完整构建。
源码名称本文中的名称含义
DevEngineRolldown 开发引擎生成完整输出和增量模块补丁
WxDevHostvpt 开发主机排队并执行所有磁盘输出、报告和构建切换
PatchJournal补丁日志保存构建身份和尚未确认应用的累计补丁
buildId构建身份区分两次完整构建的运行时和报告
seq / appliedSeq补丁序号 / 已应用序号验证增量连续性并释放已应用历史
HMR boundary更新边界调用 import.meta.hot.accept()、可以接收新导出的模块
MiniHmrMode开发更新模式选择补丁交付能力或完整重建策略,并提供对应运行时、入口改写和插件
Page re-registrationPage 原生重新注册WX glass-easel 重新执行注册代码,但保留已挂载原生实例及其 Taro 连接
WxStylePluginWX 样式插件从当前模块图和源文件生成同一事务的全局 WXSS 与 JavaScript 类名集合