Chrome Extensions v3 遷移清單

2023-10-10 12:01:58

一、前置問題

1.1為什麼需要遷移 v3?

Chrome 計劃完全停止 v2 版本維護,後續 v2 版本將無法上架谷歌外掛商店,除此之外,未來新版本 Chrome 對於 v2 版本外掛的限制會越來越大,比如安全性限制 iframe 巢狀只能通過沙盒模式資料通訊傳遞而不能直接獲取資料等等,因此 v2 遷移 v3 是必要的。

1.2 v3 版本帶來了什麼新特性?

  • 更強的隱私性,這一點在許可權設定上會有所體現,許可權劃分更細膩。
  • 更強的安全性,比如廢棄 eval 方法,禁止載入執行遠端倉庫程式碼
  • 更佳的效能,比如 background 被替換為 service workers,它不會持久執行
  • 整合和調整了部分 API ,更符合未來的發展等等。

1.3 v2 遷移 V3 我需要做什麼?

主要是四個方向的改動:

  • manifest 設定更新
  • background 遷移 Service Worker
  • API 變更
  • 安全調整

以下是具體需要做的事情。

二、manifest 更新清單

2.1 manifest.json版本號

manifest 版本號需要改為 3。

// v2
{
  ...
  "manifest_version": 2
  ...
}
// v3
{
  ...
  "manifest_version": 3
  ...
}

2.2 廢棄 persistent

persistent 用於決定 Chrome extensions 是否開啟常駐後臺,由於 v3 版本 background 遷移 service worker 後後臺已經做不到常駐,此屬性只能作廢,刪掉就好。

2.3 更新主機許可權

在Manifest V2 中,有兩種方法為你的api或任何主機獲得許可權,要麼在 permissions 陣列或 optional_permissions 陣列。

而 v3 的許可權粒度劃分會更細膩,不會像之前許可權一把梭,所以像主機存取許可權設定需要單獨新增到 host_permissions 中:

// v2
{
  "permissions": [
    "tabs",
    "bookmarks",
    "https://www.blogger.com/", // 之前主機許可權都在 permissions
  ],
  "optional_permissions": [
    "unlimitedStorage",
    "*://*/*",
  ]
}
// v3
{
  "permissions": [
    "tabs",
    "bookmarks"
  ],
  "optional_permissions": [ // 單獨設定主機許可權
    "unlimitedStorage"
  ],
  "host_permissions": [
    "https://www.blogger.com/",
  ],
  "optional_host_permissions": [// 單獨設定主機許可權
    "*://*/*",
  ]
}

2.4 Background 遷移 Service Worker

V3 使用 Service Worker 取代了 Background,這裡包含兩個層面的意思,第一是設定欄位變了(見下方程式碼),第二是 Service Worker 不再像之前的 Background 能做到一直在後臺執行,這點我們後面細說。

// v2
{
    ...,
    "background": {
        "scripts": ["background.js"],
        "persistent": false
      },
    ...
}
// v3
{
    ...,
    "background": {
        "service_worker": "background.js" // 欄位變了
      	// 刪了 persistent
      },
    ...
}

2.5 更新 web_accessible_resources 欄位

web_accessible_resources 用於指定哪些資原始檔可以被 web 頁面存取和載入,但在 v2 時也是一把梭,基本一次設定哪哪的網頁都能存取,同樣在 v3 此欄位改為資源與匹配的物件形式,看程式碼就懂了:

// v2
{
  ...
  "web_accessible_resources": [
    "images/*",
    "style/extension.css",
    "script/extension.js"
  ],
  ...
}
// v3
{
  ...
    "web_accessible_resources": [
    {
      "resources": [
        "images/*"
      ],
      "matches": [
        "*://*/*"
      ]
    },
    {
      "resources": [ // 指定資源
        "style/extension.css",
        "script/extension.js"
      ],
      "matches": [ // 誰可以存取
        "https://example.com/*"
      ]
    }
  ],
  ...
}

