独自のブラウザー拡張機能を作成する: 第 4 回、ブラウザーに共通の拡張機能を作成する

Chrome、Firefox、Safari の拡張機能において、プロセッサーにとっての余計な負荷と冗長な部分をなくす

どのブラウザーにも、それぞれに固有の長所と短所があり、支持する人もいれば、批判する人もいます。そのなかで、すべてのブラウザーに共通して言えることは、人々がブラウザーで費やす時間がますます増えているということです。この連載では、Chrome、Firefox、および Safari のベーシックな拡張機能を作成する方法を調べていきます。それによって、それぞれのブラウザーを拡張する作業とはどのようなものなのか、また共通して必要となる作業の難易度はどの程度なのか、さらには拡張機能をどのように配布するのか、といったことを学びます。連載最終回となる今回の記事では、3 つすべてのブラウザーで共有できる共通の拡張機能を作成します。

Duane O'Brien, Software developer, Freelance

Ảnh của Duane O'BrienDuane O'Brien は、へとへとに疲れたコンピューター・サイエンティストです。これまで、Web アプリケーションの開発やさまざまな PHP フレームワークに関する多数の記事を書いています。Duane についての詳細は、彼のブログツイートを調べてください。


developerWorks 貢献著者レベル

2013年 8月 01日

誰か他にこの問題を解決した人はいませんか?

Crossrider と Kango Framework (「参考文献」を参照) は、どちらもクロスブラウザーの拡張機能を作成するのに役立ちますが、それぞれ制約があります。Crossrider では、Safari に対するサポートが、Firefox や Chrome に対するサポートに比べて十分ではありません。Kango Framework では、主要なすべてのブラウザー (Internet Explorer と Opera を含む) の拡張機能を作成できると謳っていますが、プロジェクトがオープンソースでない場合にはライセンス料金を支払う必要があります。さらに、どちらのフレームワークにも共通する問題があります。それは、どちらの場合もユーザーはブラウザーを間接的に操作することから、ユーザーが扱えるのはフレームワークが提供する API のみに限定され、ユーザーにとってほとんど、あるいはまったく制御の効かない層が追加されることになるという点です。いずれのフレームワークも問題に対する最終的なソリューションとなる可能性はありますが、まずは自分で問題を解決しようとすることが自分のためになります。この解決の試みは、学習のための演習と考えてください。

これまでのところ、この連載で皆さんは Chrome、Firefox、Safari のそれぞれを対象にした Gawkblocker という名前の拡張機能を作成しました。Gawkblocker では、例えば表示に時間のかかるブログなど、ユーザーがアクセスしたくない特定のドメインをブロックすることができます。最初に Chrome 用の Gawkblocker を作成し、これを Firefox 用に微調整した後、今度は Safari で使用できるように再び微調整しました。けれども、このように 3 つの同じようなファイル一式を保守して、3 つのブラウザーのそれぞれに対して拡張機能を最新の状態に維持するとなると、余分な作業が生じ、コードも重複してしまいます。

連載最終回となる今回の記事では、このような重複したコードと余分な作業をなくすために、共通の Gawkblocker コード・ベースを作成するという目標に可能な限り近づきます (完全なソース・コードを入手するには、「ダウンロード」を参照してください)。コアの JavaScript ファイルや HTML テンプレートなど、簡単に共有できるものはいくつかあります。一方、これまでの記事を読んでおわかりのように、ストレージ・メカニズムはブラウザーによって異なり、それぞれのブラウザーに URL の変更を追跡するための固有の API があります。この記事を読み終えると、以下の質問に回答できるはずです。

  • どの程度まで、ブラウザーに共通の拡張機能にすることができるのか?
  • 作業が厄介というよりも、むしろ作業の価値があるように思える方法で、ブラウザーに共通の拡張機能にすることができるのか?

始める前に

この連載について

