内容脚本

内容脚本是在网页环境中运行的文件。通过使用标准文档对象模型 (DOM),用户能够读取浏览器所访问网页的详细信息,对这些网页进行更改,并将信息传递给父级扩展程序。

了解内容脚本功能

内容脚本在将扩展程序文件声明为可通过网络访问的资源后,便可访问它们。它们可以直接访问以下扩展程序 API:

内容脚本无法直接访问其他 API。但它们可以通过与扩展程序的其他部分交换消息来间接访问它们。

在偏僻的世界中工作

内容脚本位于一个独立的环境中,这使得内容脚本可以对其 JavaScript 环境进行更改,而不会与网页或其他扩展程序的内容脚本发生冲突。

扩展程序可能在包含如下代码的网页中运行。

webPage.html

<html>
  <button id="mybutton">click me</button>
  <script>
    var greeting = "hello, ";
    var button = document.getElementById("mybutton");
    button.person_name = "Bob";
    button.addEventListener(
        "click", () => alert(greeting + button.person_name + "."), false);
  </script>
</html>

该扩展程序可以使用注入脚本部分概述的其中一种方法注入以下内容脚本。

content-script.js

var greeting = "hola, ";
var button = document.getElementById("mybutton");
button.person_name = "Roberto";
button.addEventListener(
    "click", () => alert(greeting + button.person_name + "."), false);

进行此更改后,用户点击按钮时,系统会按顺序显示两条提醒。

注入脚本

内容脚本可以以静态方式声明以动态方式声明以编程方式注入

注入静态声明

对于应在一组已知页面上自动运行的脚本,请在 manifest.json 中使用静态内容脚本声明。

静态声明的脚本在清单中的 "content_scripts" 键下注册。该文件可以包含 JavaScript 文件和/或 CSS 文件。所有自动运行的内容脚本都必须指定匹配模式

manifest.json

{
 "name": "My extension",
 ...
 "content_scripts": [
   {
     "matches": ["https://*.nytimes.com/*"],
     "css": ["my-styles.css"],
     "js": ["content-script.js"]
   }
 ],
 ...
}

名称 类型 说明
matches 字符串数组 必需。指定要将此内容脚本注入哪些网页。如需详细了解这些字符串的语法,请参阅匹配模式;如需详细了解如何排除网址,请参阅匹配模式和 glob
css 字符串数组 可选。要注入到匹配页面的 CSS 文件列表。这些元素会按照它们在此数组中出现的顺序进行注入,然后再为网页构建或显示任何 DOM。
js 字符串数组 可选。要注入到匹配页面的 JavaScript 文件的列表。系统会按照文件在此数组中出现的顺序注入文件。此列表中的每个字符串都必须包含扩展程序根目录中某项资源的相对路径。前导斜杠(“/”)会自动剪除。
run_at RunAt 可选。指定何时应将脚本注入到网页中。默认值为 document_idle
match_about_blank boolean 可选。脚本是否应注入到 about:blank 帧中,其中父帧或起始帧与 matches 中声明的模式之一匹配。默认值为 false。
match_origin_as_fallback boolean 可选。脚本是否应注入由匹配的来源创建的帧,但其网址或来源可能与模式不直接匹配。其中包括具有不同架构的帧,例如 about:data:blob:filesystem:。另请参阅在相关帧中注入
world ExecutionWorld 可选。用于执行脚本的 JavaScript 环境。默认值为 ISOLATED。另请参阅在隔离的世界中工作

注入动态声明

如果内容脚本的匹配模式并不为人所知,或者内容脚本不应始终注入到已知主机上,则动态内容脚本非常有用。

动态声明是在 Chrome 96 中引入的,它与静态声明类似,只不过内容脚本对象是使用 chrome.scripting 命名空间中的方法在 Chrome 中注册的,而不是在 manifest.json 中注册的。Scripting API 还允许扩展程序开发者执行以下操作:

  • 注册内容脚本。
  • 获取已注册内容脚本的列表。
  • 更新已注册内容脚本的列表。
  • 移除已注册的内容脚本。

与静态声明一样,动态声明可以包含 JavaScript 文件和/或 CSS 文件。

service-worker.js

