Firefox Add-on→Web Extensionsに書き換えた話

  • このエントリーをはてなブックマークに追加

Yahoo! JAPAN Tech Advent Calendar 2017の22日目の記事です。一覧はこちら

こんにちは、ヤフオク!でフロントエンド開発をしている新卒1年目の中村(ぱちお)です。
Firefoxがバージョン57になった影響で、従来使っていた拡張機能が使えなくなってしまったため、対応した話をします。今まで私自身は拡張機能の開発をやったことがなかったので、いい経験になるかなと思い挑戦してみました。

Firefoxはバージョン57から「Web Extensions」のみをサポートし、従来型の拡張機能はサポートが打ち切られました。(参考
そのためSNSなどでも、「普段使ってた拡張機能が、ブラウザーの更新をしたら動かなくなった!」という声があがっていました。

従来の拡張機能

従来は jpm というパッケージを使って、作った拡張のソースコード群を xpi という圧縮ファイルに落とし込んで配布していました。 過去には cfx というコマンドラインツールもあったようですが、現在はどちらもDeprecatedになっています。

Web Extensions

Firefox 57から推奨されているWeb Extensionsは、ざっくりいうと様々なブラウザーで利用できる、互換性のある拡張機能のことです。
Firefoxが今までWeb Extensionsに対応していなかったわけではなく、後発だったため従来型の拡張機能が多かったんだと思います。

配布の流れは従来型と似ていて、zip形式でまとめたソースコードを基に配布する形をとります。ブラウザーで動かすものなので、もちろんソースコード自体も同じくJavaScriptです。

なぜ動かなくなったのか

ではなぜ従来型は動かなくなったのでしょうか。
原因はSDK側にありました。FirefoxのSDKの機能でブラウザのタブ操作や、DOMの取得を行ってるためです。

上記で紹介したjpmチュートリアルにあるスクリプトを見てみます。

var buttons = require('sdk/ui/button/action');
var tabs = require("sdk/tabs");

var button = buttons.ActionButton({
  .....
  onClick: handleClick
});

function handleClick(state) {
  tabs.open("http://www.mozilla.org/");
}

これは、拡張タブ上に表示されてるをボタンクリックしたら http://www.mozilla.org/ にページジャンプするスクリプトです。
最初の1,2行目がSDK依存の部分です。拡張タブ上のボタンのアクションを取得するためにSDKを利用しているため、変数が全部使えなくなり動かなくなりました。
そのため、最小限でソースコードを変える場合は、requireで定義されてる部分をWeb Extensionsに則った形で書き直します。

Web Extensionsを書いてみる

要件は「ファイル構造をできるだけ変えずにWeb Extensionsに書き換える」ことです。
先程のスクリプトが動作するように変更を加えていきます。

ファイルを追加する

/my-addon ー|
      ー manifest.json(新しく作成)
      ー package.json(元からあった)
      ー /data(元からあった)
         |_  icon-16.png
         |_  icon-32.png
         |_  icon-64.png
      ー index.js(元からあった)
      ー background.js(新しく作成)

package.jsonを参考に、manifest.jsonを書きます。

manifest.json

{
  "manifest_version": 2,
  "name": "tutorial",
  "version": "1.0.0",
  "description": "test",

  "icons": {
    "48": "data/ icon-16.png"
  },
  "browser_action": {
      "default_icon": {
          "19": "data/icon-16.png"
      },
      "default_title": "titile"
  },
  "permissions": [
    "tabs", "*://www.yahoo.co.jp/*"
  ],
  "content_scripts": [
    {
      "matches": ["*://www.yahoo.co.jp/*"],
      "js": ["index.js"]
    }
  ],
  "background": {
    "scripts": ["background.js"]
  }
}

manifest_version、name、versionは必須項目です。
"ツールバーのアイコンをクリックしたら〜"というイベントを受け取りたい場合は、 permissions を追加で指定する必要があります。

さらに、読み込ませたいスクリプトをcontent scriptsに記述します。
ただし、content scriptsには制限が多く、使えるAPIが少ないです。(例えばタブのボタンクリックの監視はできない)

(公式文章) https://developer.chrome.com/extensions/content_scripts

However, content scripts have some limitations. They cannot:
Use chrome.* APIs, with the exception of:
extension ( getURL , inIncognitoContext , lastError , onRequest , sendRequest )
i18n
runtime ( connect , getManifest , getURL , id , onConnect , onMessage , sendMessage )
storage

ではどうすればいいのかというと、backgroundを利用します。
backgroundは、読み込んだ時に1度だけ実行すればいい処理や、裏で監視してほしい処理を実行してくれます。

backgroud.js

chrome.browserAction.onClicked.addListener((tab) => {
    chrome.tabs.sendMessage(tab.id, "myAction");
});

従来は以下のように記述していました。

var button = buttons.ActionButton({
  .....
  onClick: handleClick
});

backgroundのアクションはいっぱいあるので、詳しくは 公式ページ を参照してください。
このbackground.jsですが、実行されているのが「現在の表示されているページ」ではない場所(おそらくタブのところ?)のため、DOM、グローバル変数などが操作(参照)できません。そのため content scripts側に処理を渡す必要があります。

tabs.sendMessageの第二引数で渡したアクション名を runtime.onMessage の第一引数で受け取ることができます。なので index.jsを以下のように書き換えます。

index.js

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
    if (request === "myAction") {
         handleClick();
    }
});
function handleClick() {
  window.open("http://www.mozilla.org/");
}

