內容指令碼

內容指令碼是擴充套件程式的一部分,它在網頁的上下文中執行。它可以使用標準Web API讀取和修改頁面內容。內容指令碼的行為類似於作為網站一部分的指令碼,例如使用<script>元素載入的指令碼。但是,內容指令碼只有在授予網頁源的主機許可權後才能訪問頁面內容。

內容指令碼可以訪問WebExtension API 的一個小子集,但它們可以透過訊息系統與後臺指令碼通訊,從而間接訪問 WebExtension API。後臺指令碼可以訪問所有WebExtension JavaScript API,但不能直接訪問網頁內容。

注意:某些 Web API 僅限於安全上下文,這也適用於在這些上下文中執行的內容指令碼。除了PointerEvent.getCoalescedEvents(),它可以在 Firefox 中從不安全上下文中的內容指令碼呼叫。

載入內容指令碼

您可以將內容指令碼載入到網頁中

  1. 在安裝時,載入到與 URL 模式匹配的頁面中。
  2. 在執行時,載入到與 URL 模式匹配的頁面中。
  3. 在執行時,載入到特定的標籤頁中。

每個幀,每個擴充套件程式只有一個全域性作用域。這意味著內容指令碼中的變數可以被任何其他內容指令碼訪問,無論內容指令碼是如何載入的。

使用方法 (1) 和 (2),您只能將指令碼載入到可以使用匹配模式表示其 URL 的頁面中。

使用方法 (3),您還可以將指令碼載入到與擴充套件程式一起打包的頁面中,但不能將指令碼載入到特權瀏覽器頁面(如 about:debuggingabout:addons)中。

注意:動態 JS 模組匯入現在在內容指令碼中有效。有關更多詳細資訊,請參閱 Firefox bug 1536094。只允許使用 moz-extension 方案的 URL,這不包括資料 URL(Firefox bug 1587336)。

永續性

使用 scripting.executeScript() 或(僅限 Manifest V2)tabs.executeScript() 載入的內容指令碼按請求執行且不持久。

在清單檔案的 content_scripts 鍵中定義或使用 scripting.registerContentScripts() 或(僅限 Firefox 中的 Manifest V2)contentScripts API 定義的內容指令碼預設是持久的。它們在瀏覽器重啟和更新以及擴充套件程式重啟後仍然保持註冊。

但是,scripting.registerContentScripts() API 提供了將指令碼定義為非永續性的功能。例如,當您的擴充套件程式(代表使用者)只想在當前瀏覽器會話中啟用內容指令碼時,這可能很有用。

許可權、限制和侷限性

Permissions

只有當擴充套件程式被授予該域的主機許可權時,註冊的內容指令碼才會執行。

要以程式設計方式注入指令碼,擴充套件程式需要 activeTab 許可權主機許可權。使用 scripting API 的方法需要 scripting 許可權。

從 Manifest V3 開始,主機許可權不會在安裝時自動授予。使用者可以在安裝擴充套件程式後選擇啟用或停用主機許可權。

受限域

主機許可權activeTab 許可權都對某些域有例外。內容指令碼被阻止在這些域上執行,例如,為了保護使用者免受擴充套件程式透過特殊頁面升級許可權的攻擊。

在 Firefox 中,這包括以下域

  • accounts-static.cdn.mozilla.net
  • accounts.firefox.com
  • addons.cdn.mozilla.net
  • addons.mozilla.org
  • api.accounts.firefox.com
  • content.cdn.mozilla.net
  • discovery.addons.mozilla.org
  • install.mozilla.org
  • oauth.accounts.firefox.com
  • profile.accounts.firefox.com
  • support.mozilla.org
  • sync.services.mozilla.com

其他瀏覽器對可以安裝擴充套件程式的網站有類似的限制。例如,Chrome 中限制了對 chrome.google.com 的訪問。

注意:由於這些限制包括 addons.mozilla.org,因此在安裝後立即嘗試使用擴充套件程式的使用者可能會發現它不起作用。為了避免這種情況,您應該新增適當的警告或一個入門頁面,以引導使用者離開 addons.mozilla.org

可以透過企業策略進一步限制域集:Firefox 識別 restricted_domains 策略,如 mozilla/policy-templates 中的 ExtensionSettings 所述。Chrome 的 runtime_blocked_hosts 策略在 配置 ExtensionSettings 策略 中有說明。

侷限性

