内容脚本

内容脚本是在网页的上下文中运行的 JavaScript 文件,它们可以通过标准的文档对象模型(DOM)来获取浏览器访问的网页详情,或者作出更改。

如下是内容脚本可以实现的一些功能的例子:

  • 在网页中找出未链接的URL,并将它们转换为超链接
  • 增加字体大小,使文本更具有可读性
  • 发现并处理 DOM 中的微格式数据

然而,内容脚本也有一些限制,它们不能

这些限制并不如看上去那么糟糕,内容脚本可以间接地通过与所属应用交换消息的方式,来使用 chrome.* API、访问应用数据并请求应用完成操作。内容脚本也可以像所属应用一样向拥有主机权限的站点发出跨域 XMLHttpRequest,另外也可以使用共享的 DOM 与网页通信。有关内容脚本能做什么与不能做什么的更多内部细节,请参见执行环境

清单文件

如果您的内容脚本代码每一次都需要插入到网页中,请在应用的清单文件中使用 content_scripts 字段注册它,如以下例子所示:

{
  "name": "我的应用",
  ...
  "content_scripts": [
    {
      "matches": ["http://www.google.com/*"],
      "css": ["mystyles.css"],
      "js": ["jquery.js", "myscript.js"]
    }
  ],
  ...
}

如果您只是有时需要插入代码,应该使用 permissions 字段,如以编程方式插入部分所述。

{
  "name": "我的应用",
  ...
  "permissions": [
    "tabs", "http://www.google.com/*"
  ],
  ...
}

使用 content_scripts 字段,应用可以向一个页面中插入多个内容脚本,每个内容脚本可以有多个 JavaScript 和 CSS 文件,content_scripts 数组中的每一个项目可以包含如下属性:

名称 类型 描述
matches array of strings 必选。 指定内容脚本要插入到哪些页面中去。有关这些字符串语法的更多细节请参见匹配表达式,有关如何排除 URL 的信息请参见匹配表达式和范围
exclude_matches array of strings 可选。 排除不需要插入内容脚本的页面。有关这些字符串语法的更多细节请参见匹配表达式,有关如何排除 URL 的信息请参见匹配表达式和范围
match_about_blank boolean 可选。 是否在 about:blankabout:srcdoc 中插入内容脚本,只有当继承 URL 匹配 matches 字段中声明的匹配表达式时才会插入内容脚本。继承 URL 为创建框架或窗口的文档 URL。
内容脚本不能在经过沙盒屏蔽的框架中插入。

默认为 false
css array of strings 可选。 要插入匹配页面的 CSS 文件列表,它们将在页面的所有 DOM 构造或显示之前,按照数组中指定的顺序插入。
js array of strings 可选。 要插入匹配页面的 JavaScript 文件列表,它们将按照数组中指定的顺序插入。
run_at string 可选。 控制 js 中的 JavaScript 文件何时插入,可以为 "document_start"、"document_end" 或 "document_idle",默认为 "document_idle"。

如果是 "document_start",这些文件将在 css 中指定的文件之后,但是在所有其他 DOM 构造或脚本运行之前插入。

如果是 "document_end",文件将在 DOM 完成之后立即插入,但是在加载子资源(如图像与框架)之前插入。

如果是 "document_idle",浏览器将在 "document_end" 和刚发生 window.onload 事件这两个时刻之间选择合适的时候插入,具体的插入时间取决于文档的复杂程度以及加载文档所花的时间,并且浏览器会尽可能地为加快页面加载速度而优化。

注意:如果使用 "document_idle",内容脚本不一定会收到 window.onload 事件,因为它们可能在这一事件已经发生后再执行。在大多数情况下,在 "document_idle" 时运行的内容脚本没有必要监听 onload 事件,因为它们保证在 DOM 完成后运行。如果您的脚本确实需要在 window.onload 之后运行,您可以检查 document.readyState 属性确定 onload 事件是否已经发生。
all_frames boolean 可选。 控制内容脚本运行在匹配页面的所有框架中还是仅在顶层框架中。

