首页 / WXT 浏览器扩展框架教程 / 内容脚本的上下文与生命周期

WXT 浏览器扩展框架教程

内容脚本的上下文与生命周期

本教程共 45 篇 · 第 14 篇 · 更新于 2026-08-13 · 约 3 分钟阅读

WXT内容脚本生命周期ctx清理异步

本节目标:搞懂内容脚本 main(ctx) 收到的 ctx 是什么,为什么内容脚本会「过期」,以及如何用 ctx 提供的一系列方法,让异步代码在扩展被卸载、更新或禁用后自动停止。

上一章我们学会了用 defineContentScript 注册内容脚本。你有没有注意到,main 函数第一个参数是个 ctx?它不只是一个摆设,它管着内容脚本的「生死」。

ctx 是什么

ctxContentScriptContext 类型的实例,内容脚本的主要职责是追踪上下文是否失效(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

下一章我们看看内容脚本怎么往网页里「长」出自己的界面。