chrome.scripting
  .registerContentScripts([{
    id: "session-script",
    js: ["content.js"],
    persistAcrossSessions: false,
    matches: ["*://example.com/*"],
    runAt: "document_start",
  }])
  .then(() => console.log("registration complete"))
  .catch((err) => console.warn("unexpected error", err))

service-worker.js

chrome.scripting
  .updateContentScripts([{
    id: "session-script",
    excludeMatches: ["*://admin.example.com/*"],
  }])
  .then(() => console.log("registration updated"));

service-worker.js

chrome.scripting
  .getRegisteredContentScripts()
  .then(scripts => console.log("registered content scripts", scripts));

service-worker.js

chrome.scripting
  .unregisterContentScripts({ ids: ["session-script"] })
  .then(() => console.log("un-registration complete"));

以编程方式注入

对于需要为响应事件或在特定情况下运行的内容脚本,使用程序化注入。

如需以编程方式注入内容脚本,扩展程序需要对尝试将脚本注入到的页面拥有主机权限。您可以通过在扩展程序清单中请求这些权限来授予主机权限,也可以临时使用 "activeTab" 来授予。

以下是基于 activityTab 的扩展程序的不同版本。

manifest.json:

{
  "name": "My extension",
  ...
  "permissions": [
    "activeTab",
    "scripting"
  ],
  "background": {
    "service_worker": "background.js"
  },
  "action": {
    "default_title": "Action Button"
  }
}

内容脚本可作为文件注入。

content-script.js


document.body.style.backgroundColor = "orange";

service-worker.js

chrome.action.onClicked.addListener((tab) => {
  chrome.scripting.executeScript({
    target: { tabId: tab.id },
    files: ["content-script.js"]
  });
});

或者,可以注入函数正文,并将其作为内容脚本执行。

service-worker.js

function injectedFunction() {
  document.body.style.backgroundColor = "orange";
}

chrome.action.onClicked.addListener((tab) => {
  chrome.scripting.executeScript({
    target : {tabId : tab.id},
    func : injectedFunction,
  });
});

请注意,注入的函数是 chrome.scripting.executeScript() 调用中引用的函数的副本,而不是原始函数本身。因此,函数的正文必须是自包含;如果引用函数外部的变量,会导致内容脚本抛出 ReferenceError

作为函数注入时,您还可以将实参传递给函数。

service-worker.js

function injectedFunction(color) {
  document.body.style.backgroundColor = color;
}

chrome.action.onClicked.addListener((tab) => {
  chrome.scripting.executeScript({
    target : {tabId : tab.id},
    func : injectedFunction,
    args : [ "orange" ],
  });
});

排除匹配项和 glob

如需自定义指定的网页匹配,请在声明式注册中添加以下字段。

名称 类型 说明
exclude_matches 字符串数组 可选。不包括此内容脚本本应注入的网页。如需详细了解这些字符串的语法,请参阅匹配模式
include_globs 字符串数组 可选。matches 之后应用,以仅包含也与此 glob 匹配的网址。这旨在模拟 @include Greasemonkey 关键字。
exclude_globs 字符串数组 可选。matches 之后应用,以排除与此 glob 匹配的网址。用于模拟 @exclude Greasemonkey 关键字。

如果同时满足以下两个条件,内容脚本将会注入网页中:

  • 该网址与任何 matches 格式和任何 include_globs 格式匹配。
  • 该网址也不符合 exclude_matchesexclude_globs 格式。由于 matches 属性是必需属性,因此 exclude_matchesinclude_globsexclude_globs 只能用于限制哪些网页会受到影响。

以下扩展程序会将内容脚本注入 https://www.nytimes.com/health,但不会注入 https://www.nytimes.com/business

manifest.json

{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.nytimes.com/*"],
      "exclude_matches": ["*://*/*business*"],
      "js": ["contentScript.js"]
    }
  ],
  ...
}

service-worker.js

chrome.scripting.registerContentScripts([{
  id : "test",
  matches : [ "https://*.nytimes.com/*" ],
  excludeMatches : [ "*://*/*business*" ],
  js : [ "contentScript.js" ],
}]);

glob 属性遵循与匹配模式不同且更灵活的语法。可接受的 glob 字符串是指可能包含“通配符”星号和问号的网址。星号 (*) 匹配任何长度的字符串(包括空字符串),而问号 (?) 可匹配任何单个字符。

