目次


独自のブラウザー拡張機能を作成する

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

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

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: 独自のブラウザー拡張機能を作成する

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:独自のブラウザー拡張機能を作成する

このシリーズの続きに乞うご期待。

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

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

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

始める前に

この記事に取りかかる前に、連載のこれまでの 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 でのポップアップ・ページとオプション選択ページのスクリーン・キャプチャー
Chrome でのポップアップ・ページとオプション選択ページのスクリーン・キャプチャー

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

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

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

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

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

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

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

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

図 4. Firefox と Safari のポップアップ・ページのコードの比較
並べて表示された 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 のスクリーン・キャプチャー
BA、SM、BG の各モジュールが重複していることを示す Add-on Builder のスクリーン・キャプチャー

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

まとめ

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

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

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


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

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