首页 / WXT 浏览器扩展框架教程 / Content Script:往网页里注入代码

WXT 浏览器扩展框架教程

Content Script:往网页里注入代码

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

WXTcontent-script内容脚本浏览器扩展网页注入

本节目标:认识内容脚本入口与 defineContentScript,掌握 matches 等核心配置,理解最小站点范围原则和 main 里的清理方式。

内容脚本是什么

内容脚本(content script)是注入到网页里运行的 JS。它像派到别人家的代表:能读能改网页 DOM,但和网页脚本隔离,有自己的小世界。隔离机制、UI 注入等进阶内容见 §15。

创建内容脚本

命名规则比前面几种灵活:

📂 entrypoints/
   📄 content.ts                # 通用内容脚本
   📄 google-search.content.ts  # 带名字的内容脚本
   📂 youtube.content/
      📄 index.ts               # 目录写法

带名字的写法适合多个内容脚本并存。构建后输出到 content-scripts/ 目录,比如 google-search.content.ts 变成 content-scripts/google-search.js

最小内容脚本:

// entrypoints/example.content.ts
export default defineContentScript({
  matches: ['*://*.wxt.dev/*'],
  main() {
    console.log('脚本已注入', location.href);
  },
});

核心配置项

内容脚本的配置几乎都是 manifest 选项,WXT 自动写入:

配置作用
matches(必填)哪些网址注入,用 match pattern 写法
excludeMatches排除哪些网址
runAt注入时机:document_start / document_end / document_idle
allFrames是否也注入所有 iframe
worldISOLATED(默认)或 MAIN(主世界)
registrationmanifest(默认,构建时注册)或 runtime(运行时注册,§13 / §16)
cssInjectionModeCSS 注入方式:manifest / manual / ui

match pattern 的通用写法是 scheme://host/path

matches: [
  'https://example.com/*',
  'https://*.example.com/articles/*',
],

把 match pattern 想成门牌号:https://example.com/* 是「这一户」,https://*.example.com/* 是「这条街上所有分店」。写错门牌号,脚本要么进错门(注入到不该去的站点),要么根本进不去(该注入的站点没匹配上)。写完先在心里过一遍每个 pattern 会命中哪些网址,再交给浏览器。

最小站点范围原则

内容脚本的 matches 就是扩展的网页访问范围。范围越大,权限越大,审核越严,风险越高。原则只有一条:只匹配功能需要的站点

mkext 的教程专门提醒:演示项目为了「开箱见效果」用了 http://*/* 全站匹配,真实产品绝不该照抄。功能只在一个站点用,就只匹配那个站点:

export default defineContentScript({
  matches: ['https://docs.example.com/*'],
  main(ctx) {
    // 业务逻辑
  },
});

别为了省事写

。先收窄站点和路径,再决定功能形态。只在你需要时才扩大范围。

运行时代码必须进 main

和后台一样,内容脚本文件也会被构建期 Node 导入,顶层运行时代码(比如 document.createElement)会出错。全部放进 main:

export default defineContentScript({
  matches: ['https://example.com/*'],
  main(ctx) {
    const heading = document.querySelector('h1')?.textContent;
    console.log(heading);
  },
});

main 可以是 async——这是与后台 main 的重要区别(后台 main 不能 async,见 §10)。

用 ctx 清理,别依赖返回值

扩展更新、禁用时,内容脚本会被浏览器终止,但页面里可能残留事件监听、定时器、MutationObserver。清理工作交给 ctx 对象:

main(ctx) {
  const observer = new MutationObserver(() => {});
  observer.observe(document.body, { childList: true });

  // 脚本失效时自动断开
  ctx.onInvalidated(() => observer.disconnect());
}

不要写

指望 WXT 调用返回值——WXT 不会调用它,清理是空承诺。要用 ctx 的 onInvalidatedsetTimeout 等助手。ctx 的完整生命周期与失效机制在第 14 章。

真实示例

mkext 的 Google 搜索增强脚本:只在 Google 搜索页注入,document_idle 时机执行,监听结果变化并防抖重扫,失效时断开监听:

// entrypoints/google-search.content.ts(简化)
export default defineContentScript({
  matches: ['https://www.google.com/search*'],
  runAt: 'document_idle',
  main(ctx) {
    const observer = new MutationObserver(() => {
      // 结果变化后重扫,200ms 防抖
    });
    observer.observe(document.body, { childList: true, subtree: true });
    ctx.onInvalidated(() => observer.disconnect());
  },
});

注意 runAt: 'document_idle' 的写法:等页面主体加载完再注入,避免和页面初始化抢时间。

allFrames 默认只注入顶层页面;要覆盖 iframe 里的场景(比如视频网站的播放器)才需要打开,开了之后每个 frame 都会执行一次脚本。

小结

  • 内容脚本按 matches 注入网页,最小站点范围既是原则也是审核要求。
  • runAt 控制注入时机,registration 决定「manifest 注册」还是「运行时注册」。
  • 入口注册细节与上下文生命周期分别在 §07 与 §14。