LoginSignup
11
8

More than 5 years have passed since last update.

Chrome 拡張機能を Firefox に移植してみた

Last updated at Posted at 2019-03-21

Google の検索結果から指定したサイトをブロックする Chrome 拡張機能 uBlacklist を、Firefox に移植してみました。

拡張機能のページ: https://addons.mozilla.org/ja/firefox/addon/ublacklist/
ソースコード: https://github.com/iorate/uBlacklist/tree/firefox

その時に変更する必要があった点のメモです。Chrome の最新バージョンは 73、Firefox は 66 でした。

基本的には

Chrome 拡張機能と Firefox 拡張機能 (WebExtensions) の互換性は高く、多くのコードはそのまま動きます。
しかしこの拡張機能では、以下の点を変更する必要がありました。多くは Google Drive で設定を同期する機能に関連したものです。

manifest.json

key

"key": "..."

Chrome では明示的に key キーを指定することで、拡張機能の ID を固定できます。開発中に OAuth2 を使った機能をテストするために必要です。
Firefox では key キーは使わず、代わりに以下のように ID を指定します。

"browser_specific_settings": {
  "gecko": {
    "id": "@ublacklist"
  }
}

ID には、GUID またはメールアドレスのような文字列 (上記のように @ の前が空だったり、有効なドメインでなくても大丈夫のようです) が使用できます。

oauth2

"oauth2": {
  "client_id": "...",
  "scopes": ["https://www.googleapis.com/auth/drive.file"]
}

Chrome では oauth2 キーを指定しておくと、chrome.identity.getAuthToken メソッドを使って簡単に Google の OAuth2 が使えます。
Firefox では oauth2 キーは使えず、後述のように chrome.identity.getAuthToken メソッドも使えません。

拡張機能の API

Firefox では browser オブジェクト下に Promise ベースの API が用意されていますが、chrome オブジェクト下のコールバックベースの API も Chrome 同様に使えます。しかしいくつかの API はサポートされていません。

chrome.identity.getAuthToken

chrome.identity.getAuthToken({interactive: true}, token => {
  // ...
});

このように簡単に Google の OAuth2 が使えるメソッドです。Firefox ではサポートされていませんから、Google のドキュメント を参考に、browser.identity.launchWebAuthFlow メソッドを使って自前で書く必要があります。

const getAuthToken = async details => {
  const OAUTH2_CLIENT_ID = "...";
  const OAUTH2_SCOPES = ["https://www.googleapis.com/auth/drive.file"];

  const authURL = 'https://accounts.google.com/o/oauth2/auth'
    + `?client_id=${OAUTH2_CLIENT_ID}`
    + '&response_type=token'
    + `&redirect_uri=${encodeURIComponent(browser.identity.getRedirectURL())}`
    + `&scope=${encodeURIComponent(OAUTH2_SCOPES.join(' '))}`;

  const redirectURL = await browser.identity.launchWebAuthFlow({
    url: authURL,
    interactive: details.interactive || false
  });

  const params = new URLSearchParams(new URL(redirectURL).hash.slice(1));
  if (params.has('error')) {
    throw new Error(`Authentication failed: ${params.get('error')}`);
  }
  return params.get('access_token');
};

また chrome.identity.getAuthToken は、取得したアクセストークンを有効期間中キャッシュしてくれます。上記のコードには含めていませんが、これも自分で実装する必要があります。

chrome.identity.removeCachedAuthToken

chrome.identity.removeCachedAuthToken({token: '...'}, () => {
  // ...
});

アクセストークンのキャッシュを削除するメソッドで、やはり再実装する必要があります。

ウェブ標準

dialog 要素

Firefox はデフォルトの状態では <dialog> 要素に対応していません (バージョン 66 時点)。about:config から dom.dialog_element.enabledtrue に設定することで有効にできますが、拡張機能のユーザーにそれを要求することはできませんし、また showModal メソッドが意図した挙動をしません。
そこで polyfill を導入する必要があります。

dialog-polyfill

dialog-polyfill.jsdialog-polyfill.css を読み込んだ上で、<dialog> 要素に対して dialogPolyfill.registerDialog メソッドを呼び出すと、ダイアログとして使えるようになります。

<dialog id="ubBlockDialog">
  ...
</dialog>
const blockDialog = document.getElementById('ubBlockDialog');
dialogPolyfill.registerDialog(blockDialog);

blockDialog.showModal();

この拡張機能で使っている範囲では、Chrome の <dialog> と同じ挙動が実現されています。

MutationObserver

Chrome と Firefox の挙動の違いにハマりました。と言っても、今回は拡張機能側の潜在的なバグといった感じですが。

MutationObserverobserve メソッドで {childList: true} を指定すると、子ノードの追加・削除時にコールバックが呼び出されます。これはテキストノードを含みますから、監視対象の textContent を変更すると、子のテキストノードの削除と追加が行われ、コールバックが呼び出されます。
Firefox は textContent をセットする度にコールバックを呼び出します。しかし Chrome はその内容が変わらない場合はコールバックを呼び出しません。以下のコードで確認できます。

<span id="foo"></span>
<script>
  const foo = document.getElementById('foo');
  new MutationObserver(() => {
    console.log('callback');
  }).observe(foo, {
    childList: true
  });
  setInterval(() => {
    foo.textContent = 'foo';
  }, 1000);
</script>

Firefox は 1 秒ごとにコンソールに callback と出力しますが、Chrome は最初の 1 回しか出力しません。
元々の拡張機能のコードでは、コールバック中に毎回 textContent をセットしていたため、Firefox では無限ループに突入していました。
対応としては、textContent をセットするのではなく、子のテキストノードの nodeValue を変更してあげれば良さそうです (参考)。今回は、そもそもコールバックの度に textContent をセットする設計を改めました。

サードパーティライブラリ

Google API Client Libraries (gapi)

Google の API に簡単にアクセスするためのライブラリで、各言語版が用意されています (JavaScript 版)。これが Firefox アドオンからだと動きませんでした。

const script = document.createElement('script');
script.src = 'https://apis.google.com/js/api.js';
script.addEventListener('load', () => {
  gapi.load('client', {
    callback() {
      // ...
    },
    onerror(e) {
      // e == "d'moz-extension"
      throw new Error(`The Google API client failed to load: ${e}`);
    }
  });
});
document.body.appendChild(script);

d'moz-extension というエラーメッセージが返ってきているので、Firefox 拡張機能の URL のスキーム moz-extension に対応していない感じがします。検索すると似たような事案はあるようです。
仕方がないので gapi の使用をやめて、API リクエストを行う gapi.client.request の呼び出しを、fetch を使った自前の実装に差し替えました。
さらに、Firefox 拡張機能のオリジン moz-extension://... からの fetch では CORS が通らないようなので、manifest.jsonpermissions に以下を追加して、同一オリジンポリシーを回避しました。

"permissions": [
  "...",
  "*://*.googleapis.com/*",
  "*://*.googleusercontent.com/*"
]

オプションページ

window.close

Chrome 拡張機能のオプションページは、(manifest.json"open_in_tab": true にしていなければ) モーダルダイアログとして開き、拡張機能が自分で閉じることができます。

document.getElementById('okButton').addEventListener('click', () => {
  // ...
  window.close();
});

Firefox ではオプションページは親ページに埋め込まれる形で表示され、拡張機能から勝手に閉じることはできません。

まとめ

最初に述べたように、そもそも拡張機能の互換性は高いです (Chrome が Manifest V3 に移行したらどうなるのか知りませんが…)。しかし使用している API やライブラリによっては、変更を加える必要があります。

11
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
8