これで、元のFirefoxのアドオンと同じ機能になりました。

デバッグする

実際に動作するかどうか、デバッグしてみます。

FirefoxでWebExtensionを一時的に読み込ませる方法は、Firefoxのアドレスバーに「about:debugging」と打ち込んでアクセスします。一時的なアドオンを読み込むのボタンからmanifest.jsonを選ぶと自分の書いたスクリプトが実行されるようになります。

今回はデバッグまでですが、実際に公開・配布する方法はこちらです。

io/fileのダウンロード実装したい(おまけ)

SDKにio/fileといったファイル操作とかもできてしまう便利な機能があるのですが、Web Extensionに対応するのは少々骨が折れます。
ただのダウンロードだけであれば、Web ExtensionのAPIが存在しているので、比較的簡単に実装できます。

index.js

chrome.runtime.sendMessage ({
    "action": "download_file",
    "fileName": fileName,
    "remoteUrl": remoteUrl
});

background.js

// ダウンロード:index.jsから呼ばれる
 chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
     if (request.action === 'download_file') {
         chrome.downloads.download({
             url: request.remoteUrl,
             filename:  '/data/' +request.fileName
         });
     }
 });

これでダウンロードディレクトリ配下に、filenameで指定した名前でurlの内容がダウンロードされます。content scriptsはchrome.downloadsのAPIを実行できないため、backgroundに一度処理を渡しています。

また、content scriptsからはtabsのAPIを実行できないので、tabsではなく、rumtimeでsendMessageします。
※downloadもpermissionあるのでmanifest.jsonへの記述忘れずに

さいごに

触れないであろうと思ってたことに触れる機会ができて良かったです。Web Extensions思ってたより簡単に書けたのでみなさんもどんどんやっていきましょう!

記事内容とは関係ないですが、今回環境としてはGulp+Browserifyを採用しました。配布用のzip作成などは、Gulpのタスクに加えると楽です。拡張機能だけでなく、その周辺も一緒に勉強できたので、たくさんの知見が得られました!最近parcel なんてのも出てきましたね。

あとFirefoxなのにchrome.*とソースコードに書くのは、違和感がめちゃくちゃあって面白かったです。

Yahoo! JAPANでは情報技術を駆使して人々や社会の課題を一緒に解決していける方を募集しています。詳しくは採用情報をご覧ください。

  • このエントリーをはてなブックマークに追加