First touch with Extension Development — Manifest V3

Paul Li
15 min readDec 12, 2021

Introduction

Extensions development 對我來說一直是一門充滿魅力的領域,藉由 extensions 的安裝,便可以立馬將瀏覽器功能做多元的擴展,讓瀏覽器可以更貼近我們的使用習慣,不管在網頁瀏覽、購物甚至是服務的擴展,通通都有機會可以實現,對 front-end engineers 來說幾乎沒有什麼學習曲線,只要照平時的開發習慣與態度,都能在極短的時間入門。

Chrome web store 上面的 extensions 大多是以 Manifest V2 的形勢下去開發,我們知道隨著時空背景的差異,當年 fancy 的玩意兒,到了現在可能就會變成是種累贅,甚至會影響到產品的開發與品質。

這種種的因素,Google 點滴在心頭,也因為這樣,所以推出了 Manifest V3,藉由淘汰重複性質高的 API 以及導入符合時宜的寫法,更以加速審核流程等種種誘因來驅動 developers 來快速進行 migration。除此之外,Google 亦擬好了完整的 plan 以及 roadmap 來進行以及加速整體汰換的驅動。

Manifest V2 support timeline. Chrome Developers, https://developer.chrome.com/docs/extensions/mv3/mv2-sunset/

從 timeline 可以清楚看到︰

  • 2022/01/17 將不受理非 Private 的 MV2 extensions 上架。
  • 2022/06 不再受理任何 MV2 extensions 上架。

不過既有已上架的 MV2 extensions 還是可以持續 update。所以新的 MV2 extensions 請把握時間趕在 2022/01/17 前完成上架動作。

也就是說 2022 雖不至於完全封殺 MV2 extensions,但…九死只剩一生了。既然早晚都要面對 MV3,何不早些時候開始?實際上,Chrome Developers 已經為大家整理好 Migrating to Manifest V3 小抄了,只要參照文件定可以快速將 Extensions 快速升級。

以下,筆者將會參照這份文件並且搭配自己旗下 Extensions 升級經驗和大家一同分享這有趣的過程。

manifest.json

manifest.json 就宛若設定檔的存在一般,Extension 需要的 feature、permission 以及相關介紹均需要在這兒設定。由於 MV3 針對 background script 屬性的變更以及統合部分同性質的屬性,所以我們需要做若干設定上的變更。

manifest_version

既然要撰寫 MV3,自然就是先從版號進行調整。

// MV 2
{
"manifest_version": 2
...
}
// MV3
{
"manifest_version": 3
...
}

background

background 在 MV3 中已經調整為 service worker 型態,所以相關的宣告也要進行調整,在這裡僅需設定檔案路徑即可。﹙如果需要 require 多個 script files 的話,可以在 background.js 裡頭進行 importScripts 或是 import﹚

// MV 2
{
"background": {
"scripts": [
"color-thief.js",
"background.js"
],
"persistent": false
},
...
}
// MV3
{
"background": {
"service_worker": "background.js"
},
...
}

另外,亦可以透過參數的設置,讓 background.js 以 JavaScript modules 的型態展開。

// MV3
{
"background": {
"service_worker": "background.js",
"type": "module"
},
...
}

permissions

MV2 需要調用的 Web APIs 以及 CORS fetch urls 均需要在這裡進行設置,在 MV3 中則多了一個 host_permissions 的欄位來設置 CORS host,各司其職較不混淆。如下列範例所示,便將 CORS host 收納至 host_permissions 中。

// MV 2
{
"permissions": [
"identity",
"https://api.login.yahoo.com/oauth2/*",
"https://api.login.yahoo.com/openid/v1/userinfo"
],
...
}
// MV3
{
"permissions": [
"identity"
],
"host_permissions": [
"https://api.login.yahoo.com/oauth2/*",
"https://api.login.yahoo.com/openid/v1/userinfo"
],
...
}

browser_action & page_action

初開發 Extension 總是很容易為兩者感到混淆,在 MV3 中則透過 action 將兩者整合在一起,也就是說我們可以把兩者內容收納在一塊。

// MV 2
{
"browser_action": { ... },
"page_action": { ... },
...
}
// MV3
{
"action": { ... }
...
}

由於兩者已被整在一塊兒,所以如果有在 background script 下 click 監聽的語法亦要進行調整。

// MV2
chrome.browserAction.onClicked.addListener(
() => {
...
}
);
// MV3
chrome.action.onClicked.addListener(
() => {
...
}
);

commands

Extension 搭配 keyboard shortcuts 來做啟動動作是非常 common 且便利的做法,如上所述,由於 browser_action 已經被整合到 action 中,所以 commands 裡頭以需要做相對應的調整。

// MV2
{
"commands": {
"_execute_browser_action": {
"suggested_key": {
...
}
}
},
...
}
// MV3
{
"commands": {
"_execute_action": {
"suggested_key": {
...
}
}
},
...
}

web_accessible_resources

透過 web_accessible_resources 的設定,Developers 便有機會於 web page 中使用 extension package 裡頭的檔案,比方說 image 以及 javascript。在 MV3 中寫法與 content script 拉齊,所以使用上更加靈活且具有彈性。

// MV2
{
"web_accessible_resources": [
"/mjs/common-css.js",
"/mjs/common-lib.js",
"/mjs/wc-msc-spotlight.js"
],
...
}
// MV3
{
"web_accessible_resources": [
{
"matches": ["*://*/*"],
"resources": ["/mjs/*"]
}
],
...
}