默认为 false,意味着仅在顶层框架中运行。
include_globs array of string 可选。 在应用 matches 之后仅包含同时匹配这一范围的 URL。该属性是为了模拟 Greasemonkey 中的 @include 关键字,有关更多详情请参见下面的匹配表达式和范围
exclude_globs array of string 可选。 在应用 matches 之后排除匹配这一范围的 URL。该属性是为了模拟 Greasemonkey 中的 @exclude 关键字,有关更多详情请参见下面的匹配表达式和范围

匹配表达式和范围

如果页面的 URL 匹配任何 matches 指定的表达式以及任何 include_globs 指定的表达式,并且不匹配 exclude_matchesexclude_globs 指定的表达式,则会在页面中插入内容脚本。因为 matches 属性是必需的,exclude_matchesinclude_globsexclude_globs 只能用来限制受到影响的页面。

例如,假设 matches["http://*.nytimes.com/*"]

  • 如果 exclude_matches["*://*/*business*"],那么内容脚本会插入“http://www.nytimes.com/health”,但不会插入“http://www.nytimes.com/business”。
  • 如果 include_globs["*nytimes.com/???s/*"],那么内容脚本会插入“http://www.nytimes.com/arts/index.html”和“http://www.nytimes.com/jobs/index.html”,但不会插入“http://www.nytimes.com/sports/index.html”。
  • 如果 exclude_globs["*science*"],那么内容脚本会插入“http://www.nytimes.com”,但不会插入“http://science.nytimes.com”或“http://www.nytimes.com/science”。

范围(glob)属性与匹配表达式相比遵循不同并且更灵活的语法。所有含有“通配符”星号和问号的 URL 都是可接受的范围字符串,星号(*)匹配任意长度的字符串(包括空字符串),问号(?)匹配任意单个字符。

例如,范围 "http://???.example.com/foo/*" 匹配以下任一 URL:

  • "http://www.example.com/foo/bar"
  • "http://the.example.com/foo/"

但是不匹配如下 URL:

  • "http://my.example.com/foo/bar"
  • "http://example.com/foo/"
  • "http://www.example.com/foo"

以编程方式插入

如果您的 JavaScript 或 CSS 代码不需要插入匹配的每一个页面时,例如,如果您希望当用户单击浏览器按钮的图标时才运行脚本,以编程方式插入代码就十分有用。

要向页面中插入代码,您的应用必须拥有该页面的主机权限,并且还需要用到 chrome.tabs 模块。您可以使用清单文件中的 permissions 字段获得这些权限。

一旦您拥有了相应的权限,您可以通过调用 tabs.executeScript 向页面插入 JavaScript 代码。要插入 CSS 代码,请使用 tabs.insertCSS

下列代码 当用户单击浏览器按钮时向当前标签页插入并执行 JavaScript 代码。

chrome.browserAction.onClicked.addListener(function(tab) {
  chrome.tabs.executeScript({
    code: 'document.body.style.backgroundColor="red"'
  });
});
"permissions": [
  "activeTab"
],

当浏览器正在显示一个 HTTP 页面,并且用户单击该应用的浏览器按钮时,应用将页面的 bgcolor 属性设为'red'。结果,除非页面通过 CSS 设置了背景颜色,页面将变成红色。

通常,您不会直接插入代码(如前面的例子所示),而是将代码放在一个文件中。您可以像这样插入文件的内容:

chrome.tabs.executeScript(null, {file: "content_script.js"});

执行环境

内容脚本在一个称为隔离环境的特殊环境中执行。它们可以访问所在页面的 DOM,但是不能访问当前页面创建的任何 JavaScript 变量或函数。对于每个内容脚本来说,就像没有其他 JavaScript 在当前页面上执行。反过来也是如此:在当前页面运行的 JavaScript 不能调用或访问任何内容脚本定义的函数或变量。

例如,考虑如下简单页面:

<html>
  <button id="mybutton">单击我</button>
  <script>
    var greeting = "您好,";
    var button = document.getElementById("mybutton");
    button.person_name = "Bob";
    button.addEventListener("click", function() {
      alert(greeting + button.person_name + "。");
    }, false);
  </script>
