chrome.scripting 与动态注入

静态 content_scripts 声明的是「开局即定」的注入计划;chrome.scripting 则是运行时武器:什么时候注入、注入到哪个标签页、注入什么代码,都可以在代码里现场决定。它是 activeTab 模式的最佳搭档,也是动态注册 content script 的入口。Chrome 88+ 可用,需要 scripting 权限。

executeScript:三种注入物

注入内容分三类:文件、函数、CSS(最后一个走 insertCSS):

// 1. 注入文件
await chrome.scripting.executeScript({
  target: { tabId: tab.id },
  files: ["content.js"]
});

// 2. 注入函数(可带参数)
function highlight(keyword, color) {
  const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
  const hits = [];
  while (walker.nextNode()) {
    if (walker.currentNode.textContent.includes(keyword)) hits.push(walker.currentNode);
  }
  hits.forEach(node => {
    const span = document.createElement("span");
    span.style.background = color;
    node.after(span); // 简化示意:实际需拆分文本节点
  });
  return hits.length;
}

const [result] = await chrome.scripting.executeScript({
  target: { tabId: tab.id },
  func: highlight,
  args: ["性能", "#fff3a3"]
});
console.log("命中:", result.result);

两个铁律。第一,注入的函数是序列化副本,不是原函数:函数体必须自包含,闭包变量、外部 import 一律不可用,需要的数据走 args 传(只能是可 JSON 序列化的值)。第二,返回值同样要可序列化,DOM 节点这类对象传不回来,返回计数、字符串、纯数据。

target:往哪注入

target 必填 tabId,另有三个控制维度:

参数 含义
allFrames: true 注入该标签页所有 iframe
frameIds: [1, 2] 只注入指定 frame
world: "MAIN" / "ISOLATED" 运行在页面世界还是隔离世界

frameIds 与 allFrames 互斥,同时写会报错。world 参数值得多说一句:默认 ISOLATED 与静态 content script 同处隔离世界;world: "MAIN" 则注入到页面自己的 JavaScript 世界,可以 hook 页面函数、读取页面变量——代价是与页面代码互相可见,安全边界完全不同,审核时使用 MAIN world 的扩展会被更仔细地看。

insertCSS 与 removeCSS

await chrome.scripting.insertCSS({
  target: { tabId: tab.id },
  css: "body { filter: invert(0.92) hue-rotate(180deg); }"
});

// 撤销
await chrome.scripting.removeCSS({
  target: { tabId: tab.id },
  css: "body { filter: invert(0.92) hue-rotate(180deg); }"
});

insertCSS 支持直接传字符串(executeScript 没有对应的字符串形态,只能 files 或 func)。removeCSS 需要传入与 insertCSS 完全相同的字符串,适合做成常量。这种「注入-撤销」成对的样式非常适合暗黑模式开关类功能。

动态注册 content script

scripting 命名空间的另一组方法管理「运行时登记」的自动注入规则:

// 登记
await chrome.scripting.registerContentScripts([{
  id: "user-rule-1",
  matches: ["*://example.com/*"],
  js: ["content.js"],
  runAt: "document_start",
  persistAcrossSessions: false
}]);

// 查询 / 更新 / 注销
const list = await chrome.scripting.getRegisteredContentScripts();
await chrome.scripting.updateContentScripts([{ id: "user-rule-1", excludeMatches: ["*://example.com/admin*"] }]);
await chrome.scripting.unregisterContentScripts({ ids: ["user-rule-1"] });

这是做「用户自定义站点规则」功能的正解:用户在设置页添加一条站点规则,后台 registerContentScripts 登记即可,不必写死在 manifest。persistAcrossSessions 决定登记是否跨浏览器重启存活。

权限组合速记

想做的事 需要的权限
点击图标注入当前页 scripting + activeTab
自动注入指定站点 scripting + host_permissions
用户运行时添加规则 scripting + optional_host_permissions + 动态注册

注入的 URL 匹配逻辑写多了,正则功底决定效率,见 正则实战。与静态注入的取舍对比见 《Content Scripts》 篇。官方文档:https://developer.chrome.com/docs/extensions/reference/api/scripting

注入失败的处理范式

executeScript 可能失败:目标页是受限页、frame 没权限、页面正在卸载。生产代码要把失败当正常分支处理:

async function safeInject(tabId, file) {
  try {
    const [res] = await chrome.scripting.executeScript({
      target: { tabId },
      files: [file]
    });
    return { ok: true, result: res?.result };
  } catch (e) {
    const msg = String(e?.message ?? e);
    if (msg.includes("cannot be scripted")) {
      return { ok: false, reason: "restricted" }; // chrome:// 等受限页
    }
    if (msg.includes("Cannot access")) {
      return { ok: false, reason: "no-access" };  // 无 host 权限
    }
    return { ok: false, reason: "other" };
  }
}

返回结构化结果而不是抛异常,调用方(popup 或命令处理)就能按 reason 给用户看不同的提示。这个范式在第五章实战项目里会反复出现。

动态注册与 onInstalled 的配合

registerContentScripts 的调用时机有讲究。放 worker 顶层会在每次唤醒时重复执行并报「id 已存在」,正确位置是 onInstalled(安装/更新时执行一次),并配合查询判重:

chrome.runtime.onInstalled.addListener(async () => {
  const registered = await chrome.scripting.getRegisteredContentScripts();
  const has = registered.some(s => s.id === "user-rule-1");
  if (!has) {
    await chrome.scripting.registerContentScripts([{
      id: "user-rule-1",
      matches: ["*://example.com/*"],
      js: ["content.js"],
      persistAcrossSessions: true
    }]);
  }
});

persistAcrossSessions: true 的注册跨重启保留,只在「用户改了规则」时增删;false 的注册则需要在 onStartup 里补挂。两种生命周期的选择取决于你的规则是否需要长期生效。

与静态声明的混合策略

实际项目常常静态与动态并用:核心功能用静态声明保证「装了就有」,用户自定义规则走动态注册。划分原则是「规则是否由用户数据决定」。混合时注意执行顺序:同一页面静态脚本先注入,动态注册的次之,程序化注入最后。依赖顺序的逻辑(比如先注入工具库再注入业务脚本)不要跨注册方式串联,同一种方式的数组顺序才是可靠保证。

func 注入、files 注入与动态注册三套姿势,配合 《Content Scripts》 篇的静态声明,覆盖了 MV3 注入体系的全貌。URL 匹配的正则基础见 正则实战。官方文档:https://developer.chrome.com/docs/extensions/reference/api/scripting

本章由 WorkBuddy 原创整理,API 行为依据 Chrome 扩展官方文档(Manifest V3)。