内容脚本的上下文与生命周期
本教程共 45 篇 · 第 14 篇 · 更新于 2026-08-13 · 约 3 分钟阅读
本节目标:搞懂内容脚本
main(ctx)收到的 ctx 是什么,为什么内容脚本会「过期」,以及如何用 ctx 提供的一系列方法,让异步代码在扩展被卸载、更新或禁用后自动停止。
上一章我们学会了用 defineContentScript 注册内容脚本。你有没有注意到,main 函数第一个参数是个 ctx?它不只是一个摆设,它管着内容脚本的「生死」。
ctx 是什么
ctx 是 ContentScriptContext 类型的实例,内容脚本的主要职责是追踪上下文是否失效(invalidated)。
大多数浏览器有个默认行为:扩展被卸载、更新或禁用时,不会主动停掉已经注入的内容脚本。于是旧脚本还在页面里运行,但它所属的扩展已经没了,一调用扩展 API 就会报错:
Error: Extension context invalidated.
「Extension context invalidated」可以理解为:脚本的「出生证明」被吊销了。页面还在,脚本还在,但脚本和扩展之间的通道已经断了。
ctx 提供的清理工具
既然浏览器不管,我们就得自己管。ctx 提供了几个「失效即清理」的替身方法:
ctx.addEventListener(target, type, listener, options);
ctx.setTimeout(callback, delay);
ctx.setInterval(callback, delay);
ctx.requestAnimationFrame(callback);
// 还有更多
它们和 window 上的同名方法用法一样,区别在于:一旦上下文失效,这些定时器和监听器会被自动清理,回调不会再执行。
除了让方法自动清理,你还可以手动检查状态:
if (ctx.isValid) {
// 上下文还有效,放心干活
}
if (ctx.isInvalid) {
// 已经失效,赶紧退出
}
ctx.onInvalidated(callback) 则是在失效时执行清理动作,适合断开 MutationObserver、移除 DOM 元素这类「手动善后」。
Tip在异步函数里,
await之后一定要再查一次ctx.isInvalid。await 期间扩展可能刚好被重载,醒来后的代码不该继续跑。
最常见的坑:main 的返回值没人管
很多框架习惯用返回值做清理,比如 return () => observer.disconnect()。但在 WXT 里,main 的返回值不会被调用,这么写等于把清理函数丢进了虚空:扩展重载后,旧脚本的 MutationObserver 会继续观察页面,造成重复执行和内存泄漏。
正确的姿势是把清理逻辑交给 ctx:
export default defineContentScript({
matches: ["https://www.google.com/search*"],
main(ctx) {
const observer = new MutationObserver(() => {
// 处理 DOM 变化
});
observer.observe(document.body, { childList: true, subtree: true });
// 扩展失效时断开观察器
ctx.onInvalidated(() => observer.disconnect());
},
});
一个完整的防泄漏模式
看一个生产级写法(来自 mkext 项目的 Google 搜索结果页脚本,按 0.21.4 校准)。Google 的结果列表会高频变化,所以做了 200ms 防抖,并把所有异步出口都加上了失效检查:
export default defineContentScript({
matches: ["https://www.google.com/search*"],
runAt: "document_idle",
main(ctx) {
let scheduled: number | undefined;
let running = false;
let dirty = false;
const render = async () => {
if (running || ctx.isInvalid) return;
running = true;
dirty = false;
try {
// ...采集结果、发消息、渲染徽章
const response = await sendMessage(/* ... */).catch(() => null);
// await 之后再次检查,防止扩展刚好在请求期间被重载
if (ctx.isInvalid || response?.status !== "ok") return;
// ...写入 DOM
} finally {
running = false;
if (dirty) scheduleRender();
}
};
const scheduleRender = () => {
if (ctx.isInvalid) return;
if (scheduled !== undefined) return;
// ctx.setTimeout 在上下文失效时会自动清除,不会泄漏定时器
scheduled = ctx.setTimeout(() => {
scheduled = undefined;
void render();
}, 200);
};
void render();
const observer = new MutationObserver(scheduleRender);
observer.observe(document.body, { childList: true, subtree: true });
ctx.onInvalidated(() => observer.disconnect());
},
});
这个模式里有三层防线:ctx.setTimeout 保证定时器不泄漏;ctx.onInvalidated 保证观察器被断开;每次异步返回后查 ctx.isInvalid 保证不再碰 DOM。
Note扩展每次
wxt dev热重载也会让旧上下文失效,所以这套机制不只是为了正式发布,开发时同样在保护你。
小结
- ctx 追踪内容脚本上下文的有效性,解决「扩展没了脚本还在」的问题。
- 用
ctx.setTimeout等替身方法替代原生定时器,失效自动清理。 main的返回值不会被调用,清理请走ctx.onInvalidated。- 异步代码每个
await之后都要检查ctx.isInvalid。
下一章我们看看内容脚本怎么往网页里「长」出自己的界面。