ブラウザ拡張「Vimmatic」の複数ブラウザ対応とManifest v3移行

ブラウザ拡張「Vimmatic」の複数ブラウザ対応とManifest v3移行

こんにちは、ブラウザ拡張「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のマイグレーションガイドを用意しています。

上記ガイドに従えば大抵のコードは移行できるはずです。 しかし実際に移行をしてみると、非互換な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に統一することにしました。

多くのメソッドは互換性があり、browserchromeに置換するだけで移行できる箇所が多くありました。 ですが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の技術スタックが気になる方は、以下のリポジトリからどうぞ!


Profile picture

Shin'ya Ueoka

B2B向けSaaSを提供する会社の、元Webエンジニア。今はエンジニアリング組織のマネジメントをしている。