可以使用 data: URIBlob 物件和其他類似技術載入整個標籤頁或幀。內容指令碼注入此類特殊文件的支援因瀏覽器而異,有關詳細資訊,請參閱 Firefox bug #1411641 comment 41

內容指令碼環境

DOM 訪問

內容指令碼可以訪問和修改頁面的 DOM,就像普通的頁面指令碼一樣。它們還可以看到頁面指令碼對 DOM 所做的任何更改。

但是,內容指令碼會獲得 DOM 的“乾淨”檢視。這意味著

  • 內容指令碼無法看到頁面指令碼定義的 JavaScript 變數。
  • 如果頁面指令碼重新定義了內建 DOM 屬性,內容指令碼會看到屬性的原始版本,而不是重新定義的版本。

Chrome 不相容性中的“內容指令碼環境”所述,行為因瀏覽器而異

  • 在 Firefox 中,此行為稱為Xray 視覺。內容指令碼可能會遇到來自其自身全域性作用域的 JavaScript 物件或來自網頁的 Xray 包裝版本。

  • 在 Chrome 中,此行為透過隔離世界強制執行,它使用一種根本不同的方法。

考慮一個像這樣的網頁

html
<!doctype html>
<html lang="en-US">
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
  </head>

  <body>
    <script src="page-scripts/page-script.js"></script>
  </body>
</html>

指令碼 page-script.js 執行以下操作

js
// page-script.js

// add a new element to the DOM
let p = document.createElement("p");
p.textContent = "This paragraph was added by a page script.";
p.setAttribute("id", "page-script-para");
document.body.appendChild(p);

// define a new property on the window
window.foo = "This global variable was added by a page script";

// redefine the built-in window.confirm() function
window.confirm = () => {
  alert("The page script has also redefined 'confirm'");
};

現在擴充套件程式將內容指令碼注入到頁面中

js
// content-script.js

// can access and modify the DOM
let pageScriptPara = document.getElementById("page-script-para");
pageScriptPara.style.backgroundColor = "blue";

// can't see properties added by page-script.js
console.log(window.foo); // undefined

// sees the original form of redefined properties
window.confirm("Are you sure?"); // calls the original window.confirm()

反之亦然;頁面指令碼無法看到內容指令碼新增的 JavaScript 屬性。

這意味著內容指令碼可以依賴 DOM 屬性的可預測行為,而不必擔心其變數與頁面指令碼中的變數衝突。

這種行為的一個實際後果是,內容指令碼無法訪問頁面載入的任何 JavaScript 庫。因此,例如,如果頁面包含 jQuery,內容指令碼就無法看到它。

如果內容指令碼需要使用 JavaScript 庫,那麼該庫本身應該作為內容指令碼想要使用它的內容指令碼一起注入

json
"content_scripts": [
  {
    "matches": ["*://*.mozilla.org/*"],
    "js": ["jquery.js", "content-script.js"]
  }
]

注意:Firefox 提供 cloneInto()exportFunction(),以使內容指令碼能夠訪問頁面指令碼建立的 JavaScript 物件,並將它們的 JavaScript 物件公開給頁面指令碼。

有關更多詳細資訊,請參閱與頁面指令碼共享物件

WebExtension API

除了標準 DOM API 之外,內容指令碼還可以使用以下 WebExtension API

來自 extension

來自 runtime

來自 i18n

來自 menus

所有來自

XHR 和 Fetch

內容指令碼可以使用正常的 window.XMLHttpRequestwindow.fetch() API 發出請求。

注意:在 Firefox 中,對於 Manifest V2,內容指令碼請求(例如,使用 fetch())發生在擴充套件程式的上下文中,因此您必須提供絕對 URL 來引用頁面內容。

在 Chrome 和 Firefox 中,對於 Manifest V3,這些請求發生在頁面上下文中,因此它們是針對相對 URL 發出的。例如,/api 被髮送到 https://«當前頁面 URL»/api

內容指令碼獲得與擴充套件程式其餘部分相同的跨域特權:因此,如果擴充套件程式使用 manifest.json 中的 permissions 鍵請求了某個域的跨域訪問,則其內容指令碼也可以訪問該域。

注意:使用 Manifest V3 時,當目標伺服器使用 CORS 選擇加入時,內容指令碼可以執行跨域請求;但是,主機許可權在內容指令碼中不起作用,但在常規擴充套件程式頁面中仍然有效。

這是透過在內容指令碼中公開更多特權的 XHR 和 fetch 例項來實現的,其副作用是不設定 OriginReferer 標頭,就像來自頁面本身的請求那樣;這通常是優選的,以防止請求暴露其跨域性質。