この全 4 回からなる連載では、3 つのブラウザー (Chrome、Firefox、Safari) を対象に Gawkblocker と名付けた拡張機能を作成します。

  • 第 1 回では、Google Chrome の拡張機能を作成するところから、Chrome ウェブストアに配置するまでのプロセスを説明しています。
  • 第 2 回では、Mozilla Firefox 用のアドオン (拡張機能) を作成します。
  • 第 3 回では、この拡張機能を Safari ブラウザー用に調整します。
  • 今回の記事では、作成したコードを特定のブラウザーに依存しないように微調整します。

この記事に取りかかる前に、連載のこれまでの 3 回の記事を読んで手順を完了してください。今回の記事は、Chrome 23 (カナリア・ビルド)、Firefox 16 (ベータ・チャネル)、および Safari 6.0 を対象に作成されています (「参考文献」を参照)。Safari は Apple 以外のオペレーティング・システムでは使用できないため、Safari で作業する場合には Mac を使用してください。また、HTML、CSS、および JavaScript を編集するためのツールも必要です。これまでの記事の手順を完了せずに、どうしてもこの記事から始めたいという場合には、これまでの記事で概説している、各ブラウザーの拡張機能を作成するためのセットアップの部分については、必ず目を通してください。

参考になるドキュメントは、Chrome 拡張機能のドキュメント、Firefox Add-on SDK に同梱されているドキュメント、Safari Extensions Reference ドキュメントです (「参考文献」を参照)。この記事には、読者の皆さんが既に理解している内容が凝縮されているため、これらのドキュメントを何度も参照することはないと思いますが、念のためドキュメントを開いておくとよいかもしれません。


Gawkblocker の構成内容

Chrome では、Gawkblocker は以下のものを使用します。

  • アプリケーションを管理するための背景ページ
  • ロジックのほとんどが含まれる JavaScript ファイル
  • ユーザーにブロック対象のサイトを表示するポップアップ・ページ
  • ブロック対象のサイトを管理するためのオプション選択ページ
  • ユーザーのリダイレクト先となるランディング・ページ

図 1 に、ポップアップ・ページとオプション選択ページを示します。

図 1. Chrome でのポップアップ・ページとオプション選択ページ
Chrome でのポップアップ・ページとオプション選択ページのスクリーン・キャプチャー

Firefox では、オプション選択ページとポップアップ・ページを 1 つのページに統合し、Firefox ストレージ API を使用するように JavaScript ファイルを変更して、main.js ファイルからアプリケーションを管理しました。Firefox の場合、ユーザーは直接 YouTube にリダイレクトされます。図 2 に、ポップアップ・ページとオプション選択ページを統合した Firefox バージョンのページを示します。

図 2. Firefox でのポップアップ/オプション選択統合ページ
Firefox でのポップアップ/オプション選択統合ページのスクリーン・キャプチャー

Safari で使用したのは、ポップアップ/オプション選択統合ページ、アプリケーションを管理するための背景ページ、そして Chrome で使用したのと同じ JavaScript ファイルです。図 3 に、Safariでのポップアップ/オプション選択統合ページを示します。

図 3. Safari でのポップアップ/オプション選択統合ページ
Safari でのポップアップ/オプション選択統合ページのスクリーン・キャプチャー

ここから先は、ポップアップ/オプション選択統合ページを使用します。この後すぐに、ブラウザーごとに異なる main.js と背景ページが引き続き必要になるのかどうかがわかります。


ブラウザー固有のコードの抽象化

ブラウザー固有のコードを抽象化する作業は、簡単な 2 ヶ所から始めることができます。それは、ポップアップ・コードとコアの JavaScript ファイルです。最初に、ポップアップ・コードに対処します。

図 4 に、並べて表示された Firefox 拡張機能と Safari 機能拡張のポップアップ・ページのコードを示します。

図 4. Firefox と Safari のポップアップ・ページのコードの比較
並べて表示された Firefox 拡張機能と Safari 機能拡張のポップアップ・ページのコードのスクリーン・キャプチャー