</html>

假设如下内容脚本插入到了 hello.html 中:

var greeting = "您好啊,";
var button = document.getElementById("mybutton");
button.person_name = "Roberto";
button.addEventListener("click", function() {
  alert(greeting + button.person_name + "。");
}, false);

现在,如果按钮按下,您将会看到两条问候。

隔离环境允许每一个内容脚本更改自己的 JavaScript 环境,而不用担心是否会与页面或其他内容脚本发生冲突。例如,一个内容脚本可以包含 JQuery v1 而页面可以包含 JQuery v2,并且它们互不影响。

隔离环境的另一个重要好处是将页面上的 JavaScript 和应用中的 JavaScript 完全区分开来,这样我们就可以为内容脚本提供额外功能,而这些额外功能不应该从网页中访问,我们也不用担心访问它们的网页。

网页与应用之间共享的 JavaScript 对象值得注意,例如 window.onload 事件。每一个隔离环境拥有该对象自己的副本,对该对象赋值只影响这一独立的副本。例如,网页和应用都可以给 window.onload 赋值,但是都不能读取另外一方的事件处理器。事件处理器将按照它们赋值的顺序调用。

与嵌入的页面通信

尽管内容脚本的执行环境和所在页面是互相隔离的,但是它们都可以访问页面的 DOM。如果页面想要和内容脚本通信(或者通过内容脚本与应用通信),必须通过共享的 DOM 进行。

可以使用 window.postMessage(或者 window.webkitPostMessage 用于可传输(Transferable)的对象):

var port = chrome.runtime.connect();

window.addEventListener("message", function(event) {
  // 我们只接受来自我们自己的消息
  if (event.source != window)
    return;

  if (event.data.type && (event.data.type == "FROM_PAGE")) {
    console.log("内容脚本接收到:" + event.data.text);
    port.postMessage(event.data.text);
  }
}, false);
document.getElementById("theButton").addEventListener("click",
    function() {
  window.postMessage({ type: "FROM_PAGE", text: "来自网页的问候!" }, "*");
}, false);

在上面的例子中,example.html(不是应用的一部分)向自己发送消息,由内容脚本截获并检查,然后发送至应用进程。通过这种方式,页面建立了与应用之间的通信,通过类似的方式反过来也是可能的。

安全性考虑

当您编写内容脚本时,您应该注意两个安全问题。首先,注意不要向您插入内容脚本的网站引入安全隐患。例如,如果您的内容脚本从另一个网站接收内容(例如通过发出 XMLHttpRequest),一定要注意把内容插入当前页面前过滤可能的跨站脚本攻击。例如,首选 innerText 而不是 innerHTML 插入内容。当您在 HTTPS 页面上获取来自 HTTP 的内容时要特别小心,因为如果用户在不安全的网络环境中,HTTP 内容可能会因为网络中的中间人攻击而遭到破坏。

第二,尽管在隔离环境中运行您的内容脚本提供了某些保护,但是如果您不加区分地使用来自网页的内容,恶意网页仍然可能攻击您的内容脚本。例如,以下形式是危险的:

var data = document.getElementById("json-data")
// 警告!可能会执行恶意脚本!
var parsed = eval("(" + data + ")")
var elmt_id = ...
// 警告!elmt_id可能为"); …恶意脚本… //"!
window.setTimeout("animate(" + elmt_id + ")", 200);

因此,改用更安全的不运行脚本的 API:

var data = document.getElementById("json-data")
// JSON.parse 不会运行攻击者的脚本。
var parsed = JSON.parse(data);
var elmt_id = ...
// 闭包形式的 setTimeout 不会运行脚本。
window.setTimeout(function() {
  animate(elmt_id);
}, 200);

引用应用的文件

使用 extension.getURL 获得应用的文件 URL,您可以像任何其他 URL 一样使用获得的结果,如以下代码所示。

//用来显示 <应用目录>/images/myimage.png 的代码:
var imgURL = chrome.extension.getURL("images/myimage.png");
document.getElementById("someImage").src = imgURL;