內容指令碼

內容指令碼是指在網頁情境中執行的檔案。使用標準的文件物件模型 (DOM),擴充功能就能讀取瀏覽器造訪的網頁詳細資料、變更這些網頁,並將資訊傳遞至上層擴充功能。

瞭解內容指令碼功能

內容指令碼可直接存取下列擴充功能 API:

內容指令碼無法直接存取其他 API。但他們可以透過交換訊息,間接存取擴充功能的其他部分。

您也可以使用 fetch() 等 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 布林值 選填。是否應將指令碼插入 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_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 字串 建議使用。盡可能使用 "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 使用,如果指定了 frameId 或 documentId,則無法使用:

service-worker.js

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

擴充功能可能會在與相符頁框相關的頁框中執行指令碼,但本身並未相符。常見的情況是,如果有含有網址的框架是由相符的框架建立,但這些網址本身不符合指令碼指定的模式,就會發生這種情況。

當擴充功能想要在含有 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: 網址的來源為空值)。

影格啟動程式是建立或導覽目標影格的影格。雖然這通常是直接父項或開啟者,但也可能不是 (例如在 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"攻擊。

請務必篩除惡意網頁。舉例來說,下列模式具有危險性,因此在資訊清單 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);