首页 / 浏览器扩展开发入门教程 / scripting 注入脚本

浏览器扩展开发入门教程

scripting 注入脚本

本教程共 56 篇 · 第 36 篇 · 更新于 2026-08-13 · 约 6 分钟阅读

scripting注入脚本executeScriptinsertCSS内容脚本MV3注入

本节目标:学完你能用 chrome.scripting 在运行时把 JavaScript 或 CSS 注入到指定标签页,并分清”声明式”与”编程式”两种注入路径。

在 Manifest V3 里,想往网页里塞代码,chrome.scripting 是官方推荐、也几乎是唯一的”运行时注入”入口。它替代了早期直接拼函数字符串的做法,更安全、更清晰。

36-1 为什么是 scripting

早期的扩展可以用 tabs.executeScript 注入代码,但那个接口在 MV3 里已经被收敛到 chrome.scripting 之下。统一之后,注入能力集中在 scripting 命名空间,权限也更明确:你要注入,就得声明 "scripting" 权限;你想注入到某个网站,还得有那个网站的主机权限,或者靠 activeTab 临时放行。

这带来一个好处:用户安装时权限清单干净,运行时才按需获取授权,安全感更强。

{
  "name": "Scripting API Demo",
  "version": "1.0",
  "manifest_version": 3,
  "permissions": ["scripting", "storage"],
  "host_permissions": ["https://example.com/*"],
  "background": { "service_worker": "sw.js" }
}

36-2 executeScript 基础

executeScript 的核心,是告诉 Chrome”往哪个标签、注入什么”。最少需要两个部分:target(目标标签)和注入内容。

await chrome.scripting.executeScript({
  target: { tabId: 12345 },
  func: () => {
    document.body.style.backgroundColor = "red";
  }
});

这段代码把”把页面背景变红”的逻辑,注入到 id12345 的标签页里执行。注入的函数运行在页面的**隔离世界(ISOLATED world)**中,看不到页面自己的全局变量,但能操作 DOM。

36-3 func 与 files 两种注入

executeScript 注入内容有两种写法,二选一。

第一种是 func:直接传一个函数,Chrome 会把它的”源代码”序列化后注入执行。函数可以带参数,通过 args 传过去:

await chrome.scripting.executeScript({
  target: { tabId: 12345 },
  func: (color) => {
    document.body.style.backgroundColor = color;
  },
  args: ["blue"]
});

args 里的参数会被原样传给 func,适合需要动态传值的场景。注意 func 必须是能被序列化的纯函数,里面不能引用外部闭包变量。

第二种是 files:注入一个或多个已经写好的 .js 文件。

await chrome.scripting.executeScript({
  target: { tabId: 12345 },
  files: ["content-script.js"]
});

content-script.js 是扩展包里的一个文件,里面任意写。官方示例里,服务工作者监听到特定页面加载后,就用 files 把脚本注入进去:

chrome.webNavigation.onDOMContentLoaded.addListener(async ({ tabId, url }) => {
  if (url !== "https://example.com/#inject-programmatic") return;
  const { options } = await chrome.storage.local.get("options");
  chrome.scripting.executeScript({
    target: { tabId },
    files: ["content-script.js"],
    ...options
  });
});
Tip

经验法则:逻辑短小、需要传参,用 func;逻辑复杂、要复用、或者就是把现成的脚本文件塞进去,用 files

36-4 insertCSS / removeCSS

除了注入 JS,scripting 还能注入 CSS,用来改样式而不碰逻辑。注意 insertCSSexecuteScript 不一样,它没有 func 形态,只有两种写法:css 直接传字符串,或 files 传文件数组。

// 直接传一段 css 字符串
await chrome.scripting.insertCSS({
  target: { tabId: 12345 },
  css: "body { background: yellow !important; }"
});

// 或者注入一个 css 文件
await chrome.scripting.insertCSS({
  target: { tabId: 12345 },
  files: ["styles/highlight.css"]
});