content_security_policy

使用 chrome-extension:// protocol 的頁面如果需要設置 CSP 時,可以透過這個欄位進行調整,比方說 inline script default 是關閉的,若有使用上的需求則需要調整這個欄位。MV2 以及 MV3 一樣有寫法上的差異需要特別注意。

// MV2
{
"content_security_policy": "...",
...
}
// MV3
{
"content_security_policy": {
"extension_pages": "...",
"sandbox": "..."
},
...
}

Background script

由於 background script 已經改為 service worker 型態,也就是說 DOM 操作將無法在這兒使用了,另外我們也 touch 不到 window 物件了,所有只要有相關的操作都要搬回去 content script 中操作。算是結構上的調整,亦是整個 migration 過程中最廢時耗力的工。

如下所示,window 已不復存,在這裡可以使用 chrome.tabs 來達到相同作用﹙開啟新視窗﹚。

// MV2
function goAuctionSearch(keyword) {
const params = new URLSearchParams({ p: keyword });
const url = `https://tw.bid.yahoo.com/search/auction/product?${params}`;

window.open(url, '_blank');
}
// MV3
function goAuctionSearch(keyword) {
const params = new URLSearchParams({ p: keyword });
chrome.tabs.create({
url: `https://tw.bid.yahoo.com/search/auction/product?${params}`
});
}

下例是另一個 DOM 操作的轉換,主要作用為透過 background script 來 fetch CORS image 並且轉換為 dataURI 的形式回傳 content script。原本的做法是直接在 background script 中使用 <canvas /> 來進行 fetch 以及型態轉換,不過 canvas 以及 image 已經不能使用了,所以要尋找其他替代方案,在這裡筆者直接使用 FileReader 來替代,其實也可以透過 OffscreenCanvas 來達到相同的作用。

// MV2
chrome.runtime.onMessage.addListener(
(src, sender, sendResponse) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');

const img = new Image();
// fetch image & convert to dataUrl
img.crossOrigin = 'anonymous';
img.onload = (evt) => {
const { width, height, src } = evt.target;

canvas.width = width;
canvas.height = height;
ctx.clearRect(0, 0, width, width);
ctx.drawImage(img, 0, 0);
sendResponse(canvas.toDataURL()); // GC
img.onload = null;
img.src = null;
img = null;
};
img.src = src;
return true;
}
);
// MV3
chrome.runtime.onMessage.addListener(
(src, sender, sendResponse) => {
const reader = new FileReader();
reader.addEventListener('load', function () {
sendResponse({ dataURL:reader.result });
}, false);
fetch(src)
.then(response => response.blob())
.then(blob => {
reader.readAsDataURL(blob);
})
.catch(error => console.log(`fetch image fail:${error.message}`));
return true;
}
);

Persisting state with storage APIs

由於 service worker 的特性,所以變數的值有可能因為會被洗掉造成不預期的錯誤,可以搭配 Storage API 來存取 persisting variable。

// MV2// Don't do this! The service worker will be created and destroyed over the lifetime of your
// exension, and this variable will be reset.
let savedName = undefined;

chrome.runtime.onMessage.addListener(({ type, name }) => {
if (type === "set-name") {
savedName = name;
}
});

chrome.browserAction.onClicked.addListener((tab) => {
chrome.tabs.sendMessage(tab.id, { name: savedName });
});

搭配 Storage API 後的 MV3 寫法

chrome.runtime.onMessage.addListener(({ type, name }) => {
if (type === "set-name") {
chrome.storage.local.set({ name });
}
});

chrome.action.onClicked.addListener((tab) => {
chrome.storage.local.get(["name"], ({ name }) => {
chrome.tabs.sendMessage(tab.id, { name });
});
});

Content Script

content script 在使用上倒是沒有差異,比較可惜的是它依然無法以 JavaScript Modules 的方式展開。大部分的 Web APIs 均有支援 promise 的型態了,如果有餘力的話,可以搭配 async / await 來做些 refactor,可以讓code 更加符合現有的開發模式,既有 callback 寫法依舊有支援,所以就算沒有調整大致上也不會問題。

由於部分 Web APIs 已經 sunset 了,所以如果發現 migrate MV3 後功能無法正常運作,多半是中了招了,這時候也只能去 API Reference 查找新的寫法了。

Conclusion

由於 migration 文件寫得還算清楚,所以在轉換的過程還算順暢。個人覺得最為麻煩的部分莫過於 background script 型態的轉變,由於已經是 service worker 的型態,導致結構上可能需要做調整,如果有 DOM 或者是 window 的操作,通通都要扔回 content script 中去做了,不然簡單一點的 extension 應該 manifest.json 調整一下就可以正常運作了。

最後,提供一個冷知識給大家,如果想知道自己常用的 extensions 是屬於 MV2 or MV3 的話,只要前往 chrome://extensions/ 並開啟 Developer mode,若 Extension information card 中 Inspect views 顯示 service worker 的訊息﹙如範例所示﹚,便表示該 Extension 已經是 Manifest V3 ready 了。

Extension MV3 ready
Extension MV3 ready

如果對筆者撰寫的 Extension 有興趣的話,也可以透過下方的 Reference 連結進行一覽以及安裝。

以上,提供給大家參考,希望有所助益~ #Extensions #MV2 #MV3

Reference

--

--

Paul Li

Paul is the lead programmer of the AMP project at Yahoo Taiwan and is always eager for modern web technologies. He is also focusing on UX for vivid user flow.