2.6 合併 browser_action 與 page_action 為 action

// v2
{
  "browser_action": { … },
  "page_action": { … }
}

// v3 直接合併成一個即可
{
  "action": { … }
}

2.7 content_security_policy 需要從 string 改為物件設定

content_security_policy 用於定義載入和執行內容的安全策略,在 v3 版本你需要通過物件的形式來做設定:

// v2
"content_security_policy": "script-src 'self' 'unsafe-eval' https://cdn.lr-in-prod.com; object-src 'self'"

// v3
"content_security_policy": {
  "extension_pages": "script-src 'self'; object-src 'self'",
  "sandbox": "sandbox allow-same-origin",
  "web_accessible_resources": "https://cdn.lr-in-prod.com"
}

需要注意的是,v3 處於安全考慮,不再允許執行 eval,所以上方程式碼中 unsafe-eval 在 v3 就沒什麼意義了。

三、 background 遷移 Service Worker

我們在 manifest 更新提到 background 遷移 Service Worker 需要更新 manifest 中的欄位名,當然除了 key 變了之外, Service Worker 還會有一些本質的區別。避免大家混淆,Service Worker 和 v2 的 background 還是同一個檔案,只是現在定義,使用場景都存在部分差異,接下來細說變化。

3.1 Service Worker 不再支援 DOM 存取。

之前寫在 background 的關於 dom 操作程式碼需要移出此檔案,現有的 Service Worker 更適合用於做訊息推播,時間監聽之類的活。

當然,如果你非要在 Service Worker 使用 DOM ,你可以將訊息從 Service Worker 傳送到頁面指令碼,然後在頁面指令碼中執行相應的 DOM 操作。

第二種辦法就是通過 Offscreen API 建立離屏檔案,在離屏檔案中進行 DOM 的操作。簡單理解就是外掛單獨開闢一個虛擬環境用於你來操作 DOM, 比如:

// manifest.json
"permissions": ["offscreen"]

// offscreen.html
<script src="offscreen.js"></script>

// offscreen.js
let textEl = document.querySelector('#text');
textEl.value = data;
textEl.select();
document.execCommand('copy');

// 在 Service Worker 中建立離屏檔案
chrome.offscreen.createDocument({
  url: chrome.runtime.getURL('offscreen.html'),
  reasons: ['YOUR_REASON'],
  justification: 'YOUR_JUSTIFICATION',
});

但需要注意的是離屏檔案使用外掛自身 API 又有不少限制,具體可以檢視檔案 chrome.offscreen

3.2 Service Worker 不支援 Window 呼叫

v3 的 Service Worker 不支援呼叫 Window,因此 localStorage 直接用不了,注意只是在 Service Worker 中,其它檔案你想用還是正常用,對應的我們需要更換為 chrome.storage.local 或者其它儲存方式。

3.3 Service Worker 不再支援後臺常駐執行

在 v2 版本由於 background 支援後臺持久執行,我們可能直接在 background 定義持久變數,如下程式碼你希望統計事件派發次數:

let num = 0;
chrome.runtime.onMessage.addListener((message) => {
    num++;
    console.log(count);
});

但由於 v3 不再持久執行,那麼上述代邏每次被啟用 num 會不斷被重置為 0,如果你還需要達到上述效果得結合本地快取:

chrome.runtime.onMessage.addListener((message) => {
  const count = await chrome.storage.local.get(['num']);
    num++;
    chrome.storage.local.set({'num': num})
});

3.4 保證 Service Worker 同步註冊事件監聽

在 Manifest V3 中,背景頁面已被替換為 Service Worker。與 Manifest V2 不同,Service Worker 是事件驅動的,並且在事件被派發時會重新初始化。這意味著在 Service Worker 中非同步註冊事件監聽器的方式可能無法保證正常工作,因為在事件派發時,監聽器可能尚未註冊完畢。

比如在 v2:

chrome.storage.local.get(["badgeText"], ({ badgeText }) => {
  chrome.browserAction.setBadgeText({ text: badgeText });
  // 非同步註冊
  chrome.browserAction.onClicked.addListener(handleActionClick);
});