この 2 つの間にある重要な違いは、背景ページから GB オブジェクトにアクセスする方法です。Safari では (そして表示されていませんが、Chrome でも)、ポップアップ・コードから直接背景ページを呼び出すことができます。一方、Firefox では、ポップアップ・コードから main.js ファイルにメッセージを送信しなければなりません。さらに、ポップアップ・ページを開くときに実行する初期化についても、ブラウザーごとに多少の異なる処理が必要です。したがって、初期化と GB オブジェクトの呼び出しを外部化すれば、1 つのポップアップ・コードを共通して使用できるようになります。

ブラウザー固有のアクションを処理する BA というオブジェクトを作成して、そのオブジェクトを BA.js に含めます。BA オブジェクト内では、各ブラウザーを、そのブラウザーで実行する必要があるアクションにマッピングします。また、StorageManager オブジェクト (SM) を、このオブジェクト専用のファイルに移します。以上の変更により、ポップアップ・ページの JavaScript コードはリスト 1 のようになります。

リスト 1. ポップアップ・ページの JavaScript コード
<script src="BA.js"></script>
<script src="SM.js"></script>
<script src="GB.js"></script>
<script>
    $(document).ready(function () {
        BA.handle.popup();
        ...
        showBlockList(GB.getBlockedSites());
    });
</script>

BA オブジェクト内に、ブラウザーとブラウザー固有のアクションのマップを定義します。ここで確実にわかることは、BA オブジェクトはポップアップ・ページをセットアップし、背景ページをセットアップし、ブラウザーのリクエストをインターセプトする必要があることです (リスト 2 を参照)。

