内容脚本是在网页上下文中运行的文件。借助标准 文档 对象模型 (DOM),它们能够读取浏览器访问的网页的详细信息、对其进行 更改,并将信息传递给其父级扩展程序。
了解内容脚本的功能
内容脚本可以直接访问以下扩展程序 API:
domi18nstorageruntime.connect()runtime.getManifest()runtime.getURL()runtime.idruntime.onConnectruntime.onMessageruntime.sendMessage()
内容脚本无法直接访问其他 API。但它们可以通过与其他扩展程序部分交换消息来间接访问这些 API。
您还可以使用 fetch() 等 API 从内容脚本访问扩展程序中的其他文件。为此,您需要将这些文件声明为
可供 Web 访问的资源。请注意,这也会将资源公开给在同一网站上运行的任何第一方或第三方脚本。
在隔离的世界中工作
内容脚本存在于隔离的世界中,这使得内容脚本能够更改其 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 |
布尔值 | 可选。脚本是否应注入到 about:blank 框架
其中父框架或打开器框架与
matches 中声明的其中一种模式匹配。默认值为 false。 |
match_origin_as_fallback |
布尔值 |
可选。脚本是否应注入到由匹配来源创建的框架中,但这些框架的网址或来源可能无法直接与模式匹配。这些框架包括具有不同架构的框架,例如
about:, data:, blob:, 和
filesystem:. 另请参阅
在相关框架中注入。
|
world |
ExecutionWorld |
可选。脚本要在其中执行的 JavaScript 世界。默认值为 ISOLATED。另请参阅
在隔离的世界中工作。
|
在文档生命周期的给定阶段内,清单中静态声明的内容脚本 是第一个注入的,在以任何其他方式注册的内容脚本 之前注入。它们会按照在清单中指定的顺序注入。
使用动态声明注入
当内容脚本的匹配模式不为人所知,或者内容脚本不应始终注入到已知宿主中时,动态内容脚本非常有用。
动态声明是在 Chrome 96 中引入的,与 静态
声明类似,但内容脚本对象是使用
chrome.scripting 命名空间中的方法而不是在
manifest.json中向 Chrome 注册的。借助 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"暂时授予宿主权限。
以下是基于 activeTab 的扩展程序的不同版本。
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_matches或exclude_globs模式匹配。由于matches属性是必需的,因此exclude_matches、include_globs和exclude_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/barhttps://the.example.com/foo/
但是,它与以下各项不匹配 :
https://my.example.com/foo/barhttps://example.com/foo/https://www.example.com/foo
此扩展程序会将内容脚本注入到 https://www.nytimes.com/arts/index.html 和 https://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.com 和 https://.nytimes.com/history 中,但不会注入到 https://science.nytimes.com 或 https://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 |
字符串 | 首选。请尽可能使用 "document_idle"。浏览器 会选择在 "document_end" 和立即之后注入脚本的时间
window.onload
事件触发。注入的确切时间取决于文档的复杂程度和加载所需的时间,并针对网页加载速度进行了优化。以 "document_idle" 运行的内容脚本不需要监听window.onload 事件,它们保证在 DOM 完成后运行。如果脚本确实需要在 window.onload 之后运行,扩展程序可以使用 document.readyState 属性检查 onload 是否已触发。 |
document_start |
字符串 | 脚本会在 css 中的任何文件之后注入,但在构建任何其他 DOM 或运行任何其他脚本之前注入。 |
document_end |
字符串 | 脚本会在 DOM 完成后立即注入,但在图片和框架等子资源加载之前注入。 |
指定框架
对于在清单中指定的声明式内容脚本,借助 "all_frames" 字段,扩展程序可以指定 JavaScript 和 CSS 文件应注入到与指定网址要求匹配的所有框架中,还是仅注入到标签页中的最顶层框架中:
manifest.json
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"all_frames": true,
"js": ["contentScript.js"]
}
],
...
}
使用 chrome.scripting.registerContentScripts(...) 以编程方式注册内容脚本时,可以使用 allFrames 参数指定内容脚本应注入到与指定网址要求匹配的所有框架中,还是仅注入到标签页中的最顶层框架中。此参数只能与 tabId 搭配使用,如果指定了 frameIds 或 documentIds,则无法使用此参数:
service-worker.js
chrome.scripting.registerContentScripts([{
id: "test",
matches : [ "https://*.nytimes.com/*" ],
allFrames : true,
js : [ "contentScript.js" ],
}]);
在相关框架中注入
扩展程序可能需要在与匹配框架相关的框架中运行脚本,但这些框架本身并不匹配。在这种情况下,常见的情形是框架的网址是由匹配框架创建的,但这些框架的网址本身与脚本的指定模式不匹配。
当扩展程序想要注入到具有 about:、data:、blob: 和 filesystem: 架构的框架中时,就会出现这种情况。在这些情况下,网址与内容脚本的模式不匹配(对于 about: 和
data:,甚至根本不包含网址中的父级网址或源,如 about:blank 或 data: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)。
框架的启动器是创建或导航到目标框架的框架。虽然这通常是直接父框架或打开器框架,但可能不是(例如,框架导航到 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 文件中声明为 可供 Web 访问的资源:
manifest.json
{
...
"web_accessible_resources": [
{
"resources": [ "images/*.png" ],
"matches": [ "https://example.com/*" ]
},
{
"resources": [ "fonts/*.woff" ],
"matches": [ "https://example.com/*" ]
}
],
...
}
内容安全政策
在隔离的世界中运行的内容脚本具有以下 内容安全政策 (CSP):
script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' chrome-extension://abcdefghijklmopqrstuvwxyz/; object-src 'self';
与其他扩展程序上下文应用的限制类似,这可以防止使用 eval() 以及加载外部脚本。
对于未打包的扩展程序,CSP 还包括 localhost:
script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' http://localhost:* http://127.0.0.1:* chrome-extension://abcdefghijklmopqrstuvwxyz/; object-src 'self';
当内容脚本注入到主世界中时,网页的 CSP 适用。
确保安全
虽然隔离的世界提供了一层保护,但使用内容脚本可能会在扩展程序和网页中造成漏洞。如果内容脚本从单独的网站接收内容(例如通过调用 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,这些 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);