例如,glob https://???.example.com/foo/\* 与以下任何一项匹配:

  • https://www.example.com/foo/bar
  • https://the.example.com/foo/

但是,它与以下内容匹配:

  • https://my.example.com/foo/bar
  • https://example.com/foo/
  • https://www.example.com/foo

此扩展程序会将内容脚本注入 https://www.nytimes.com/arts/index.htmlhttps://www.nytimes.com/jobs/index.htm*,但不会注入 https://www.nytimes.com/sports/index.html

manifest.json

{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.nytimes.com/*"],
      "include_globs": ["*nytimes.com/???s/*"],
      "js": ["contentScript.js"]
    }
  ],
  ...
}

此扩展程序会将内容脚本注入 https://history.nytimes.comhttps://.nytimes.com/history,但不会注入 https://science.nytimes.comhttps://www.nytimes.com/science

manifest.json

{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.nytimes.com/*"],
      "exclude_globs": ["*science*"],
      "js": ["contentScript.js"]
    }
  ],
  ...
}

可以加入其中之一、全部或部分,以实现正确的范围。

manifest.json

{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.nytimes.com/*"],
      "exclude_matches": ["*://*/*business*"],
      "include_globs": ["*nytimes.com/???s/*"],
      "exclude_globs": ["*science*"],
      "js": ["contentScript.js"]
    }
  ],
  ...
}

运行时间

run_at 字段用于控制何时将 JavaScript 文件注入网页。首选的默认值为 "document_idle"。如需了解其他可能的值,请参阅 RunAt 类型。

manifest.json