注意:在 Firefox 中,對於 Manifest V2,需要執行行為如同內容本身傳送的請求的擴充套件程式可以使用 content.XMLHttpRequestcontent.fetch()

對於跨瀏覽器擴充套件程式,必須對這些方法的存在進行功能檢測。

這在 Manifest V3 中是不可能的,因為 content.XMLHttpRequestcontent.fetch() 不可用。

注意:在 Chrome 中,從版本 73 開始,以及 Firefox 中,從版本 101 開始,當使用 Manifest V3 時,內容指令碼受制於與它們執行的頁面相同的 CORS 策略。只有後臺指令碼擁有更高的跨域許可權。請參閱 Chrome 擴充套件程式內容指令碼中跨域請求的更改

與後臺指令碼通訊

儘管內容指令碼無法直接使用大多數 WebExtension API,但它們可以使用訊息 API 與擴充套件程式的後臺指令碼通訊,從而可以間接訪問後臺指令碼可以訪問的所有相同 API。

後臺指令碼和內容指令碼之間通訊有兩種基本模式

  • 您可以傳送一次性訊息(帶可選響應)。
  • 您可以在兩端之間建立持久連線,並使用該連線交換訊息。

一次性訊息

要傳送一次性訊息(帶可選響應),您可以使用以下 API

在內容指令碼中 在後臺指令碼中
傳送訊息 browser.runtime.sendMessage() browser.tabs.sendMessage()
接收訊息 browser.runtime.onMessage browser.runtime.onMessage

例如,這是一個內容指令碼,它監聽網頁中的點選事件。

如果點選的是連結,它會向後臺頁面傳送帶有目標 URL 的訊息

js
// content-script.js

window.addEventListener("click", notifyExtension);

function notifyExtension(e) {
  if (e.target.tagName !== "A") {
    return;
  }
  browser.runtime.sendMessage({ url: e.target.href });
}

後臺指令碼監聽這些訊息,並使用 notifications API 顯示通知

js
// background-script.js

browser.runtime.onMessage.addListener(notify);

function notify(message) {
  browser.notifications.create({
    type: "basic",
    iconUrl: browser.extension.getURL("link.png"),
    title: "You clicked a link!",
    message: message.url,
  });
}

(此示例程式碼略微改編自 GitHub 上的 notify-link-clicks-i18n 示例。)

基於連線的訊息傳遞

如果您在後臺指令碼和內容指令碼之間交換大量訊息,傳送一次性訊息可能會變得很麻煩。因此,另一種模式是在兩個上下文之間建立持久連線,並使用此連線交換訊息。

雙方都有一個 runtime.Port 物件,它們可以使用它來交換訊息。

要建立連線

這會返回一個 runtime.Port 物件。

一旦每一側都有一個埠,雙方就可以

  • 使用 runtime.Port.postMessage() 傳送訊息
  • 使用 runtime.Port.onMessage() 接收訊息

例如,以下內容指令碼一載入就執行以下操作

  • 連線到後臺指令碼
  • Port 儲存在變數 myPort
  • 監聽 myPort 上的訊息(並記錄它們)
  • 當用戶點選文件時,使用 myPort 向後臺指令碼傳送訊息
js
// content-script.js

let myPort = browser.runtime.connect({ name: "port-from-cs" });
myPort.postMessage({ greeting: "hello from content script" });

myPort.onMessage.addListener((m) => {
  console.log("In content script, received message from background script: ");
  console.log(m.greeting);
});

document.body.addEventListener("click", () => {
  myPort.postMessage({ greeting: "they clicked the page!" });
});

相應的後臺指令碼

  • 監聽內容指令碼的連線嘗試

  • 收到連線嘗試時

    • 將埠儲存在名為 portFromCS 的變數中
    • 使用埠向內容指令碼傳送訊息
    • 開始監聽埠上收到的訊息,並記錄它們
  • 當用戶點選擴充套件程式的瀏覽器操作時,使用 portFromCS 向內容指令碼傳送訊息

js
// background-script.js

let portFromCS;

function connected(p) {
  portFromCS = p;
  portFromCS.postMessage({ greeting: "hi there content script!" });
  portFromCS.onMessage.addListener((m) => {
    portFromCS.postMessage({
      greeting: `In background script, received message from content script: ${m.greeting}`,
    });
  });
}

browser.runtime.onConnect.addListener(connected);

