最近思うところあってブラウザー拡張を作って公開しました。(Macが対応していない絵文字を使っているので、変に見えます。いずれどうにかする。) 仕事中についSNS見ちゃうのを止めるやつです。

スイッチオンにしてTwitterとかを開くと止められます。

作ったものについてはそのうち記事に書きたいと思ってます。あとブラウザー拡張の作り方についてもちゃんとした形にしたいなと思って準備中。

それはそれとして、作成中に得た知見のひとつ、互換性についてです。

先にまとめ

  • ポリフィル使えばすぐコード共通化できるしPromise化する
  • 共通化したコードはFirefoxに寄る
  • Edgeはもうひと手間
  • async/await はいいぞ

互換性

前提として、ブラウザー搭載のAPIは以下のような感じ。

  • Chromeは chrome オブジェクト以下にAPIを持つ
  • Firefoxは chrome 、Edgeは browser にChrome互換APIを持つ
  • Chrome互換APIは、コールバックは引数に与える

Firefoxは互換APIに加えて別にAPIがあります。

  • Firefoxは browser にChromeのAPIと同機能だが別I/FのAPIを持つ
  • Firefox独自APIはPromise化されている
  • async/await を利用できる
  • 後述のポリフィルで、ChromeとEdgeでもいける(いえーい)

つまりどのブラウザーでもだいたい同じ機能がそろってるけど、インターフェイスがちょっと違うよと。表にまとめるとこんな感じ。

ブラウザー chrome browser ポリフィル
Chrome
Edge
Firefox

Promiseだと嬉しいって話

「ちょっと違うインターフェイス」の差はPromise化されているかどうかです。なので共通化する場合は、全部Promise化するか、逆に全部Promiseを剥がすかのどちらかになるわけですが、前者Promise化する方をお勧めします。というわけでその理由なんですけれども。

コールバック関数方式

まずこちらの例。Chrome互換APIで、保存した情報を持ってくるやつです。

chrome.storage.local.get(["item1", "item2"], (result) => {
  console.log('# result', result.item1, result.item2);
});

まあこれはこれで問題ないんだけど。

Promise方式

一方FirefoxがChrome互換APIとは別に独自に持っている browser 系APIだと、これがPromiseになる。(「にもなる」の方が正しいかも。)

browser.storage.local.get(["item1", "item2"]).then((result) => {
  console.log('# result', result.item1, result.item2);
});

これだけだと、まああんまり変わらないように見える。

いちおうメリットとして、Promiseを使うと非同期処理を連結してもコードのインデントが深くならないというものががあります。が、そんなものより、ES2017で導入された async/await を利用できる面が大きい。

async/await

前述のPromiseを使ったコードは、( async な関数の中で)以下のように書ける。

const result = await browser.storage.local.get(["item1", "item2"]);
console.log('# result', result.item1, result.item2);

まっすぐ書けるのでたいそう見やすい。

どうすか

Promiseの方が良くないすか?

APIをPromise化するライブラリ

というわけでPromise化したくなりましたか? なりましたね? なったので、これ↓を導入して実現します。

残念ながらライブラリのファイルが単体で公開されていないようです。ビルドシステムを導入していない場合でも、一度npmでインストールしてからファイルをコピーしてきます。

$ npm install webextension-polyfill
$ cp node_modules/webextension-polyfill/dist/browser-polyfill.min.js .

でもって読み込むようにする。

<script src="/browser-polyfill.min.js"></script>
<script src="/popup.js"></script>

これでChromeでも browser を使って、 async/await 記法でさらさら書けるようになります。やったー!

Edge対応

最終的にできあがったのはこれ↓

ライブラリを有効化する

件のライブラリは現状ではEdgeに対応しておらず、検討中みたい。

詳細省略するけど、ライブラリ読み込み前にこれ↓を実行したらEdgeもうまいこといった。

// ※別途に `cloneDeep()` 的なものをご用意ください
"use strict";

// "SCRIPT5045: Assignment to read-only properties is not allowed in strict mode" を避ける
try {
  if (window.browser) {
    browser.storage.local.get = browser.storage.local.get;
  }
} catch (error) {
  // 上書きできるようにする
  window.chrome = cloneDeep(window.browser);

  // Chrome偽装
  window.browser = undefined;
}

openOptionsPage()

他に、自分の見つけた範囲だと chrome.runtime.openOptionsPage() が実装されていないようなので、自前でpolyfillを実装した。他にもそういうのありそう。

window.chrome.runtime.openOptionsPage = () => {
  const { options_page } = browser.runtime.getManifest();
  browser.tabs.create({
    url: `/${options_page}`,
  });
};

manifest.json の互換性

今回特につまづかなかったけど、いちおうブラウザーによって必須だったり名前が違ったりするところがある。

一覧

options_ui

Chromeでは chrome_style: true が推奨。Edgeでは options_page になる。

{
…
  "options_page": "options_ui/index.html",
  "options_ui": {
    "page": "options_ui/index.html",
    "chrome_style": true
  }
}

あとFirefoxでも browser_style: true が推奨だけど、初期値がそうなっているので記述しなくてもいい。

APIの互換性

ライブラリがあるとは言ってももともと実装されていないものはされていないのであきらめる。

MDNの互換性の表を見るのが良さそう。

例えば先ほどの runtime.openOptions() は、やっぱり対応してなかった。

その他

  • Firefoxの chrome オブジェクトについての記述がMDNで見当たらなかった
  • Firefoxの browser でもChrome互換APIと同様にコールバックを引数に与えられるっぽい。でもMDNでの記述は見当たらなかった
  • Edgeは全く関係ない chrome オブジェクトを持っているっぽいけど詳細不明
  • ブラウザー拡張の標準化がW3Cで進行中 → Browser Extensions
  • 標準化されたAPIはPromise化されている(つまりFirefox風)
  • Chromeは標準APIに寄せる気がないらしい[要出典]
  • EdgeはChromeだけを見ているらしい[要出典]
  • Edge拡張を作るのは簡単だけど、公開するの難しそうでつらい

おしまい

async/await いいわー。

更新履歴

2018-04-24

  • Edge対応コードの "use strict" がなんか抜けてたのを修正
  • Edge対応コード完成版のリンクを追加
  • APIの互換性について追加