Chrome/Firefox両対応の拡張機能(アドオン)を簡単に作る方法

10 min read読了の目安(約9400字

HTML Minify Clipboardという Firefox アドオンを作りました。
このアドオンのソースコードはChrome拡張機能としても動作します。
今回作成してみて、Chrome/Firefoxなど様々なブラウザで使える拡張機能の開発がとても簡単だったので、その作り方を紹介します。

ひな型作成

アドオンを作るにあたり、ブラウザの拡張機能を開発するためのひな型とそれをビルドするための仕組みをあらかじめ構築してくれるCLIツールを使いました。
これを使えば、必要なコードの追加とビルドコマンドを実行するだけで拡張機能が作れるのでとても楽でした。

Generator: WebExtension

https://github.com/webextension-toolbox/generator-web-extension

まず、グローバルに以下をインストールします。

$ npm install -g yo generator-web-extension

拡張機能を開発するディレクトリを作成し(ここではhtml-minify-clipboard)、そのディレクトリの中でyo web-extensionを実行します。

$ mkdir html-minify-clipboard
$ cd html-minify-clipboard
$ yo web-extension

実行後、ひな型に必要な情報を聞かれるので、自分の作りたいものに応じて入力していきます。

? What would you like to call this extension? (html minify clipboard)
? And how would you call it if you only had 12 characters (short_name)? (html minify)
? How would you like to describe this extension? (Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusm
od tempor incididunt ut labore et dolore magna aliqua.)

拡張機能名や説明など入力できますが、app/_locales/en/messages.jsonで後から編集可能なので、そのままエンターキーを押して進めます。

? Would you like to use UI Action? (Use arrow keys)
> No
  Browser
  Page
? Would you like to override a chrome page? (Use arrow keys)
> No
  Bookmarks Page
  History Page
  Newtab Page

今回作成した拡張機能は、選択中のテキストについて処理を行うものなのでNoを選択してエンターキーを押します。

? Would you like more UI Features? (Press <space> to select, <a> to toggle all, <i> to invert selection)
>(*) Options Page
 ( ) Devtools Page
 ( ) Content Scripts
 ( ) Omnibox

オプションページから設定を変更できるようにするために Options Page のみチェックしました。

? Would you like to use permissions? (Press <space> to select, <a> to toggle all, <i> to invert selection)
>( ) ActiveTab
 ( ) Alarms
 ( ) Bookmarks
 ( ) BrowsingData
 ( ) BrowserSettings
 ( ) ContextMenus
 ( ) ContextualIdentities
(Move up and down to reveal more choices)

パーミッションは後から指定するので何も選択しませんでした。
変更は、app/manifest.json{"permissions": []}の配列内に指定します。

? Would you like to install promo images for the Chrome Web Store? (y/N) 

デフォルトのNを選択

これだけでひな型が完成します。以下のようなファイルが作成されます。

html-minify-clipboard
  + app
     + _locales
         + en
             messages.json
     + images
         icon-16.png
         icon-128.png
     + pages
         options.html
     + scripts
         background.js
         options.js
     + styles
         options.css
       manifest.json
  + node_modules
    .gitignore
    package-lock.json
    package.json
    README.md

上記のうち、開発が必要なファイルを紹介します。

app/_locales/en/messages.json

拡張機能名など編集できます。公開するときに書き換えましょう。

app/pages/options.html

オプションページを設定します。

app/scripts/background.js
app/scripts/options.js

拡張機能で使用するスクリプトです。
app/scriptsjsファイルを格納しておくと、webpackによりビルドされ自動的にコピーされます。
background.js に拡張機能で動かしたいコードを記述します。
options.js にはオプションページ内で動かしたいコードを記述します。

app/manifest.json

拡張機能で使用するリソースやパーミッションを編集します。

開発

このひな型を元に開発していきます。HTML Minify Clipboard

  1. 選択中の文字列の上で右クリックしてメニューを開く
  2. メニュー内のMinify the selected HTML codesをクリックする
  3. 選択中の文字列をHTML Minifierを使って圧縮する
  4. 処理した文字列をクリップボードにコピーしてdoneと表示されるアラートを出す

ということを行っています。

これらをどのように実装しているか説明していきます。

まずapp/script/background.jsで選択中の文字列の上で表示するメニューにMinify the selected HTML codes項目を出すようにします。

app/script/background.js
browser.contextMenus.create({
    title : "Minify the selected HTML codes",
    type : "normal",
    contexts : ["selection"],
    onclick : function(){
        saveToClipboard();
    }
});

browser.contextMenus.createでメニューに項目を追加できます。
contexts : ["selection"]で文字列が選択中の時に表示できるようにしています。
onclickでクリックしたときに saveToClipboard() を呼び出すようにしています。

async function saveToClipboard() {
    const tabId = browser.tabs.getCurrent().id;
    let target = "failed", selected = "", results, code;
    try {
        code = "typeof window.minify === 'function';";
        results = await browser.tabs.executeScript(tabId, {code});
        
        if (!results || results[0] !== true) {
            const file = 'scripts/htmlminifier.js';
            await browser.tabs.executeScript(tabId, {file});
        }

        code = `window.getSelection().toString();`;
        results = await browser.tabs.executeScript(tabId, {code});
        selected = results[0];
        
        results = await browser.storage.sync.get(defaults);

        let params = "{";
        for(const label of Object.keys(results)) {
            params += "\""+label+"\":"+results[label].toString()+",";
        }
        params += "}";
        selected = selected.replace(/\`/mg, "\\`");
        code = "window.minify(`"+selected+"`,"+params+");"
        results = await browser.tabs.executeScript(tabId, {code});
        
        if (results && results[0] && results[0] !== true) {
            target = results[0];
        }
        execCommandCopy(target);
        code = "alert(\"done\");";
        browser.tabs.executeScript(tabId, {code});
    } catch(error) {
        console.error('Failed to copy text: ' + error);
        execCommandCopy(target);
        code = "alert(\""+target+"\");";
        browser.tabs.executeScript(tabId, {code});
    }
}

browser.tabs.executeScript でWebページ上でJavaScriptを実行できます。
例えば

code = `window.getSelection().toString();`;
results = await browser.tabs.executeScript(tabId, {code});
selected = results[0];

で、Webページ内の選択中の文字列を取得することができます。
外部のJSライブラリは

const file = 'scripts/htmlminifier.js';
await browser.tabs.executeScript(tabId, {file});

あらかじめapp/scripts/htmlminifier.jsをコピーしておき、上記のように呼び出すことができます。
webpack でビルドされるので、async/awaitが使えてコードがすっきり書けました。

オプションページは以下のように作成しました。

app/pages/options.html
<!DOCTYPE html>
<html>
<head><title>Options</title></head>
<body>
<hr>
<div id="options"></div>
<div id="status"></div>
<button id="save">Save</button>
<script src="../scripts/options.js"></script>
</body>
</html>

<div id="options"></div>app/scripts/options.jsのコードで設定項目を追加して<button id="save">Save</button>で設定を保存します。

app/scripts/options.js
function restore_options() {
    var defaults = {};
    for (var label of Object.keys(options)) {
        defaults[label] = options[label].default;
    }
    var browser = browser || chrome;
    browser.storage.sync.get(defaults, function(items) {
        var div = document.getElementById('options');
        for (var label of Object.keys(options)) {
            var input = document.createElement("input");
            input.type = "checkbox";
            input.id = label
            input.checked = items[label];
            div.appendChild(input);
            var elem = document.createElement("label");
            elem.innerHTML = "<b>"+label+"</b>";
            div.appendChild(elem);
            div.appendChild(document.createElement("br"));
            var span = document.createElement("span");
            span.innerHTML = options[label].description;
            div.appendChild(span);
            var hr = document.createElement("hr");
            div.appendChild(hr);
        }
    });
}

function save_options() {
    var opts = {}
    for (var label of Object.keys(options)) {
        opts[label] = document.getElementById(label).checked;
    }
    var browser = browser || chrome;
    browser.storage.sync.set(opts, function() {
        var status = document.getElementById('status');
        status.textContent = 'Options saved.';
        setTimeout(function() {
            status.textContent = '';
        }, 750);
    });
}

document.addEventListener('DOMContentLoaded', restore_options);
document.getElementById('save').addEventListener('click', save_options);

restore_options()div#options に設定項目をロードします。
saveボタンを押すとsave_options()が呼ばれます。

オプションページ

これまでのコードを動かすにはapp/manifest.jsonにパーミッションを設定する必要があります。

app/manifest.json
{
  "name": "__MSG_appName__",
  "short_name": "__MSG_appShortName__",
  "description": "__MSG_appDescription__",
  ...
  "permissions": [
    "clipboardRead",
    "clipboardWrite",
    "activeTab",
    "storage",
    "contextMenus",
    "tabs",
    "<all_urls>"
  ]
}

大体このような感じで実装しています。
実際のコードは以下にあります。上記では一部省略しているコードもあるので、詳しくは以下を参照してください。
https://github.com/uedayou/html-minify-clipboard

ビルド

実装できたら後はビルドするだけです。

Chrome拡張機能用ビルドコマンド

$ npm run build chrome

Firefoxアドオン用ビルドコマンド

$ npm run build firefox

ビルドするとdistディレクトリにchromefirefoxディレクトリが、また、packagesディレクトリに以下のような zip ファイルが作成されます。

html-minify-clipboard.v1.0.1.chrome.zip

動作確認

Chromeで動作確認をしてみます。
Chrome でchrome://extensionsを開きます。

ディベロッパーモードを有効にすると、パッケージ化されていない拡張機能を読み込むというボタンが表示されるので、それをクリックします。
先ほどビルドしてできたchromeディレクトリを指定すると、拡張機能がChrome上に読み込まれると思います。

これで、通常の拡張機能と同じように動かすことができます。

拡張機能のサンプルコード

この記事では、選択中のテキストを処理する拡張機能の作り方を紹介しました。
そのほかのさまざまな機能のサンプルコードは以下のリポジトリが充実しているので参考にしてみてください。
Generator: WebExtensionでひな型を作って、サンプルコードをいろいろ組み合わせるだけでも簡単に様々なことができると思います。

webextensions-examples
https://github.com/mdn/webextensions-examples