リスト 2. BA オブジェクトのコード
actionMap = {
    'safari' : {
        'popup' : function () {
        },
        'background' : function (callback) {
        },
        'intercept' : function (candidate, site, watchthis) {
        }
    },
    'chrome' : {
    ...
}

BA オブジェクトには、現在どのブラウザーの中で使用されているのかを判別させる必要もあります。それには、API を使用可能であるかをテストします。使用されているブラウザーが Safari であれば safari API、Chrome であれば chrome API、Firefox であれば (現在 BA オブジェクトが main.js またはポップアップ・コードのどちらで使用されているかによって) require API または addon API が使用されます。このモジュールを作成するときにはテストを行い、使用する段階になったときにモジュールが使用可能な状態であるようにしてください。リスト 3 に、API を使用可能であるかのテストを行うコードを記載します。

リスト 3. Firefox、Safari、および Chrome の API を使用可能であるかのテスト
    if (typeof safari !== 'undefined') {
        console.log("Safari");
        // Reach the global page using safari.extension.globalPage.contentWindow
        my.handle = actionMap.safari;
    } else if (typeof chrome !== 'undefined') {
        console.log("Chrome");
        // Reach the global page using chrome.extension.getBackgroundPage()
        my.handle = actionMap.chrome;
    } else if (typeof require !== 'undefined') {
        console.log("Firefox - background");
        // Listen for a message using addon.port.on(messagename, callback);
        // Send a message using addon.port.emit(messagename, message); 
        my.handle = actionMap.firefox;
    } else if (typeof addon !== 'undefined') {
        console.log("Firefox - popup");
        // Listen for a message using addon.port.on(messagename, callback);
        // Send a message using addon.port.emit(messagename, message); 
        my.handle = actionMap.firefox;
    }

Safari で BA.handle.popup() を呼び出すときには、BA.handle プロパティーがあらかじめ actionMap.safari 内のオブジェクトに設定されています。これは、この特定のブラウザーに必要な popup() セットアップ関数を格納するオブジェクトです。

background.html ページと、拡張機能に使用する main.js ファイルでは、同様のことを行います。背景ページを (リスナーをアタッチするなどして) セットアップするために BA.handle.background() を呼び出し、この関数に、サイトをブロックするかどうかを判別する関数を渡します。JavaScript の background.html ページは、リスト 4 のような内容になります。

リスト 4. JavaScript の background.html ページ
<script src="BA.js"></script>
<script src="SM.js"></script>
<script src="GB.js"></script>
<script>
    $(document).ready(function () {
        ...
        function shouldIBlockThis(candidate) {
            var site,
                blockedSites = GB.getBlockedSites();
            for (site in blockedSites) {
                if (blockedSites.hasOwnProperty(site)) {
                    BA.handle.intercept(candidate, site, GB.getWatchThisInstead());
                }
            }
        }
        BA.handle.background(shouldIBlockThis);
    });
</script>

main.js のコードも同じような内容で、require を使用して BA モジュールと GB モジュールを取り込むだけに過ぎません。取り込まれた GB モジュールは SM モジュールをインポートするので、main.js 内には SM モジュールが必要なくなります。


既存のモジュールの微調整

BASMGB の各モジュールが、拡張機能を使用する特定のブラウザーに依存しないようにするために、これらのモジュールをほんの少し微調整します。具体的に言うと、ブラウザーが Firefox の場合には、モジュールのエクスポートと読み込みが必要です。したがって、exportsrequire の有無をチェックするようにすることで、これらを条件付きで使用するようにします。GB モジュール内では、まずリスト 5 に記載するコードで SM モジュールをインポートします。

リスト 5. SM モジュールのインポート
if (typeof require !== 'undefined') {
    var SM = require("SM").SM;
}

//And you end with this, to export the GB module

if (typeof exports !== 'undefined') {
    exports.GB = GB;
}

問題は、この SM モジュールです。Firefox では拡張機能の内部から localStorage にアクセスすることはできないため、第 2 回では simple-storage を使用したことを思い出してください。BA モジュールには、このストレージのハンドラーを追加することができます。こうすることは、理にかなっています。さらに、SM オブジェクト内にもチェック (localStorage の有無のチェック) を追加することができます。このようにして、localStorage が使用可能である場合は、新しいバージョンの Firefox でこのストレージをサポートできると同時に、古いバージョンの Firefox に対するサポートを維持することができます。リスト 6 に、この次善策を記載します。

リスト 6. Firefox 用の次善策
    if (typeof localStorage !== 'undefined') {
        // Currently supported in Chrome and Safari

        my.get = function (key) {
            return localStorage.getItem(key);
        };
        my.put = function (key, value) {
            return localStorage.setItem(key, value);
        };
        my.remove = function (key) {
            return localStorage.removeItem(key);
        };
    } else if (typeof require !== 'undefined') {
        // This is Firefox.  You call require and use simple-storage
        SS = require("simple-storage");
        console.log("SimpleStorage");
    
        my.get = function (key) {
            return SS.storage[key];
        };
        ...
    }

この次善策は、SM モジュールを他のプロジェクトに再利用することも可能にします。この構造を配置した後、ブラウザー固有のコードを含めることができます。


ブラウザー固有の仕上げ

ポップアップ・ページは、ブラウザーによって多少の違いがあります。Safari の場合、ポップアップ・ページの表示をリセットできるように、ページが開くタイミングを知る必要があります (リスト 7 を参照)。

リスト 7. Safari のポップアップ・ページ
'popup' : function () {
    // Safari pop-up pages retain state. Always swap the options and list divs  
to default when the pop-up page closes.
    safari.application.addEventListener("popover", function () {
        $("#onestep").show();
        $("#options").hide();
    }, true);
}

嬉しいことに、Chrome に必要な追加の作業はありません。

Firefox については、ページが開くタイミングで port メッセージをセットアップして表示をリセットする必要があります (リスト 8 を参照)。

リスト 8. Firefox のポップアップ・ページ
'popup' : function () {
    // Firefox pop-up pages retain state. Always swap the options and list divs  
to default when the pop-up page closes.
    addon.port.on("popshow", function () {
        $("#onestep").show();
        $("#options").hide();
    });
    // Firefox pop-up pages cannot get the GB object directly, so you pass messages.
    addon.port.emit("pop");
    $("#watchthis").click(function () {
        addon.port.emit("watchthis");
    });
    $("#makethathappen").click(function () {
        addon.port.emit("makethathappen", $("#watchthatinstead").val());
    });
    $("#blockthistoo").click(function () {
        addon.port.emit("dontgothere", $("#dontgothere").val());
    });
    addon.port.on("blocklist", function (blocklist) {
        showBlockList(blocklist);
    });
    addon.port.on("watchthatinstead", function (instead) {
        $("#watchthatinstead").val(instead);
    });
}

背景ページと main.js にも、同じような処理を行います。Safari には、beforeNavigate イベント・リスナーを追加します (リスト 9 を参照)。

リスト 9. Safari での beforeNavigate イベント・リスナーの追加
'background' : function (callback) {
    safari.application.addEventListener("beforeNavigate", callback, true);
}

Chrome には、タブ・リスナーを追加します (リスト 10 を参照)。

リスト 10. Chrome 用のタブ・リスナーの追加
'background' : function (callback) {
    chrome.tabs.onUpdated.addListener(function (tabId, changedInfo, tab) {
        callback(tab);
    });
    chrome.tabs.onCreated.addListener(function (tab) {
        callback(tab);
    });
}

Firefox については、パネルとウィジェットを作成して port 通信の残りの半分をセットアップし、該当するモジュールを取り込みます (リスト 11 を参照)。

リスト 11. Firefox でのパネルとウィジェットの作成
'background' : function (callback) {
    var data = require("self").data,
        tabs = require("tabs"),

        GB = require("GB").GB,
        popupPanel = require("panel").Panel({
            height: 500,
            contentURL: data.url("popup.html"),
            onShow : function () {
                this.port.emit("popshow", true);
            }
        });
    tabs.on("ready", function (tab) {
        callback(tab);
    });
    require("widget").Widget({
        id: "GBBrowserAction",
        label: "Gawkblocker",
        contentURL: data.url("GB-19.png"),
        panel: popupPanel
    });
    popupPanel.port.on("pop", function () {
        popupPanel.port.emit("blocklist", GB.getBlockedSites());
        popupPanel.port.emit("watchthatinstead", GB.getWatchThisInstead());
    });
    ...
}

次に対処するのは、intercept ハンドラー (サイトをブロックするかどうかを調べるために実行されるコード) です。ブラウザーごとにハンドラーの動作は少し異なりますが、いずれも同じように構造化されています。タブまたはイベント・オブジェクトは intercept ハンドラーに、チェック対象のサイトと、サイトがブロックされている場合にユーザーのリダイレクト先とするランディング・ページを渡します。すると、intercept ハンドラーが特定のブラウザーに必要なアクションを実行します。Safari の場合、そのコードはリスト 12 のようになります。

リスト 12. Safari での intercept ハンドラー
'intercept' : function (candidate, site, watchthis) {
    if (candidate.url && candidate.url.match(site)) {
        candidate.preventDefault();
        candidate.target.url = watchthis;
    }
}

Safari と Chrome については、これで作業は完了です。共通ファイルを投入すれば、これらのファイルが Safari と Chrome の両方に機能します。


難題

Firefox については、作業はまだ完了ではありません。この例の場合、Chrome と Safari でのポップアップ・ページと背景ページは両方とも localStorage 内の同じ領域にアクセスできることから、この 2 つのブラウザーに対処するのは簡単です。その一方、Firefox では、ポップアップ・コード内から simple-storage オブジェクトまたは main.js コンテキストのオブジェクトにアクセスすることはできません。メッセージ・パッシングを使用しているのは、すべてこれが理由です。ポップアップ・ページ内のブロック対象サイトのリストを生成する際に、Firefox ではメッセージを main.js に渡すための click ハンドラーが各サイトに追加で必要になります。その追加ハンドラーは、これらの click ハンドラーを作成するブロック内に追加することができます (リスト 13 を参照)。

リスト 13. Firefox での click ハンドラーの追加
$("#unblock-" + i).click(function () {
    GB.removeBlockedSite(index);
    BA.handle.removeSite(index);
    showBlockList(GB.getBlockedSites());
});

次に、BA モジュール内の actionMap.firefox オブジェクトに removeSite メソッドを追加します (リスト 14 を参照)。

リスト 14. removeSite メソッドの追加
'removeSite' : function (index) {
    addon.port.emit("unblock", index);
}

このメソッドを追加したことで警告が発生するようになる場合は、これよりも有効な方法を探してください。拡張機能の要件によっては、コードが例外的であるかのように思えるかもしれません。その場合には、一歩離れて自分が何を行っているのかを再評価してください。

さらに悪いことに、Firefox の場合は困った状況に陥ります。ポップアップ・ページは BASMGB の各モジュールを組み込むものの、実際に使用するデータはすべて main.js ファイルから提供され、このファイルにも BASMGB の各モジュールが必要となります。しかしポップアップ・ページと main.js ファイルがこれらのモジュールを共有することはできないため、Firefox 拡張機能の場合は同じモジュールを 2 つずつ用意することになります (図 5 を参照)。

図 5. 不本意な 2 つのモジュール・セット
BA、SM、BG の各モジュールが重複していることを示す Add-on Builder のスクリーン・キャプチャー

同じモジュールを複数用意するのは、最善の方法ではありません。モジュールを共有したり、モジュールを受け渡したりすることも場合によってはできるかもしれませんが、そのための作業は度を超しているように思えるかもしれません。この難題が、拡張機能をブラウザーで共通にするという取り組みそのものを行う価値に疑問を投げかけます。


まとめ

現時点で、この記事の冒頭に挙げた質問に回答できるはずです。

  • どの程度まで、ブラウザーに共通の拡張機能にすることができるのか?これまでの手順で明らかなように、かなりのレベルまでできます。コアの JavaScript クラスとポップアップ・コードは、ブラウザーに依存しません。背景ページの JavaScript は、main.js とほぼ同一で、その他のモジュールはどのブラウザーでも機能します。
  • 作業が厄介というよりも、むしろ作業の価値があるように思える方法で、ブラウザーに共通の拡張機能にすることができるのか?その可能性はあります。GB モジュールは簡単でわかりやすく、BA モジュールはブラウザーに関する処理のほとんどを行うことができます。ただし、この記事の例では、Firefox と Chrome/Safari との間での拡張機能の違いに対処しなければなりませんでした。

Firefox 拡張機能のポップアップ・コードと main.js にオブジェクトを共有させたり、少なくとも同じストレージ域にアクセスさせたりすることには、かなりの魅力があります。それによって、メッセージ・パッシングのほぼすべてを取り除くことができるからです。特定の 1 つの問題にこだわり過ぎないでください。RequireJS や他の CommonJS スタイルのローダーのようなものを使用すれば、3 つすべてのブラウザーを同じように扱える可能性があります。また、BA モジュールを各ブラウザーに対応する 3 つのモジュールに置き換えて、ブラウザーごとに個別のコードにするという方法もあります。


ダウンロード

内容ファイル名サイズ
Gawkblocker source codesourcecode.zip6KB

参考文献

学ぶために

製品や技術を入手するために

  • Mozilla Firefox: お使いのプラットフォームに対応した Firefox をダウンロードしてください。
  • Firefox Add-on SDK: SDK をダウンロードしてください。
  • Firefox Add-on Builder: ここから Add-on Builder にアクセスしてください。
  • Gawkblocker: 著者の Add-on Builder プロファイルから Firefox 向けの Gawkblocker をダウンロードしてください。
  • Chrome Developer Tools: The Chromium Projects の Dev channel から Google Chrome Release を利用して Developer Tools の最新バージョンを入手してください。
  • Kango フレームワーク: JavaScript 拡張機能を作成するためのフレームワークを調べてください。
  • Crossrider: このフレームワークを使用して、クロスブラウザーの拡張機能を作成してください。
  • IBM 製品の評価をご自分に最適な方法で行ってください。評価の方法としては、製品の評価版をダウンロードすることも、オンラインで製品を試してみることも、クラウド環境で製品を使用することもできます。また、SOA Sandbox では、数時間でサービス指向アーキテクチャーの実装方法を効率的に学ぶことができます。

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Open source, Web development
ArticleID=938396
ArticleTitle=独自のブラウザー拡張機能を作成する: 第 4 回、ブラウザーに共通の拡張機能を作成する
publish-date=08012013