生命周期与回调
插件不是「装上就完事」:宿主会在不同时机把插件注入到三个上下文,再把系统事件推给你。本页把什么事件、在哪个层、什么时候、带什么载荷一次说清。
本页你将学到
- attach / detach / pageChange 等事件在三层的真实投递矩阵;
- attach = 每次注入、页面导航销毁不派 detach 等时序事实;
onReady / onAttach / onNetwork等钩子糖的全表与用法;- messaging 回声过滤的标准写法。
事件订阅 API
on(name, fn)
订阅宿主事件或自定义事件。每层全局(wadLogicLayer / wadRenderLayer / wadPanelLayer)都有同名方法。
- 签名:
on(name, fn) → 本层全局对象(可链式) fn的参数由事件决定,见下方矩阵;- 事件载荷走总线,必须 JSON 可序列化。
// 逻辑层
wadLogicLayer.on('pageChange', function (p) {
wadLogicLayer.log('路由: ' + p.route);
});off(name, fn)
取消订阅。必须传同一个函数引用,匿名函数无法解绑。
function onPage(p) { /* … */ }
wadLogicLayer.on('pageChange', onPage);
wadLogicLayer.off('pageChange', onPage); // 同一个引用优先用钩子糖
钩子糖(onAttach / onPageChange …)直接返回解绑函数,不用保存引用,见下文。
真实投递矩阵
图例:✅ 宿主主动推给本层 · — 不可达(换通道)
| 事件 | 逻辑层 | 渲染层 | 面板 | 载荷 | 触发时机 |
|---|---|---|---|---|---|
attach | ✅ | ✅ | ✅ | 逻辑层 appid;渲染层 appid 字符串;面板 on('attach') 收 {appid, version} | 每次注入都重发(见下) |
detach | ✅ | ⚠️ 仅停用/卸载/全局关 | ✅ 停用/卸载/全局关已可达 | 逻辑层 appid;渲染/面板 {} | 页面导航=JS 世界销毁,不派发;小程序关闭 reason=destroy |
pageChange | ✅ | — | ✅ | {route, appid, options} | 业务页面路由变化(渲染层不可达:路由变化时旧页面 JS 世界整体销毁、新页面重注入) |
storageWrite | ✅ | — | ✅ | {op:'set'|'remove'|'clear', key, value?, plugin} | 业务调 wx.setStorage* / removeStorage* / clearStorage*(渲染层无 wx storage 通道) |
network | ✅ | — | ✅ | {id, url, method, status, kind, at, ws?…} | 捕获管线产出摘要 |
textEditIntent | 发送方 | ✅ 接收 | — | {on, block} | 逻辑层发意图,宿主缓存并直推 |
messaging.* | ✅ | ✅ | ✅ | meta 信封 | 任意层 messaging.emit |
| sharedStorage 变更 | ✅ | ✅ | ✅ | change 对象 | 本插件任一层写共享存储 |
注意点:
on('network')魔法已移除:逻辑层用onNetwork()/capture.watch(),面板用capture.watch();- 顶层
emit主要投递本插件面板事件;面板 emit 还会派发到同插件逻辑层。完整跨层广播用messaging; - 插件自己命名空间(
__wad_前缀)的 storage 写入不触发storageWrite(防自反馈)。
时序事实
attach = 每次注入
attach 不是「装一次、发一次」:宿主每一次把插件注入到上下文,都会重发一次 attach。
| 场景 | attach |
|---|---|
| 安装 / 启用插件 | 重发 |
| 插件更新(同包名覆盖) | 重发 |
| 生效小程序白名单变化 | 重发 |
| 页面切换导致渲染层重注入 | 重发 |
| 切换小程序(新 isolate) | 重发 |
所以不要把 attach 当一次性初始化:幂等注册(重复 register 覆盖旧值)、先清理再注入,是标准写法。
// 逻辑层:attach 里做幂等初始化
wadLogicLayer.onAttach(function (appid) {
wadLogicLayer.log('注入到 ' + appid);
wadLogicLayer.register('greet', function (name) { return '你好,' + name; });
});页面导航销毁不派 detach
页面导航会销毁旧渲染层实例,但宿主不为此派发 detach——紧随其后的新页面注入会重发 attach。渲染层的 detach 只在配置移除类场景出现:停用 / 卸载 / 切小程序。
推论:渲染层不要把「清理 DOM」放在 detach 里等导航场景,注入时先清理上一代遗留(幂等注入):
// 渲染层:幂等注入
wadRenderLayer.dom.render(function (doc) {
if (doc.getElementById('__wad_banner')) return; // 上一次注入的还在,先复用
var b = doc.createElement('div');
b.id = '__wad_banner';
b.textContent = 'demo';
doc.body.appendChild(b);
});面板 detach 已可达
面板层的 detach 现在真实可达:插件被停用、卸载或插件总开关关闭时,打开着的面板会收到 detach,随后面板被关闭。在 detach 里把定时器、长轮询收掉即可。
意图泵 800ms tick
textEditIntent 意图不是即时事件:逻辑层发出后宿主缓存,由宿主泵每 800ms 调一次渲染层的 globalThis.__wadApplyIntents({插件id: 意图}),直推最新意图快照。
- 意图到达最多有 800ms 延迟;
- 面板关闭、切页、后退到缓存页都会收敛到最新意图(专为跨页面会话级开关设计);
- 每次渲染层注入后第一拍就会推,所以新页面拿到的总是最新状态。
// 逻辑层:发意图(宿主特判事件名,缓存并直推渲染层)
var enabled = false;
function pub() { wadLogicLayer.emit('textEditIntent', { on: enabled, block: true }); }
wadLogicLayer.onAttach(function () { enabled = false; pub(); });// 渲染层:直推入口(800ms 泵调用)
globalThis.__wadApplyIntents = function (map) {
var it = map && map[wadRenderLayer.id];
if (it) setEditing(!!it.on, it.block !== false);
};面板 network 直连最快
network 摘要到面板层的路径是直连宿主捕获管线(capture.watch),不经跨层转发;逻辑层的 onNetwork 同为直连。三处时延排序:
| 通道 | 时延 |
|---|---|
面板 capture.watch | 最低(直连管线) |
逻辑层 onNetwork / capture.watch | 低(直连管线) |
| 渲染层经 messaging 转发 | 高(多一跳,且渲染层无原生订阅) |
钩子糖全表
钩子糖是高频事件的快捷注册:每个钩子都返回解绑函数,调用即取消,不需要保存函数引用。
| 钩子 | 可用层 | 等价事件 | 触发时机 |
|---|---|---|---|
onReady(fn) | 三层 | ready.then | 就绪:逻辑层/渲染层注入完成;面板总线连上 |
onAttach(fn) | 逻辑/渲染 | on('attach') | 每次注入(面板用 on('attach')) |
onDetach(fn) | 三层 | on('detach') | 停用 / 卸载 / 切小程序 / 全局关 |
onPageChange(fn) | 逻辑层 | on('pageChange') | 业务路由变化 |
onStorageWrite(fn) | 逻辑层 | on('storageWrite') | 业务写 wx storage |
onNetwork(fn) | 逻辑层 | on('network')(已移除) | 捕获管线产出摘要 |
onOpen(fn) | 面板 | onReady 后 appid | 面板打开 |
onClose(fn) | 面板 | on('close')(特判已移除) | 面板关闭 |
onReady(fn)
就绪回调,等价于挂 ready Promise,但可解绑。
const off = wadPanelLayer.onReady(function () {
wadPanelLayer.log('总线连上了');
});
// off(); 不需要时解绑onAttach(fn) / onDetach(fn)
onAttach 每次注入都重发;onDetach 只在停用/卸载/切小程序/全局关时触发,页面导航不派。
// 逻辑层
wadLogicLayer.onAttach(function (appid) {
wadLogicLayer.log('attached ' + appid);
});
wadLogicLayer.onDetach(function (appid) {
wadLogicLayer.log('detached ' + appid);
});// 面板:detach 已可达
wadPanelLayer.onDetach(function () {
clearInterval(timer); // 收掉长任务
});onPageChange(fn)
wadLogicLayer.onPageChange(function (p) {
// p = {route, appid, options}
wadLogicLayer.log('route: ' + p.route + ' options: ' + JSON.stringify(p.options));
});onStorageWrite(fn)
wadLogicLayer.onStorageWrite(function (e) {
// e = {op, key, value?, plugin}
if (e.key.indexOf('token') >= 0) wadLogicLayer.log('token 被写入');
});onNetwork(fn)
订阅网络摘要(逻辑层)。回调载荷与 capture.watch 一致。
const off = wadLogicLayer.onNetwork(function (e) {
if (e.status >= 400) wadLogicLayer.log('失败 ' + e.status + ' ' + e.url);
});onOpen(fn) / onClose(fn)
面板壳生命周期。on('open') / on('close') 特判已移除,统一走这两个钩子。
// 面板层
wadPanelLayer.onOpen(function () {
wadPanelLayer.log('panel open', wadPanelLayer.instanceId);
});
wadPanelLayer.onClose(function () {
// 面板即将关闭(pagehide),把该收的收掉
});回声过滤模式
messaging 广播不排除发送者自己:你自己也会收到自己发的消息(回声)。由接收方按 meta 过滤:
// 任一层订阅
wadPanelLayer.messaging.on('cart:updated', function (data, meta) {
// meta = {from, instanceId, plugin, eventId, at}
if (meta.plugin !== wadPanelLayer.id) return; // 只听自己插件的
if (meta.from === 'panel') return; // 过滤本层回声
renderCount(data.count);
});两种常用写法:
| 写法 | 做法 |
|---|---|
| 按 from 过滤 | meta.from === 本层 时 return,跳过自己这层的回声 |
| 按 eventId 去重 | 记录已处理的 meta.eventId,同一事件在多层各派发一次、eventId 相同 |
面板实例回声(PC + 手机同时开面板)按 instanceId 过滤:
wadPanelLayer.messaging.on('ui-sync', function (d, meta) {
if (meta.instanceId === wadPanelLayer.clientId) return; // 过滤自己的回声
apply(d);
});完整语义见 messaging。