在 v3 請保證註冊同步:

chrome.action.onClicked.addListener(handleActionClick);

chrome.storage.local.get(["badgeText"], ({ badgeText }) => {
  chrome.action.setBadgeText({ text: badgeText });
});

3.5 v3 Service Worker 以及整個外掛都不再建議 XMLHttpRequest

在 Manifest V3 中,由於安全性和隔離性的考慮, Service Worker 禁止使用 XMLHttpRequest(),其它地方不建議使用 XMLHttpRequest。總體來講,建議將後臺指令碼中對 XMLHttpRequest() 的呼叫替換為使用全域性的 fetch() 來執行網路請求。

const xhr = new XMLHttpRequest();
console.log('UNSENT', xhr.readyState); 

xhr.open('GET', '/api', true);
console.log('OPENED', xhr.readyState);

xhr.onload = () => {
    console.log('DONE', xhr.readyState);
};
xhr.send(null);

V3 替換為 fetch:

const response = await fetch('https://www.example.com/greeting.json'');
console.log(response.statusText);

說直白點,如果你之前外掛在 background 使用了 axios(基於 XMLHttpRequest 的封裝),那麼此時你必須把 background 的請求替換成 fetch,考慮到 fetch 與 XMLHttpRequest 存在部分差異,比如 fetch 會認為 404 500 錯誤碼都不是 reject,以及 fetch 不會像 axios 直接整合請求響應攔截,所以如果你要替換一個使用上等價於 axios 的庫,這裡我推薦 ky,它解決了上述我說的對於錯誤碼的處理,retry 封裝,請求響應攔截封裝等等。

(關於 axios 與 ky 的使用差異,後續我單獨提供一篇檔案)

3.6 使用 Alarms API 代替定時器

同理,由於 Service Worker 不再常駐執行,以前我們可能通過定時器非同步來更改外掛圖示或其它操作,這都可能因為 Service Worker 釋放導致定時器無法按預期執行,使用 Alarms 代替定時器;需要注意的除 Service Worker 外定時器還是隨便你使用。

// v2
const TIMEOUT = 3 * 60 * 1000; 
setTimeout(() => {
  chrome.action.setIcon({
    path: getRandomIconPath(),
  });
}, TIMEOUT);

// v3
async function startAlarm(name, duration) {
  await chrome.alarms.create(name, { delayInMinutes: 3 });
}

chrome.alarms.onAlarm.addListener(() => {
  chrome.action.setIcon({
    path: getRandomIconPath(),
  });
});

四、API 更新

4.1 tabs.executeScript() 替換為 scripting.executeScript()

使用 scripting.executeScript() 需要在 manifest 設定許可權

"permissions": ["activeTab", "scripting"],

注入指令碼方式程式碼層面的程式碼變化,比如 tabID 不再單獨作為引數,files 支援傳遞多個檔案,格式也變成了一個陣列:

// v2
async function getCurrentTab() {/* ... */}
let tab = await getCurrentTab();

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

// v3
async function getCurrentTab()
let tab = await getCurrentTab();

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

如果是直接執行程式碼,變化如下:

// v2
chrome.tabs.executeScript(
  tab.id,
	{
  	code: alert("Hello, World!")
	}
);

// v3
chrome.scripting.executeScript({
  target: { tabId: tab.id },
  function: () => {
    alert("Hello, World!");
  },
});

4.2 tabs.insertCSS() tabs.removeCSS() 替換為 scripting.insertCSS() scripting.removeCSS()

在 Manifest V2 中,我們使用 chrome.tabs.insertCSS 方法來向分頁注入 CSS 樣式,而在 Manifest V3 中,這些方法已經從 tabs API 移到了 scripting API。這個遷移需要更新清單檔案中的許可權,以及修改程式碼。

同理,使用 scripting.insertCSS 也需要設定許可權

"permissions": [
  "activeTab", // 如果你只需要在當前啟用的分頁中注入 CSS
  "scripting"  // 新增 scripting 許可權
],

注入樣式檔案前後對比:

// v2
chrome.tabs.insertCSS(tabId, injectDetails, () => {
  file: 'style.css'
});

// v3
const insertPromise = await chrome.scripting.insertCSS({
  files: ["style.css"],
  target: { tabId: tab.id }
});
// 剩餘的程式碼

注入字串的前後對比:

// v2
chrome.tabs.insertCSS(tabId, {
  code: 'body { background-color: lightblue; }'
}, () => {
  // 在注入樣式後執行的回撥
});

// v3 支援 promise 取代回撥的寫法,當然你也能繼續用回撥
const insertPromise = chrome.scripting.insertCSS({
  target: { tabId: tab.id },
  css: 'body { background-color: lightblue; }'
});

4.3 更推薦 promise 回撥用法取代回撥

上方例子在注入樣式檔案 v2 採用回撥形式處理注入之後的行為,v3 更建議 promise 用法,當然這只是建議並不是硬性要求,保留原有回撥並不會出錯。

4.4 合併 Browser Actions and Page Actions 為 Actions

這個在 manifest 遷移已經提了一次,除了設定合併外,這兩個 API 也被合併為 actions

// v2
chrome.browserAction.onClicked.addListener(tab => { ... });
chrome.pageAction.onClicked.addListener(tab => { ... });
                                                
// v3
chrome.action.onClicked.addListener(tab => { ... });

4.5 替換 v2 background 上下文獲取的函數

在 Manifest V3 中,不同的擴充套件上下文只能通過訊息傳遞與 service worker 進行互動。因此,你需要替換那些期望與後臺上下文互動的呼叫,具體包括以下幾個:

  1. chrome.runtime.getBackgroundPage(): 這個函數通常用於獲取後臺頁(background page)的參照,以便與後臺頁通訊。在 Manifest V3 中,由於沒有後臺頁的概念,你需要使用訊息傳遞來與 service worker 通訊,而不是直接獲取後臺頁的參照。
  2. chrome.extension.getBackgroundPage(): 類似於 chrome.runtime.getBackgroundPage(),這個函數也用於獲取後臺頁的參照,而在 Manifest V3 中,同樣需要改用訊息傳遞來實現與 service worker 通訊。
  3. chrome.extension.getExtensionTabs(): 這個函數用於獲取擴充套件的分頁資訊。在 Manifest V3 中,分頁的概念發生了變化,因此需要採用不同的方法來獲取相關資訊。

第一和第二點好理解,畢竟 service worker 不再常駐,不能直接獲取 background 直接用裡面的變數,需要改為通訊的形式。

關於第三點,因為 Manifest V3 引入了一些重大的更改,包括對分頁(tabs)的管理方式。如果你需要獲取有關分頁的資訊,你可以使用 chrome.tabs API。

// 獲取當前分頁資訊:
chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) {
  // tabs[0] 包含了當前分頁的資訊
  console.log(tabs[0]);
});