{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.nytimes.com/*"],
      "run_at": "document_idle",
      "js": ["contentScript.js"]
    }
  ],
  ...
}

service-worker.js

chrome.scripting.registerContentScripts([{
  id : "test",
  matches : [ "https://*.nytimes.com/*" ],
  runAt : "document_idle",
  js : [ "contentScript.js" ],
}]);
名称 类型 说明
document_idle string 首选。尽可能使用 "document_idle"

浏览器会选择一个在 "document_end" 之间以及 window.onload 事件触发后立即注入脚本的时间。注入的确切时刻取决于文档的复杂程度和加载需要多长时间,并且针对网页加载速度进行了优化。

"document_idle" 运行的内容脚本不需要监听 window.onload 事件,它们一定会在 DOM 完成后运行。如果脚本确实需要在 window.onload 之后运行,该扩展程序可以使用 document.readyState 属性检查 onload 是否已触发。
document_start string 脚本是在来自 css 的任何文件之后、构建任何其他 DOM 或运行任何其他脚本之前注入的。
document_end string 脚本会在 DOM 完成后、子资源(如图片和帧)加载之前立即注入。

指定框架

通过 "all_frames" 字段,该扩展程序可以指定是应将 JavaScript 和 CSS 文件注入到符合指定网址要求的所有帧,还是仅注入到标签页的最顶层框架。

manifest.json

{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.nytimes.com/*"],
      "all_frames": true,
      "js": ["contentScript.js"]
    }
  ],
  ...
}

service-worker.js

chrome.scripting.registerContentScripts([{
  id: "test",
  matches : [ "https://*.nytimes.com/*" ],
  allFrames : true,
  js : [ "contentScript.js" ],
}]);
名称 类型 说明
all_frames boolean 可选。默认为 false,这意味着仅匹配顶层帧。

如果指定了 true,所有帧都将注入到其中,即使该帧不是标签页中的最顶层帧也是如此。系统会分别检查每个帧是否符合网址要求。如果不符合网址要求,它将不会注入到子框架中。

扩展程序可能希望在与匹配帧相关但本身并不匹配的帧中运行脚本。这种情况的一种常见情况是,其网址由匹配的帧创建,但其网址本身与脚本的指定格式不匹配。

如果扩展程序想要在网址采用 about:data:blob:filesystem: 架构的框架中注入,就会出现这种情况。在这些情况下,网址与内容脚本的格式不匹配(对于 about:data:,甚至完全不在网址中包含父网址或来源,如 about:blankdata:text/html,<html>Hello, World!</html> 中一样)。但是,这些帧仍然可以与创建帧相关联。

如需注入到这些帧中,扩展程序可以在清单中的内容脚本规范中指定 "match_origin_as_fallback" 属性。

manifest.json

{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.google.com/*"],
      "match_origin_as_fallback": true,
      "js": ["contentScript.js"]
    }
  ],
  ...
}

如果此政策已指定并设为 true,Chrome 会通过查看帧的发起者的来源来确定帧是否匹配,而不是查看帧本身的网址。请注意,这可能与目标帧的原点(例如data: 网址的来源为 null)。

帧的发起者是创建目标帧或浏览目标帧的帧。虽然该元素通常是直接父项或 Opener,但也可能不是(例如,某个框架在 iframe 内导航 iframe 时)。

因为这会比较发起者帧的源站,所以发起者帧可以位于该源站的任何路径上。为明确这一点,Chrome 要求将 "match_origin_as_fallback" 指定为 true 的所有内容脚本也指定 * 路径。

如果同时指定了 "match_origin_as_fallback""match_about_blank",则优先使用 "match_origin_as_fallback"

与嵌入页面的通信

虽然内容脚本的执行环境和托管它们的页面是相互隔离的,但它们会共享对页面 DOM 的访问权限。如果网页想要通过内容脚本与内容脚本通信,或通过内容脚本与扩展程序通信,则必须通过共享 DOM 实现。

例如,可以使用 window.postMessage() 完成:

content-script.js

var port = chrome.runtime.connect();

window.addEventListener("message", (event) => {
  // We only accept messages from ourselves
  if (event.source !== window) {
    return;
  }

  if (event.data.type && (event.data.type === "FROM_PAGE")) {
    console.log("Content script received: " + event.data.text);
    port.postMessage(event.data.text);
  }
}, false);

example.js

document.getElementById("theButton").addEventListener("click", () => {
  window.postMessage(
      {type : "FROM_PAGE", text : "Hello from the webpage!"}, "*");
}, false);

非扩展程序页面 example.html 会将消息发布到其自身。内容脚本会拦截并检查此消息,然后再将其发布到扩展程序进程。这样,页面就可以与扩展程序进程建立通信渠道。反之亦然。

访问扩展程序文件

如需从内容脚本访问扩展程序文件,您可以调用 chrome.runtime.getURL() 来获取扩展程序资源的绝对网址,如以下示例所示 (content.js):

content-script.js

let image = chrome.runtime.getURL("images/my_image.png")

如需在 CSS 文件中使用字体或图片,您可以使用 @@extension_id 构建网址,如以下示例所示 (content.css):

content.css

body {
 background-image:url('chrome-extension://__MSG_@@extension_id__/background.png');
}

@font-face {
 font-family: 'Stint Ultra Expanded';
 font-style: normal;
 font-weight: 400;
 src: url('chrome-extension://__MSG_@@extension_id__/fonts/Stint Ultra Expanded.woff') format('woff');
}

所有资源都必须在 manifest.json 文件中声明为网络可访问的资源

manifest.json

{
 ...
 "web_accessible_resources": [
   {
     "resources": [ "images/*.png" ],
     "matches": [ "https://example.com/*" ]
   },
   {
     "resources": [ "fonts/*.woff" ],
     "matches": [ "https://example.com/*" ]
   }
 ],
 ...
}

确保安全

虽然隔离的世界提供了一层保护,但使用内容脚本可能会在扩展程序和网页中产生漏洞。如果内容脚本从另一个网站收到内容(例如通过调用 fetch()),在注入内容之前,请务必过滤内容以防范跨站脚本攻击。仅通过 HTTPS 通信以避免"man-in-the-middle"攻击。

确保过滤掉恶意网页。例如,以下模式很危险,在 Manifest V3 中是不允许使用的:

错误做法

content-script.js

const data = document.getElementById("json-data");
// WARNING! Might be evaluating an evil script!
const parsed = eval("(" + data + ")");
错误做法

content-script.js

const elmt_id = ...
// WARNING! elmt_id might be '); ... evil script ... //'!
window.setTimeout("animate(" + elmt_id + ")", 200);

相反,应选用不会运行脚本的更安全的 API:

正确做法

content-script.js

const data = document.getElementById("json-data")
// JSON.parse does not evaluate the attacker's scripts.
const parsed = JSON.parse(data);
正确做法

content-script.js

const elmt_id = ...
// The closure form of setTimeout does not evaluate scripts.
window.setTimeout(() => animate(elmt_id), 200);