Chrome 計劃完全停止 v2 版本維護,後續 v2 版本將無法上架谷歌外掛商店,除此之外,未來新版本 Chrome 對於 v2 版本外掛的限制會越來越大,比如安全性限制 iframe 巢狀只能通過沙盒模式資料通訊傳遞而不能直接獲取資料等等,因此 v2 遷移 v3 是必要的。
主要是四個方向的改動:
以下是具體需要做的事情。
manifest 版本號需要改為 3。
// v2
{
...
"manifest_version": 2
...
}
// v3
{
...
"manifest_version": 3
...
}
persistent 用於決定 Chrome extensions 是否開啟常駐後臺,由於 v3 版本 background 遷移 service worker 後後臺已經做不到常駐,此屬性只能作廢,刪掉就好。
在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": [// 單獨設定主機許可權
"*://*/*",
]
}
V3 使用 Service Worker 取代了 Background,這裡包含兩個層面的意思,第一是設定欄位變了(見下方程式碼),第二是 Service Worker 不再像之前的 Background 能做到一直在後臺執行,這點我們後面細說。
// v2
{
...,
"background": {
"scripts": ["background.js"],
"persistent": false
},
...
}
// v3
{
...,
"background": {
"service_worker": "background.js" // 欄位變了
// 刪了 persistent
},
...
}
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/*"
]
}
],
...
}
// v2
{
"browser_action": { … },
"page_action": { … }
}
// v3 直接合併成一個即可
{
"action": { … }
}
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 就沒什麼意義了。
我們在 manifest 更新提到 background 遷移 Service Worker 需要更新 manifest 中的欄位名,當然除了 key 變了之外, Service Worker 還會有一些本質的區別。避免大家混淆,Service Worker 和 v2 的 background 還是同一個檔案,只是現在定義,使用場景都存在部分差異,接下來細說變化。
之前寫在 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
v3 的 Service Worker 不支援呼叫 Window,因此 localStorage 直接用不了,注意只是在 Service Worker 中,其它檔案你想用還是正常用,對應的我們需要更換為 chrome.storage.local 或者其它儲存方式。
在 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})
});
在 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 });
});
在 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 的使用差異,後續我單獨提供一篇檔案)
同理,由於 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(),
});
});
使用 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!");
},
});
在 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; }'
});
上方例子在注入樣式檔案 v2 採用回撥形式處理注入之後的行為,v3 更建議 promise 用法,當然這只是建議並不是硬性要求,保留原有回撥並不會出錯。
這個在 manifest 遷移已經提了一次,除了設定合併外,這兩個 API 也被合併為 actions
// v2
chrome.browserAction.onClicked.addListener(tab => { ... });
chrome.pageAction.onClicked.addListener(tab => { ... });
// v3
chrome.action.onClicked.addListener(tab => { ... });
在 Manifest V3 中,不同的擴充套件上下文只能通過訊息傳遞與 service worker 進行互動。因此,你需要替換那些期望與後臺上下文互動的呼叫,具體包括以下幾個:
chrome.runtime.getBackgroundPage()
: 這個函數通常用於獲取後臺頁(background page)的參照,以便與後臺頁通訊。在 Manifest V3 中,由於沒有後臺頁的概念,你需要使用訊息傳遞來與 service worker 通訊,而不是直接獲取後臺頁的參照。chrome.extension.getBackgroundPage()
: 類似於 chrome.runtime.getBackgroundPage()
,這個函數也用於獲取後臺頁的參照,而在 Manifest V3 中,同樣需要改用訊息傳遞來實現與 service worker 通訊。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' });
以下方法或者屬性需要在 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 |
V3 出於安全考慮,不再允許執行一些危險的 JavaScript 操作,比如字元相關的方法 executeScript
、eval()
以及 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,再通過與外界通訊,但是非必要不建議這麼做。
v3 出於安全考慮,不能直接參照或執行託管在遠端伺服器上的 JavaScript 程式碼,防止惡意程式碼的執行,比如:
說通俗點,你需要執行的程式碼都應該屬於外掛程式碼自身的一部分,假設外掛使用到了 react,一種解決辦法是我們本地開發 npm react 後,再打包外掛時應該將 react 原始碼也一起打包進去。
另一種辦法,就是直接將 cdn 程式碼直接拷貝到本地,然後全域性通過 scripting.executeScript
注入。
這一點在 manifest 提過一次,除了 content_security_policy
由 v2 字串變成 v3 物件之外,"script-src"、"object-src" 和 "worker-src" 指令,只有以下四個值是被允許的:
self
: 這表示只允許從與擴充套件自身相關的源載入指令碼、物件或 Worker 指令碼。none
: 這表示不允許載入任何指令碼、物件或 Worker 指令碼。wasm-unsafe-eval
: 這表示允許載入 WebAssembly 模組,但禁止執行不安全的 eval 操作。localhost
源,包括 http://localhost
、http://127.0.0.1
或這些域上的任何埠。所以在 manifest 我們強調了像 unsafe-eval
這種直接廢棄了,畢竟不支援 eval 執行了。