こんにちは、ブラウザ拡張「Vimmatic」の開発をしている@ueokandeです。 Vimmaticの前身であるVim VixenはFirefoxのみのサポートでしたが、VimmaticはFirefoxとChromeの両方をサポートします。 複数ブラウザ対応と同時に、Manifest v3への移行も行いました。 この記事では、Manifest v3への移行とFirefox/Chromeで気をつけたことを紹介します。
Manifest v3
GoogleはManifest v3という新しい拡張機能の仕様を提案し、Google Chrome 88からManifest v3に対応しています。 Mozillaもそれに追従する形で、Firefox 101からManifest v3の開発者プレビューを開始しました。 将来はManifest v2サポートの停止が予想され、Chrome Web Storeは2022年1月にManifest v2の新たな受付を停止しています。
GoogleとMozillaはManifest v3のマイグレーションガイドを用意しています。
- Manifest V3 migration guide | Firefox Extension Workshop https://extensionworkshop.com/documentation/develop/manifest-v3-migration-guide/
- Migrate to Manifest V3 - Chrome Developers https://developer.chrome.com/docs/extensions/migrating/
上記ガイドに従えば大抵のコードは移行できるはずです。 しかし実際に移行をしてみると、非互換なAPIや複数ブラウザサポートでいくつか気をつけるポイントがありました。
browser
名前空間からchrome
名前空間への移行
ChromeはExtension APIをchrome
名前空間で提供しますが、Firefoxはbrowser
名前空間とchrome
名前空間を提供します。
chrome
は非同期処理の結果をコールバックベースで受け取りますが、Firefoxの browser
はPromiseを返します。
Vim VixenはFirefoxのみをサポートしていたので、async
/await
が使えるbrowser
を利用していました。
Manifest v3ではchrome
名前空間以下のメソッドもPromiseベースとなり、async
/await
を使ってメソッドを呼び出せます。
FirefoxとChromeの両方をサポートするVimmaticは、browser
からchrome
に移行することにしました。
Chrome上でbrowser
を利用できるwebextension-polyfillがありますが、Manifest v3をサポートしていないためchrome
に統一することにしました。
多くのメソッドは互換性があり、browser
をchrome
に置換するだけで移行できる箇所が多くありました。
ですがManifest v3ではいくつかのAPIのパラメータや定義場所が変更されているので注意が必要です。
特に気をつける必要があったのが、browser.runtime.onMessage
のメッセージハンドラです。
browser
でははメッセージに対するレスポンスをイベントハンドラの戻り値で返せます。
一方でchrome
は、Manifest v3であってもManifest v2と同様にコールバックで結果を返します。
Vimmaticでchrome
移行する上でつまずいたポイントの1つでした。
// Before
browser.runtime.onMessage.addListener(
(message, sender) => {
return doSomethingAsynchronously(message)
}
)
// After
chrome.runtime.onMessage.addListener(
(message, sender, sendResponse) => {
doSomethingAsynchronously(message).then((response: any) => {
sendResponse(response);
}).catch((err) => {
console.error(err);
})
return true;
}
)
バックグラウンドスクリプトのサービスワーカー化
Manifest v2ではバックグラウンドスクリプトはユーザーに見えないHTML上にロードされます。 Manifest v3では、サービスワーカー上にバックグラウンドスクリプトがロードされます。
ブラウザは非アクティブなサービスワーカーを、任意のタイミングで終了できます。 そのときメモリ上(変数)に格納した値は、ワーカー終了時にリセットされます。 Vim Vixenもスクロール位置を記憶するマーク機能や検索キーワードなど、タブ間で共有する情報をメモリに格納しています。
たとえば以下のようなコードは、MemoryDbの状態が永続化されません。
const MemoryDb = {};
const set(key, value) => MemoryDb[key] = value;
const get(key) => MemoryDb[key];
状態の揮発を防ぐために、Storage APIを使って保存します。
const set(key, value) = chrome.storage.set({ [key]: value });
const get(key) = chrome.storage.local.get(key).then(o => o[key]);
Storage API移行で留意すべきことがいくつかあります。
- Storage APIは非同期APIなので、それを処理するコードも非同期処理になる。
- ブラウザを再起動してもデータが残り続ける。バージョンアップなどで保存するデータのスキーマ変更に留意する。
特に後者が厄介です。 スキーマの非互換によって発生する不具合は、アドオンの新規インストール時には問題がなくても、バージョンアップ時に顕在化します。 また「再起動したら直る」も通用しないので、ストレージ周りのアクセスには留意する必要があります。
非互換APIの両ブラウザ対応
chrome
名前空間に統一しても、全てのAPIが両ブラザでサポートされているわけではありません。
愚直にAPIがサポートしているかtypeof
などで調べても良いですが、それでは処理が複雑化しそうです。
VimmaticではDIコンテナを使っており、ブラウザによって異なる実装を注入することで対応しました。
以下がブラウザごとに異なる実装を注入する例です。
process.env.BROWSER
はesbuildのdefineオプションで定義したパラメータで、ビルド時に指定した値で置換されます。
if (process.env.BROWSER === "firefox") {
container
.bind("BrowserSettingRepository")
.to(FirefoxBrowserSettingRepositoryImpl);
} else {
container
.bind("BrowserSettingRepository")
.to(ChromeBrowserSettingRepositoryImpl);
}
おわりに
FirefoxとChromeで互換性があることを期待しましたが、なんだかんだブラウザ間の違いに悩まされました。
APIだけでなく、必要なパーミッションもブラウザごとに異なります。
またバックグラウンドスクリプトのサービスワーカー化について言及しましたが、Firefoxはまだサポートしてません。
結果的に、マニフェストファイル(manifest.json
)をブラウザごとに分けることにしました。
将来もブラウザ間の互換性に気を付けながらVimmaticを開発していきます。 継続的に複数ブラウザ上の動作を担保するのは難しいので、将来的には自動テストなどでカバーする予定です。 Vimmaticの他の開発Tipsについては別記事で改めて紹介します。 Vimmaticの技術スタックが気になる方は、以下のリポジトリからどうぞ!