browser.browserAction.onClicked.addListener(() => {
  portFromCS.postMessage({ greeting: "they clicked the button!" });
});

多個內容指令碼

如果您有多個內容指令碼同時通訊,您可能希望將它們的連線儲存在一個數組中。

js
// background-script.js

let ports = [];

function connected(p) {
  ports[p.sender.tab.id] = p;
  // …
}

browser.runtime.onConnect.addListener(connected);

browser.browserAction.onClicked.addListener(() => {
  ports.forEach((p) => {
    p.postMessage({ greeting: "they clicked the button!" });
  });
});

一次性訊息和基於連線的訊息傳遞之間的選擇

一次性訊息和基於連線的訊息傳遞之間的選擇取決於您的擴充套件程式期望如何使用訊息傳遞。

推薦的最佳實踐是

  • 在以下情況下使用一次性訊息:
    • 一條訊息只期望一個響應。
    • 少量指令碼監聽接收訊息(runtime.onMessage 呼叫)。
  • 在以下情況下使用基於連線的訊息傳遞:
    • 指令碼在會話中交換多條訊息。
    • 擴充套件程式需要知道任務進度或任務是否中斷,或者想要中斷使用訊息傳遞啟動的任務。

與網頁通訊

預設情況下,內容指令碼無法訪問頁面指令碼建立的物件。但是,它們可以使用 DOM window.postMessagewindow.addEventListener API 與頁面指令碼通訊。

例如

js
// page-script.js

let messenger = document.getElementById("from-page-script");

messenger.addEventListener("click", messageContentScript);

function messageContentScript() {
  window.postMessage(
    {
      direction: "from-page-script",
      message: "Message from the page",
    },
    "*",
  );
}
js
// content-script.js

window.addEventListener("message", (event) => {
  if (
    event.source === window &&
    event?.data?.direction === "from-page-script"
  ) {
    alert(`Content script received message: "${event.data.message}"`);
  }
});

有關此功能的完整工作示例,請訪問 GitHub 上的演示頁面並按照說明操作。

警告:以這種方式與不受信任的網頁內容互動時要非常小心!擴充套件程式是特權程式碼,可以擁有強大的功能,而惡意的網頁很容易欺騙它們訪問這些功能。

舉一個簡單的例子,假設接收訊息的內容指令碼程式碼執行以下操作

js
// content-script.js

window.addEventListener("message", (event) => {
  if (
    event.source === window &&
    event?.data?.direction === "from-page-script"
  ) {
    eval(event.data.message);
  }
});

現在頁面指令碼可以使用內容指令碼的所有特權執行任何程式碼。

在內容指令碼中使用 eval()

注意:eval() 在 Manifest V3 中不可用。

在 Chrome 中

eval 始終在內容指令碼的上下文中執行程式碼,而不是在頁面的上下文中執行。

在 Firefox 中

如果您呼叫 eval(),它會在內容指令碼的上下文中執行程式碼。

如果您呼叫 window.eval(),它會在頁面的上下文中執行程式碼。

例如,考慮一個像這樣的內容指令碼

js
// content-script.js

window.eval("window.x = 1;");
eval("window.y = 2");

console.log(`In content script, window.x: ${window.x}`);
console.log(`In content script, window.y: ${window.y}`);

window.postMessage(
  {
    message: "check",
  },
  "*",
);

此程式碼僅使用 window.eval()eval() 建立一些變數 xy,記錄它們的值,然後向頁面傳送訊息。

收到訊息後,頁面指令碼記錄相同的變數

js
window.addEventListener("message", (event) => {
  if (event.source === window && event.data && event.data.message === "check") {
    console.log(`In page script, window.x: ${window.x}`);
    console.log(`In page script, window.y: ${window.y}`);
  }
});

在 Chrome 中,這會產生如下輸出

In content script, window.x: 1
In content script, window.y: 2
In page script, window.x: undefined
In page script, window.y: undefined

在 Firefox 中,這會產生如下輸出

In content script, window.x: undefined
In content script, window.y: 2
In page script, window.x: 1
In page script, window.y: undefined

同樣適用於 setTimeout()setInterval()Function()

警告:在頁面上下文中執行程式碼時要非常小心!

頁面環境由可能惡意的網頁控制,這些網頁可以重新定義您與之互動的物件,使其行為出乎意料

js
// page.js redefines console.log

let original = console.log;

console.log = () => {
  original(true);
};
js
// content-script.js calls the redefined version

window.eval("console.log(false)");