// 獲取所有分頁資訊:
chrome.tabs.query({}, function(tabs) {
  // tabs 包含了所有分頁的資訊
  console.log(tabs);
});

// 更新分頁的URL:
chrome.tabs.update(tabId, { url: 'https://new-url.com' });

// 開啟一個新的分頁:
chrome.tabs.create({ url: 'https://example.com' });

4.6 替換不再支援的 API(需要全域性搜尋對應替換)

以下方法或者屬性需要在 v3 進行替換:

v2 屬性或方法 需要替換為
chrome.extension.connect() chrome.runtime.connect()
chrome.extension.connectNative() chrome.runtime.connectNative()
chrome.extension.getExtensionTabs() chrome.extension.getViews()
chrome.extension.getURL() chrome.runtime.getURL()
chrome.extension.lastError 當方法返回promise 使用 promise.catch()
chrome.extension.onConnect chrome.runtime.onConnect
chrome.extension.onConnectExternal chrome.runtime.onConnectExternal
chrome.extension.onMessage chrome.runtime.onMessage
chrome.extension.onRequest chrome.runtime.onRequest
chrome.extension.onRequestExternal chrome.runtime.onMessageExternal
chrome.extension.sendMessage() chrome.runtime.sendMessage()
chrome.extension.sendNativeMessage() chrome.runtime.sendNativeMessage()
chrome.extension.sendRequest() chrome.runtime.sendMessage()
chrome.runtime.onSuspend(background中使用) 不再支援在 service worker 使用,請用 beforeunload 代替
chrome.tabs.getAllInWindow() chrome.tabs.query()
chrome.tabs.getSelected() chrome.tabs.query()
chrome.tabs.onActiveChanged chrome.tabs.onActivated
chrome.tabs.onHighlightChanged chrome.tabs.onHighlighted
chrome.tabs.onSelectionChanged chrome.tabs.onActivated
chrome.tabs.sendRequest() chrome.runtime.sendMessage()
chrome.tabs.Tab.selected chrome.tabs.Tab.highlighted

