一、描述

content scripts 实际上是可以在网页的上下文中使用的 scripts,通过使用 DOM,能够读取浏览器访问的当前网页的详细信息,并且能够将信息传递到他们之上的扩展程序中。

二、content scripts 的功能

content scripts 脚本可以通过和扩展程序消息传递来访问它们的 parent extension(还是英文表述清楚一些)使用的 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

  • i18n
  • storage
  • runtime

    • connect
    • getManifest
    • getURL
    • id
    • onConnect
    • onMessage
    • sendMessage

除了上面的 API 之外,内容脚本无法直接访问其他的 API

三、独立的运行环境

content scripts 有一个独立的运行环境,内容脚本可以在其本身的 js 环境作变动,不会与页面和其他的内容脚本冲突。

一个扩展程序可以在 web 网页中运行,其代码类似于下面示例:

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

当然,也可以单独抽离出 content script,然后注入进去:

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

环境隔离原则,不允许内容脚本、扩展程序和忘了访问其他人创建的任何变量或者功能,这也使内容脚本能够启用网页无法访问的功能。

四、注入脚本

内容脚本可以以编程方式或声明性方式注入。

1、通过编程方式注入

对需要在特定场合运行的内容脚本,可以使用编程注入。

如果要注入编程内容脚本,需要在 manifest 中提供 activeTab 的权限。这将授予对 active 的站点主机的安全访问权限以及对选项卡的临时访问权限,从而使内容脚本能够在当前 active 选项卡上运行,无需指定跨域权限。

{
    "name": "My extension",
    ...
    "permissions": [
      "activeTab"
    ],
    ...
  }

content script 能够通过代码的形式直接被注入

 chrome.runtime.onMessage.addListener(
    function(message, callback) {
      if (message == "changeColor"){
        chrome.tabs.executeScript({
          code: 'document.body.style.backgroundColor="orange"'
        });
      }
   });

当然你也可以注入整个 js 文件:

 chrome.runtime.onMessage.addListener(
    function(message, callback) {
      if (message == "runContentScript"){
        chrome.tabs.executeScript({
          file: 'contentScript.js'
        });
      }
   });

2、声明式的注入

声明式注入可以指定在某个页面上运行内容脚本。

声明式注入的方式是在 manifest 文件中的 content_scripts 字段下面注册:

{
 "name": "My extension",
 ...
 "content_scripts": [
   {
     "matches": ["http://*.nytimes.com/*"],
     "css": ["myStyles.css"],
     "js": ["contentScript.js"]
   }
 ],
 ...
}

声明可以是一个列表,列表的每个元素一般会使用下面几个字段: matchescssjsmatch_about_blank

其中 matches 可以匹配在符合条件的页面中注入 content scripts,更多的匹配规则可以在https://developer.chrome.com/extensions/match_patterns 找到。 matches 这个字段是必须要传入的

css 是可选字段,注入一个 css 样式列表,在页面渲染或者DOM显示之前,会依次按照顺序注入。

js 是可选字段,注入 js 列表,是按照数组顺序依次注入。

match_about_blank,bool 类型,是否需要对 about:blank 页面进行注入,默认是 false

3、排除条件匹配或全局匹配

上面说了 content_scripts 的每个匹配项都有一个 matches 来符合这个匹配条件,但是除此之外,我们可能需要更加复杂的条件,比如出去某个符合条件的字段,这样的相关的字段包括:exclude_matchesinclude_globsexclude_globs

只要 URL 不匹配 exclude_matchesexclude_globs 模式,这个条件下如果 URL 匹配任何 matches 模式和任何 include_globs 模式,则内容脚本将注入页面。

由于 matches 属性是必需的,因此 exclude_matchesinclude_globsexclude_globs 只能用于限制受影响的页面。

举例说明 exclude_matches:

比如一个扩展程序将内容脚本注入 http://www.nytimes.com/health ,但不会注入 http://www.nytimes.com/business 的匹配模式可以这么写:

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

而 glob 属性则是更加灵活的语法,和 matches 是不同的,可以接受的 glob 字符串是可能包含 通配符 星号(*)和问号(?)的 URL,星号(*)匹配任何长度的任何字符串,包括空字符,而问号(?)匹配任何的单个字符。

举例说明,glob http://???.example.com/foo* 能够匹配下面两个链接:

但是他不能匹配下面3个地址:

因此在下面的 manifest 配置中 能够将内容脚本注入到 http:/www.nytimes.com/ arts /index.htmlhttp://www.nytimes.com/ jobs /index.html,但是不会注入到 http://www.nytimes.com/ sports /index.html

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

4、runtime

将 JavaScript 文件注入网页时,由 run_at 字段控制,而默认的值是 document_idle,不过如果需要的话也可以指定为 document_startdocument_end

{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["http://*.nytimes.com/*"],
      "run_at": "document_idle",
      "js": ["contentScript.js"]
    }
  ],
  ...
}
  • document_idle: 首选值,能够在 document_endwindow.onload 结束之后立即注入内容脚本
  • document_start:在 css 之后,在任何的 DOM 渲染或者 js 脚本之前注入
  • document_end: document 完成之后,但是在图像和帧动画之前注入

5、指定 frame

content_scriptsall_frames 配置成 true 可以将 content scripts 注入到所有的 frame 中。

默认值是 false,只能注册到最顶部的 frame 中

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

五、嵌入页面的通信

虽然内容脚本的执行环境和托管它们的页面彼此隔离,但它们共享对页面 DOM 的访问。

如果页面希望与内容脚本通信,或者通过内容脚本与扩展通信,则必须通过共享 DOM 进行通信。

可以使用 window.postMessage 完整一个 example:

contentscript.js

var port = chrome.runtime.connect();

window.addEventListener("message", function(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.html

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

非扩展程序页面页面 example.html将消息发给自己。内容脚本拦截并检查此消息,然后将其发布到扩展程序的进程。

通过这种方式,页面建立了与扩展程序的通信线路,这个方式也可以反过来。

六、安全性保证

虽然隔离环境提供了一层保护,但使用内容脚本可能会在扩展程序和网页中产生漏洞。如果内容脚本从单独的网站接收内容(例如创建 XMLHttpRequest ),请在注入之前小心过滤 XSS 攻击。仅通过 HTTPS 进行通信,以避免“中间人”攻击。

务必过滤恶意网页。例如,以下模式是危险的:

var data = document.getElementById("json-data")
// WARNING! Might be evaluating an evil script!
var parsed = eval("(" + data + ")")
var elmt_id = ...
// WARNING! elmt_id might be "); ... evil script ... //"!
window.setTimeout("animate(" + elmt_id + ")", 200);

应该使用不执行脚本的更安全的 API:

var data = document.getElementById("json-data")
// JSON.parse does not evaluate the attacker's scripts.
var parsed = JSON.parse(data);
var elmt_id = ...
// The closure form of setTimeout does not evaluate scripts.
window.setTimeout(function() {
  animate(elmt_id);
}, 200);