注意上面的 css 字段传的是字符串。当注入出错时(比如页面是受保护的 chrome:// 页),会抛异常,可以用 try/catch 接住,给用户友好提示:

try {
  await chrome.scripting.insertCSS({
    target: { tabId: currentTab.id },
    css: items.css
  });
  console.log("样式注入成功");
} catch (e) {
  console.error(e);
  console.log("注入失败,是否处于特殊页面?");
}

想撤销样式,用 removeCSS,参数要和当初 insertCSS 时一致:

await chrome.scripting.removeCSS({
  target: { tabId: 12345 },
  css: "body { background: yellow !important; }"
});

36-5 target 与 world

target 不只是一个 tabId,它还能精细控制注入范围。比如 allFrames: true 表示连页面里的 iframe 一起注入:

await chrome.scripting.executeScript({
  target: { tabId: 12345, allFrames: true },
  func: () => console.log("我在主框架和所有 iframe 里都跑了")
});

world 这个字段决定代码跑在哪个”世界”:默认是 "ISOLATED"(隔离世界),和页面原有脚本互不干扰,这是最安全的选项;如果一定要和页面自己的 JS 共享全局状态(比如调用页面里已定义的库),才用 "MAIN"(主世界)。绝大多数情况用默认的隔离世界就对了。

await chrome.scripting.executeScript({
  target: { tabId: 12345 },
  func: () => { /* ... */ },
  world: "ISOLATED" // 默认值,可省略
});
Warning

MAIN 世界意味着你的代码和网页脚本共处同一环境,等于把信任交给了那个网页。除非确有需要,否则别用 MAIN,免得被页面脚本干扰或污染。

36-6 动态内容脚本

前面讲的都是”运行时临时注入”。还有一种”动态内容脚本”,用 registerContentScripts 注册后,让它像清单里声明的 content_scripts 一样,永久按规则自动注入:

await chrome.scripting.registerContentScripts([
  {
    id: "dynamic-script",
    js: ["content-script.js"],
    matches: ["https://example.com/*"],
    runAt: "document_idle",
    allFrames: false,
    world: "ISOLATED"
  }
]);

注册之后,只要打开匹配 matches 的页面,脚本就会自动注入,不用每次手动 executeScript。不想用了就注销:

await chrome.scripting.unregisterContentScripts({ ids: ["dynamic-script"] });

想看看当前注册了哪些,用 getRegisteredContentScripts

const scripts = await chrome.scripting.getRegisteredContentScripts();
console.log(scripts.map((s) => s.id));

36-7 获取注入脚本的返回值

executeScript 不只是”射出去不管”,它能把注入函数里 return 的值带回来。返回结果是一个数组,每个元素对应一个注入目标(比如 allFrames 时会有多个),结构形如 { result, frameId }

const [result] = await chrome.scripting.executeScript({
  target: { tabId: 12345 },
  func: () => document.title
});
console.log("页面标题是:", result.result);

注意拿到的是数组,要先取下标再取 .result。这个功能很实用——比如你想读取页面上的某个数据、统计 DOM 节点数量、或者确认注入是否生效,都可以通过返回值拿回来,而不必再走一遍消息通信。

Tip

返回值必须是 JSON 可序列化的类型(字符串、数字、普通对象、数组等)。你不能在注入函数里直接 return document.body 这种 DOM 节点,它序列化不了,会得到空值或报错。

36-8 权限声明速查

scripting 用到生产环境前,清单里这几样要备齐:必选的 "scripting" 权限;要注入的具体网站,写在 host_permissions 里,或用 activeTab 在用户主动调用时临时获取;注入的目标标签必须真实存在且可注入。

很多”注入没反应”的工单,根因都是权限没给够:要么是忘了声明 scripting,要么 host_permissions 没覆盖目标网址。排查时先确认这三件套,再去看 target.tabId 是不是有效值。

Note

动态内容脚本和”清单里写死的内容脚本”能力几乎一样,区别只是它能在运行时由代码决定规则,更灵活。注册型脚本默认跨会话保留(persistAcrossSessions 默认 true);需要临时性的,就显式传 persistAcrossSessions: false

36-9 权限与常见坑

把这一章收个尾:用 scripting 注入,三件事缺一不可——声明 "scripting" 权限、有目标网站的主机权限(或 activeTab)、目标标签确实存在且不是受保护页面(chrome://chrome-extension:// 等无法注入)。

另一个坑是注入的 func 是序列化后重建的,函数内部引用外部变量会失效,所有要用的值都通过 args 传进去。还有,executeScript 在隔离世界运行,能改 DOM 但读不到页面 JS 的私有变量,这是安全设计,不是 bug。

// 一个完整的最小示例:点图标后给当前页加红色边框
chrome.action.onClicked.addListener(async (tab) => {
  await chrome.scripting.executeScript({
    target: { tabId: tab.id },
    func: () => { document.documentElement.style.outline = "3px solid red"; }
  });
});