五、安全改進

5.1 禁用 evel 等字串執行方法

V3 出於安全考慮,不再允許執行一些危險的 JavaScript 操作,比如字元相關的方法 executeScripteval()以及 new Function等方法。

eval()方法大家走知道能強制執行字串,這裡不過多解釋,關於 new Function 建立函數同樣能以字串的形式定義函數體,同理也被禁止。

關於executeScript其實上文scripting.executeScript我們已經給了例子,這裡再貼個例子:

// v2 不再推薦
chrome.tabs.executeScript(
  tab.id,
	{
  	code: alert("Hello, World!")
	}
);

// 不允許使用 eval
chrome.scripting.executeScript({
  target: { tabId: tab.id },
  function: () => {
    eval('alert("This is unsafe!")');
  }
});

// v3 正確用法
chrome.scripting.executeScript({
  target: { tabId: tab.id },
  function: () => {
    alert("Hello, World!");
  },
});

// 或者把程式碼部分單獨定義方法
const fn = () => alert("Hello, World!");
chrome.scripting.executeScript({
  target: { tabId: tab.id },
  function: fn,
});

當然有版本繞過 v3 報錯強制使用 eval,比如開啟沙盒模式,在沙盒中使用 eval,再通過與外界通訊,但是非必要不建議這麼做。

5.2 不再支援載入和執行遠端受控程式碼(這個很頭疼)

v3 出於安全考慮,不能直接參照或執行託管在遠端伺服器上的 JavaScript 程式碼,防止惡意程式碼的執行,比如:

  • 不允許從伺服器上動態拉取 JavaScript 檔案並執行。
  • 不允許通過 CDN 遠端載入程式碼。

說通俗點,你需要執行的程式碼都應該屬於外掛程式碼自身的一部分,假設外掛使用到了 react,一種解決辦法是我們本地開發 npm react 後,再打包外掛時應該將 react 原始碼也一起打包進去。

另一種辦法,就是直接將 cdn 程式碼直接拷貝到本地,然後全域性通過 scripting.executeScript 注入。

5.3 安全策略設定值調整

這一點在 manifest 提過一次,除了 content_security_policy 由 v2 字串變成 v3 物件之外,"script-src"、"object-src" 和 "worker-src" 指令,只有以下四個值是被允許的:

  1. self: 這表示只允許從與擴充套件自身相關的源載入指令碼、物件或 Worker 指令碼。
  2. none: 這表示不允許載入任何指令碼、物件或 Worker 指令碼。
  3. wasm-unsafe-eval: 這表示允許載入 WebAssembly 模組,但禁止執行不安全的 eval 操作。
  4. 僅適用於未打包的擴充套件:localhost 源,包括 http://localhosthttp://127.0.0.1 或這些域上的任何埠。

所以在 manifest 我們強調了像 unsafe-eval 這種直接廢棄了,畢竟不支援 eval 執行了。