Content scripts
内容脚本是在网页上下文中运行的文件。通过使用标准的文档对象模型 (DOM),他们能够阅读浏览器访问的网页的详细信息,对其进行更改,并将信息传递给其父扩展程序。
# 了解内容脚本功能
内容脚本可以通过与扩展程序交换消息(messages)来访问其父扩展程序使用的 Chrome API
。他们还可以使用 chrome.runtime.getURL()
访问扩展文件的 URL,并使用与其他 URL 相同的结果。
// Code for displaying <extensionDir>/images/myimage.png:
var imgURL = chrome.runtime.getURL("images/myimage.png");
document.getElementById("someImage").src = imgURL;
此外,内容脚本可以直接访问以下 chrome API:
内容脚本无法直接访问其他 API。
# 在孤立的世界中工作(Work in isolated worlds)
内容脚本存在于一个孤立的世界中,允许内容脚本对其 JavaScript 环境进行更改,而不会与页面或其他扩展的内容脚本发生冲突。
隔离世界是页面或其他扩展无法访问的私有执行环境。这种隔离的一个实际结果是扩展程序内容脚本中的 JavaScript 变量对主机页面或其他扩展程序的内容脚本不可见。这个概念最初是在 Chrome 最初推出时引入的,为浏览器选项卡提供隔离。
扩展程序可以在网页中运行,代码类似于下面的示例。
<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>
该扩展可以使用注入脚本(Inject scripts)部分中概述的技术之一注入以下内容脚本。
var greeting = "hola, ";
var button = document.getElementById("mybutton");
button.person_name = "Roberto";
button.addEventListener("click", () =>
alert(greeting + button.person_name + ".")
, false);
通过此更改,当单击按钮时,两个警报会按顺序显示。
不仅每个扩展都在自己的孤立世界中运行,而且内容脚本和网页也是如此。这意味着这些(网页、内容脚本和任何正在运行的扩展)都不能访问其他人的上下文和变量。
# Inject scripts(注入 脚本)
内容脚本可以静态声明(declared statically)或以编程方式注入(programmatically injected)。
# 注入静态声明(Inject with static declarations)
将 manifest.json 中的静态内容脚本声明用于应在一组众所周知的页面上自动运行的脚本。
静态声明的脚本在“content_scripts”字段下的清单中注册。它们可以包括 JavaScript 文件、CSS 文件或两者。所有自动运行的内容脚本都必须指定匹配模式。
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"css": ["my-styles.css"],
"js": ["content-script.js"]
}
],
...
}
Name | Type | Description |
---|---|---|
matches |
array of strings | 必需的。指定此内容脚本将被注入到哪些页面。有关这些字符串的语法的更多详细信息,请参阅匹配模式(Match Patterns),有关如何排除 URL 的信息,请参阅匹配模式和全局(Match patterns and globs)。 |
css |
array of strings | 可选的。要注入匹配页面的 CSS 文件列表。在为页面构造或显示任何 DOM 之前,它们按照它们在此数组中出现的顺序注入。 |
js |
array of strings | 可选的。要注入匹配页面的 JavaScript 文件列表。它们按照它们在此数组中出现的顺序注入。 |
match_about_blank |
boolean | 可选的。脚本是否应注入 about:blank 框架,其中父框架或开启框架与matches 中声明的模式之一匹配。默认为假。 |
# Inject programmatically(以编程方式注入)
对需要运行以响应事件或特定场合的内容脚本使用程序化注入。
为了以编程方式注入内容脚本,您的扩展程序需要对其尝试注入脚本的页面的主机权限。可以通过将它们作为扩展清单的一部分(请参阅 host_permissions
)或通过 activeTab 临时请求来授予主机权限。
下面我们将看看基于 activeTab
(activeTab-based) 的扩展的不同版本。
//// manifest.json ////
{
"name": "My extension",
...
"permissions": [
"activeTab"
],
"background": {
"service_worker": "background.js"
}
}
内容脚本可以作为文件注入......
//// content-script.js ////
document.body.style.backgroundColor = 'orange';
//// background.js ////
chrome.action.onClicked.addListener((tab) => {
chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ['content-script.js']
});
});
...或者函数体可以作为内容脚本注入和执行。
//// background.js ////
function injectedFunction() {
document.body.style.backgroundColor = 'orange';
}
chrome.action.onClicked.addListener((tab) => {
chrome.scripting.executeScript({
target: { tabId: tab.id },
function: injectedFunction
});
});
请注意,注入的函数是 chrome.scripting.executeScript
调用中引用的函数的副本,而不是原始函数本身。因此,函数体必须是自包含的;对函数外部变量的引用将导致内容脚本抛出一个 ReferenceError
。
# 排除匹配项和全局(Exclude matches and globs)
通过在声明性注册中包含以下字段,可以自定义指定的页面匹配。
Name | Type | Description |
---|---|---|
exclude_matches |
array of strings | 可选的。排除此内容脚本将被注入的页面。有关这些字符串的语法的更多详细信息,请参阅匹配模式(Match Patterns)。 |
include_globs |
array of strings | 可选的。在匹配之后应用以仅包含那些也与此 glob 匹配的 URL。旨在模拟@include Greasemonkey 关键字。 |
exclude_globs |
array of string | 可选的。在匹配之后应用以排除与此 glob 匹配的 URL。旨在模拟@exclude Greasemonkey 关键字。 |
如果以下两个都为真,则内容脚本将被注入到页面中:
- 它的 URL 匹配任何匹配模式(
matches
)和任何include_globs
模式 - 该 URL 也不匹配
exclude_matches
或exclude_globs
模式。因为需要matches
属性,exclude_matches
、include_globs
和exclude_globs
只能用来限制哪些页面会受到影响。
以下扩展将内容脚本注入 https://www.nytimes.com/
health 但不会注入 https://www.nytimes.com/business
。
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"exclude_matches": ["*://*/*business*"],
"js": ["contentScript.js"]
}
],
...
}
chrome.scripting.registerContentScript({
id: 1,
matches: ["https://*.nytimes.com/*"],
exclude_matches: ["*://*/*business*"],
js: ["contentScript.js"]
});
Glob 属性遵循与匹配模式(match patterns)不同的、更灵活的语法。可接受的 glob 字符串是可能包含“通配符(wildcard)”星号和问号的 URL。星号 * 匹配任何长度的任何字符串,包括空字符串,而问号 ?匹配任何单个字符。
例如,glob https://???.example.com/foo/* 匹配以下任何一项:
但是,它与以下内容不匹配:
此扩展将内容脚本注入 https://www.nytimes.com/arts/index.html 和 https://www.nytimes.com/jobs/index.html,但不会注入 https://www.nytimes.com/sports/index.html:
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"include_globs": ["*nytimes.com/???s/*"],
"js": ["contentScript.js"]
}
],
...
}
chrome.scripting.registerContentScript({
id: 1,
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:
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"exclude_globs": ["*science*"],
"js": ["contentScript.js"]
}
],
...
}
chrome.scripting.registerContentScript({
id: 1,
matches: ['https://*.nytimes.com/*'],
exclude_globs: ['*science*'],
js: ['contentScript.js']
});
可以包括其中的一个、全部或一些以实现正确的范围。
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"exclude_matches": ["*://*/*business*"],
"include_globs": ["*nytimes.com/???s/*"],
"exclude_globs": ["*science*"],
"js": ["contentScript.js"]
}
],
...
}
chrome.scripting.registerContentScript({
matches: ['https://*.nytimes.com/*'],
exclude_matches: ['*://*/*business*'],
include_globs: ['*nytimes.com/???s/*'],
exclude_globs: ['*science*'],
js: ['contentScript.js']
});
# Run time(运行)
run_at
字段控制何时将 JavaScript 文件注入网页。首选和默认值是“document_idle
”,但如果需要,您也可以指定“document_start
”或“document_end
”。
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"run_at": "document_idle",
"js": ["contentScript.js"]
}
],
...
}
chrome.scripting.registerContentScript({
matches: ['https://*.nytimes.com/*'],
run_at: 'document_idle',
js: ['contentScript.js']
});
Name | Type | Description |
---|---|---|
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 完成后立即注入,但在图像和帧等子资源加载之前。 |
# Specify frames(指定框架)
“all_frames”字段允许扩展程序指定 JavaScript 和 CSS 文件是应该注入到与指定 URL 要求匹配的所有框架中,还是仅注入到选项卡中的最顶部框架中。
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"all_frames": true,
"js": ["contentScript.js"]
}
],
...
}
chrome.scripting.registerContentScript({
matches: ['https://*.nytimes.com/*'],
all_frames: true,
js: ['contentScript.js']
});
Name | Type | Description |
---|---|---|
all_frames |
boolean | 可选的。默认为 false,意味着仅匹配顶部框架。如果指定为 true,它将注入所有框架,即使该框架不是选项卡中的最顶层框架。每个框架都独立检查 URL 要求,如果不满足 URL 要求,它将不会注入子框架。 |
# Communication with the embedding page(与嵌入页面的通信)
尽管内容脚本的执行环境和承载它们的页面彼此隔离,但它们共享对页面 DOM 的访问。如果页面希望与内容脚本或通过内容脚本与扩展进行通信,则必须通过共享 DOM 进行。
一个例子可以使用 window.postMessage
来完成:
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);
document.getElementById("theButton").addEventListener("click", () => {
window.postMessage({ type: "FROM_PAGE", text: "Hello from the webpage!" }, "*");
}, false);
非扩展页面 example.html 向其自身发布消息。该消息被内容脚本拦截和检查,然后发布到扩展进程。这样,页面就建立了与扩展进程的通信线路。可以通过类似的方式进行相反的操作。
# Stay secure(保持安全)
虽然孤立的世界提供了一层保护,但使用内容脚本可能会在扩展程序和网页中产生漏洞。如果内容脚本从单独的网站接收内容,例如发出 XMLHttpRequest
,请在注入内容之前小心过滤内容跨站点脚本攻击。仅通过 HTTPS 进行通信以避免“中间人("man-in-the-middle")”攻击。
请务必过滤恶意网页。例如,以下模式是危险的,在 MV3 中是不允许的:
Don't
const data = document.getElementById("json-data")
// WARNING! Might be evaluating an evil script!
const parsed = eval("(" + data + ")")
Don't
const elmt_id = ...
// WARNING! elmt_id might be "); ... evil script ... //"!
window.setTimeout("animate(" + elmt_id + ")", 200);
相反,更喜欢不运行脚本的更安全的 API:
Do
const data = document.getElementById("json-data")
// JSON.parse does not evaluate the attacker's scripts.
const parsed = JSON.parse(data);
Do
const elmt_id = ...
// The closure form of setTimeout does not evaluate scripts.
window.setTimeout(() => animate(elmt_id), 200);
By.一粒技术服务