Content ScriptsなChrome拡張をFirefox拡張に

追記(2015-10-16):WebExtensionsの発表

これを書いた数ヶ月後の8月21日、Mozillaが今後のアドオンをWebExtensionsというChrome拡張APIベースなものに移行していく計画を発表した。

なので、今後のことを考えたりもともとの「Chrome拡張をFirefoxでも動かしたい」というのを考えるとWebExtensionsベースなものにしたほうが良さそう。実際に試したところ、Chrome拡張のmanifest.jsonにWebExtensionsで必要なメンバを足しただけでふつうに動いたので、Content ScriptsベースなChrome拡張ならほとんど手間を書けずにクロスブラウザな拡張にできそう。

というわけで、このエントリで書いたAdd-on SDKベースな拡張はいささかレガシーなものになりそうだけど、気になる方はお読みくださいな。


お手製のしょうもないChrome拡張をFirefox拡張にしてみた。といってもタイトルにある通りContent Scripts、つまりユーザースクリプトレベルなものなので、そんな面倒ではなかった。

Firefox拡張のContent Scripts

まずは同じコンセプトのものがFirefoxにあるのか、ドキュメントを探す。あった。

Many add-ons need to access and modify the content of web pages. But the main add-on code doesn't get direct access to web content. Instead, SDK add-ons need to factor the code that gets access to web content into separate scripts that are called content scripts. This page describes how to develop and implement content scripts.

Content scripts can be one of the more confusing aspects of working with the SDK, but you're very likely to have to use them. There are five basic principles:

  • the add-on's main code, including "main.js" and other modules in "lib", can use the SDK high-level and low-level APIs, but can't access web content directly
  • content scripts can't use the SDK's APIs (no access to globals exports, require) but can access web content
  • SDK APIs that use content scripts, like page-mod and tabs, provide functions that enable the add-on's main code to load content scripts into web pages
  • content scripts can be loaded in as strings, but are more often stored as separate files under the add-on's "data" directory
  • a message-passing API allows the main code and content scripts to communicate with each other

拡張からページのDOMは直接触れられずContent Scripts経由というのはChromeと同じらしい。あとIsolated Worldというコンセプトも共通。

スクリプトの読み込み方は違いがあるけど、Content Scriptsのファイルはそのまま流用できそう(Chromeでしか対応してないものを使ってない限りは)。わーい。

jpm

Firefoxの場合はどうやらAdd-on SDKをインストールしないといけないらしい。これまではそのSDKPythonベースのやつだったらしいんだけど、Fx38からNodeベースのjpmというものになったとのこと。

というわけで npm install -g jpm でさくっとインストール後、jpm init で名前やらエンドポイントやらを設定。

Content Scriptsの読み込み

Chrome拡張では manifest.json からContent Scriptを動作させたいURL(のパターン)とそのファイルを明示的に指定していた。

...
"content_scripts": [
  {
    "matches": [ "https://foo.bar/*" ],
    "js": [ "baz.js" ],
    "css": [ "quux.css" ]
  },
  ...

Firefox拡張の場合はエンドポイントのファイルを用意して、そこからプログラマブルな感じで指定するらしい。jpm initした場合はindex.jsができる。

今回の拡張はページにスクリプトファイルをぶっこむものなので、page-modというAPIを使う。

const data = require('sdk/self').data
const pageMod = require('sdk/page-mod')

pageMod.PageMod({
  include: "https://foo.bar/*",
  contentScriptFile: [ data.url('baz.js') ],
  contentStyleFile: [ data.url('quux.css') ]
})

ファイルの置き場

エンドポイントを指定したら jpm init したディレクトリ直下にできたのだけれど、Content Scriptsのファイルとかはその中に data ディレクトリを作ってそこに置かないといけないらしい。ここで少しはまった。initしたら作ってほしいな。

Chrome拡張はとくにディレクトリわけとかしてなかったので、そのまま読み込めないか試すもアウト。どうやらファイルを指定したとこで使ったself.data.url()data ディレクトリ内のファイルのみしか読めないらしい。まじか…

気になったのでソースを見てみる。

exports.data = Object.freeze({
  url: uri,
  load: function read(path) {
    return readURISync(uri(path));
  }
});

はいはい。では uri はというと…

const uri = (path="") =>
  path.contains(":") ? path : addonDataURI + path.replace(/^\.\//, "");

ええ。それで addonDataURI はというと、

const addonDataURI = baseURI + "data/";

(´Д`)

固定だ…ちょうmagicだ……

というわけであきらめかけてたんだけど、ふと data.url('../hoge.js') としてみたら読み込めた。でもいいのかな…まあ動かなくなったら考えよう。

ディレクトリ固定、もやっとすることはあるけど拡張のレビューとか、他のひとの拡張を知りたいときには見るところが決まっていて便利なのだろうなとも思う。Chromeだとmanifest.json見ないといけないし。

その他

作った拡張は jpm xpi でパッケージ化すれば再起動不要なアドオンとしてインストールできる。

Content Scriptレベルならふつーにできたけど、UIになにかぶっこむとかそういうのはAPIがだいぶ違うので、できるだけモジュールに落とすとかしないと共通部分が減るばかりな気がした。試したいんだけどそういう拡張を作ったことがない(…)ので、なんかアイデアを持とう。