続・先取り! Google Chrome Extensions

第4回ベータ版に向けたExtension総復習

こんにちは、株式会社ALBERTの太田です。Extensionsが有効になるベータ版のリリースが近づいてきました。そこで今回は、現時点(2009年11月23日)でのExtensionsの作り方をまとめてみます。

Extensionsの開発の下準備

まずは開発環境を整理します。といっても、基本的にHTML/CSS/JavaScriptで作成するので最低限エディタがあればなんとかなります。

ただ、実際に開発/テスト/リリースをする場合には、プロファイルの異なる複数のChromeを起動できると何かと便利です。そういった場合、起動オプションで--user-data-dirを指定することで同時起動ができますUser Data Directory - Custom-Location⁠。

プロファイル ディレクトリの指定
[パス省略]\chrome.exe --user-data-dir="C:\chrome_profile\test1"

このようにして、ディレクトリを分けることで複数のプロファイルで開発・テストが行えます。

また、⁠Chromeではなく)Chromiumの最新のスナップショットも利用すれば異なるバージョンを試すこともできます(今のところ、複数バージョンのChromeを同時にインストールする方法は用意されていないようです⁠⁠。Chromiumのスナップショットにはsnapshots版continuous版があり、continuous版のほうが自動テストを通っていて、最新版のファイルに固定のURLでアクセスできるのでお勧めです。Windowsであれば、mini_installer.exeを実行すればChromiumが最新版に更新されます。バッチでアップデートするようにしておくとよいでしょう。

起動オプションは不要に

Extensionsを開発/利用するのに以前は --enable-extensionというオプションが必要でしたが、現在は不要となっています。そのほかにもデータベース、ストレージ、Cookieの開発ツールを使用するのに以前は --enable-databasesオプションが必要でしたが、⁠2009年11月23日時点のDev版では)デフォルトで有効になっています。

開発関連以外でも、HTML5関連のリモートフォント(--enable-remote-fonts⁠⁠、WebSockets(--enable-web-sockets⁠⁠、WebDatabase(--enable-databases⁠⁠、WebStorage(--enable-local-storage)などは(2009年11月23日時点のdev版では)どれもデフォルトで有効になっており、各オプションを指定する必要はありません(ただし、sessionStorageのみ現在は無効のままとなっています⁠⁠。また、Toolstriptsの廃止に伴い、Extensionsバーを上に表示する --show-extensions-on-topなども廃止予定です。

Extensionsのひな形とmanifest

まず、Extensionsに最低限必要なのはmanifest.jsonというJSONファイルと、HTML/CSS/JavaScriptファイルのどれか1つ以上です。それ以外は、使用するAPIによって必要なファイルが変わり、manifest.jsonの記述内容も同様に変わります。サンプルとしてAPIを一通り使用する場合のmanifest.jsonを見てみます。

全部載せのmanifest.json
{
  "name": "sample",
  "description": "Google Chrome Sample Extension",
  "version": "0.0.0.1",
  "permissions": [
    "http://*/*",
    "https://*/*",
    "tabs"
  ],
  "update_url": "http://example.com/updates.xml",
  "options_page": "options_page.html",
  "background_page": "background.html",
  "chrome_url_overrides": {
    "newtab": "newtab.html"
  },
  "icons": {
    "16": "icon16.png",
    "32": "icon32.png",
    "64": "icon64.png",
    "128": "icon128.png"
  },
  "content_scripts": [
    {
      "js": [
        "sample.js"
      ],
      "css": [
        "sample.css"
      ],
      "matches": [
        "http://*/*"
      ]
    }
  ],
  "page_action": {
    "default_icon": "icon16.png",
    "default_title": "Sample",
    "popup": "popup.html"
  }
}

nameとdescriptionはそのままExtensionの名前と説明です。versionはドット区切りの数字で、ドットは3つまでです。数字は左から順に比較され、大きいバージョンと判定されれば新しいバージョンがインストールされます。例えば、1.1.Xは、Xの値がいくつであっても1.2をアップデートすることはできません。またバージョンがまったく同じ場合もアップデートされません。

permissionsは拡張のアクセス権限を決定します。クロスオリジン通信を行う場合はアクセス先のオリジンを、タブ/ウィンドウ関連のAPIを使用する場合にはtabsの指定、動的にContent Scriptsを挿入する場合(executeScriptやinsertCSSなどを使用する場合)には挿入対象となるページのオリジンを指定します。なお、セキュリティ/メンテナンスの問題からchromeで始まるオリジンを指定することはできません(Extensionsギャラリーが公開される予定のchrome.google.comも特別に指定できません。Issue 28228⁠。

update_urlは自動アップデートを行う場合にアップデート情報を書いたXMLのURLを記述します。XMLの中身はUpdate manifestの通りで、appid(拡張のID)とcodebase(crxファイルの場所)にversion(最新のバージョン)を記述します。

options_page、background_page、chrome_url_overridesはそれぞれOptions PageBackground PagesOverride Pagesを使用する場合に記述し、対応するHTMLファイルを用意します。当然ですが、HTMLファイルはCSSやJavaScriptを外部ファイルに分けることができ、jQueryなどのライブラリをそのまま利用できます。

iconsはインストール時や拡張の一覧ページなどで使用されるアイコンを指定できます。サイズは16px、32px、48px、128pxの4つを記述でき(省略も可能です⁠⁠、画像フォーマットはWebKitがサポートするものであれば使用できます。

content_scriptsは読み込まれたページのURLとmatchする条件を指定し、その際に適用するJavaScriptまたはCSSを指定します。条件は複数記述でき、適用するファイルも複数指定可能です。

page_action、browser_actionは、アドレスバー(Omnibox)やその右隣(ツールバー)に表示するアイコンを指定します。page_actionとbrowser_actionはどちらか1つしか指定できません。1つのExtensionは1つのアイコン領域しか持つことができません。そのアイコンには画像を使用しますが、Canvasで描いたImageDataを使用することも可能なので動的に生成することも可能です。popupでアイコンをクリックした際にhtmlを表示することもできます。

manifest.jsonの内容は以上ですが、このほかにもmanifest.jsonに記述しないHTMLファイルをBrowserActionなどから開いて、通常のタブに開いたページと同じように使用することも可能です。popupはすぐに閉じてしまいますので、Twitterクライアントなどはタブで開いたほうがよいかもしれません。

Extensionsの開発手順

では実際にmanifest.jsonを記述し、それに対応するファイルを用意していきます。今回もSBMカウンタをサンプルとします(そろそろ別のサンプルを出すべきところですが、今回だけお付き合いください⁠⁠。

SBMカウンタのmanifest.json
{
  "name": "SBM Counter",
  "description": "Social Bookmark Counter Extension",
  "version": "4.2.1",
  "permissions": [
    "tabs",
    "http://*/",
    "https://*/"
  ],
  "update_url": "http://ss-o.net/chrome_extension/sbm_counter/updates.xml",
  "options_page": "options_page.html",
  "background_page": "background.html",
  "icons": {
    "128": "sbm_icon_128.png",
    "64": "sbm_icon_64.png",
    "32": "sbm_icon_32.png",
    "16": "sbm_icon.png"
  },
  "browser_action": {
    "default_icon": "sbm_icon.png",
    "default_title": "SBM Counter",
    "popup" : "popup.html"
  }
}

Tabs APIとexecuteScriptをすべてのサイトで使用するので、permissionsはほぼフルアクセスの権限を与えています。

このmanifestに対応したHTML、画像、CSSやJavaScriptファイルを用意していきます。なお、前回はpage_actionを使用しましたが、今回はbrowser_actionを使用しています。また、複数のサービスをカウントするためにExtensionsを複数用意する方法を取りましたが、BrowserActionにはブックマーク数の合計数を表示して、Tooltipに各サービスごとのブックマーク数を、popupにブックマーク数と詳細ページ、ブックマーク追加ページへのリンクを表示することにします。

Background Page

では、最初にBackground PageのHTMLを用意します。

Background PageのHTML
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script type="text/javascript" src="Services.js"></script>
<script type="text/javascript" src="md5.js"></script>
<script type="text/javascript" src="background.js"></script>
</head>
<body>
</body>
</html>

DOCTYPEやhtml要素などを省略せずに書きましたが、JavaScriptを動かすだけなので、script要素だけに省略しても問題ありません。

scriptはサービスの定義を記述したServices.js、delicious用のMD5ライブラリ、Background Pageの本体となるbackground.jsの3つに分けました。

このbackground.jsはChromeが起動した際に一度だけ初期化されます。そこで、最初に起動時の初期化処理を記述します。

Background Pageでの設定の初期化
var SBMConf = {
  disable_sites:[
    "^http://localhost(:\\d+)?/",
    "^https://",
    "^http://b\\.hatena\\.ne\\.jp/entry"
  ],
  isInlineCount:true,
  usageServiceConfig:[
    {id:'hatena', enable:true},
    {id:'delicious', enable:true}
  ]
}
if (!localStorage.disable_sites) {
  localStorage.disable_sites = JSON.stringify(SBMConf.disable_sites);
} else {
  SBMConf.disable_sites = JSON.parse(localStorage.disable_sites);
}
if (!localStorage.isInlineCount) {
  localStorage.isInlineCount = Boolean(SBMConf.isInlineCount);
} else {
  SBMConf.isInlineCount = Boolean(localStorage.isInlineCount);
}
if (!localStorage.usageServiceConfig) {
  localStorage.usageServiceConfig = JSON.stringify(SBMConf.usageServiceConfig);
} else {
  SBMConf.usageServiceConfig = JSON.parse(localStorage.usageServiceConfig);
}
SBMConf.usageServiceConfig.forEach(function(opt){
  Services[opt.id].enable =  opt.enable;
});

最初のSBMConfはインストール直後用のデフォルトの設定です。localStorageに値が存在すればその値をJSON.parseで取り出してSBMConfに反映し、存在しなければ逆にJSON.stringifyでlocalStorageに値をセットしています。なお、isInlineCountは真偽値(プリミティブな値)なので、JSONを経由させていません。JSONを使ってもエラーにはならないようなのですが、本来JSONはプリミティブ値だけを取ることはできないので、念のため直接プリミティブ値として扱うようにしました。

最後のusageServiceConfigの処理は、サービスのON/OFFをServicesオブジェクトに反映させています。

SBMクラスの実装
function SBM(tab){
  if (tab) {
    this.init(tab);
  }
}
SBM.prototype = {
  init:function _sbm_init(tab){
    var tabid = tab.id, url = tab.url;
    this.count = 0;
    this.title = new Array(Services.length);
    this.tabid = tabid;
    this.title = tab.title;
    this.url = url.replace('#','%23');
    this.encoded_url = encodeURIComponent(url);
    Services.forEach(function(service, index){
      this.request(service, index);
    },this);
  },
  request:function _sbm_request(service, index){
    if (!service.enable) return;
    var self = this, xhr = new XMLHttpRequest();
    var api_url = fill(service.api_get, this);
    xhr.open('GET', api_url, true);
    xhr.onload = function(){
      var count = service.responceFilter(xhr.responseText);
      self.update(count,service,index);
    };
    xhr.send();
  },
  update:function _sbm_update(count, service, i){
    this.count += parseInt(count, 10) || 0;
    this.title[i] = count + ' count(' + service.name + ')';
    chrome.browserAction.setBadgeText({
      tabId:this.tabid,
      text:this.count
    });
    chrome.browserAction.setTitle({
      tabId:this.tabid,
      title:this.title.join('\n')
    });
  }
};

続いて、SBMクラスの実装です。tabオブジェクトを受け取り、サービスごとにリクエストを投げ、レスポンスが返ってきたらそれぞれカウントをBrowserActionのアイコンに反映しています。レスポンスは順番通りに返ってくるわけではないので、サービスが増えて順番がまちまちになっても正常に動作するようにcount、titleの初期化を工夫しています。BrowserActionもtabidを指定することでBadgeText、TitleをPageActionのようにタブごとに保持させることができます。

タブの更新の監視
chrome.tabs.onUpdated.addListener(function(tabid, inf){
  if (inf && inf.status === 'loading') {
    chrome.tabs.get(tabid, function(tab){
      if (!site_check(tab.url)) {
        new SBM(tab);
      }
    });
  }
});
function site_check(url){
  if (url.indexOf('http') !== 0 || url.length > 255) return true;
  return SBMConf.disable_sites.some(function(site){
    return new RegExp(site).test(url);
  });
}

仕上げにchrome.tabs.onUpdatedでタブの更新をチェックし、タブの読み込みが開始した際にURLが有効なものであればSBMクラスをnewしてインスタンスを作ります。これで、合計ブックマーク数の表示と、Tooltipにサービスごとのブックマーク数を表示することができました。

Options Page

オプションページは chrome://extensions/ のオプションボタンをクリックした際に開かれるページです。今のところ純粋なHTMLページで、特にほかのページと異なる部分はありません(OptionsPage用の共通CSSを適用する予定はあります⁠⁠。ほかのExtensionページと同様、chrome.extension.getBackgroundPageでバックグラウンドページとやり取りをすることができ、localStorageなどを使ってExtensionの設定をカスタマイズする際に使用します。OptionsPageのlocalStorageとBackgroundPageのlocalStorageは同じオブジェクトなので、OptionsPageでlocalStorageを書き換えれば即、BackgroundPageに反映されます。なお、localStorageの値はExtensionのIDをドメインに見立てて保存されます。

BrowserActionのPopup

Popupもやはり基本はHTMLですが、こちらはページ内容に応じて表示領域が動的に変化するようになっています。また、2009年11月24日時点のdev版では、popupに対してDevToolsを開くことができません。そこで、chrome-extension://extension_id/popup.htmlのようにpopupを直接タブに開いたり、BackgroundPageのconsole.logを呼び出してコンソール出力をするといった方法でデバッグする必要があります。

さて、popupでのブックマーク数の表示ですが、BackgroundPageのSBMクラスと処理は概ね同じなので、SBMクラスを継承します。

PopupでのSBMクラスの実装
var BackGround = chrome.extension.getBackgroundPage();
var log = BackGround.console;
var SBMConf = BackGround.SBMConf;
function PSBM(tab){
    if (tab) {
        this.init(tab);
    }
}
PSBM.prototype = new BackGround.SBM();
var _request = PSBM.prototype.request;
PSBM.prototype.request = function(service, index){
    if (service.enable) {
        _request.call(this, service, index);
    } else {
        document.querySelector('#sbm_' + service.id).style.display = 'none';
    }
};
PSBM.prototype.update = function(count, service, index){
    document.querySelector('#add_' + service.id).href = fill(service.api_add, this, service);
    var link = document.querySelector('#text_' + service.id);
    link.href = fill(service.api_link, this, service);
    link.textContent = count;
};

BackgroundPageのSBMクラスは引数に何も渡さなければinitメソッドを呼ばないようにしてあるので、init、request、updateの各メソッドだけを持ったインスタンスを作ります。そのインスタンスを新しいPSBMクラスのprototypeに入れると、PSBMはSBMクラスを継承したことになります。

PSBMクラスでは継承元のrequestの前に無効なサービスであればその表示を隠す処理を入れ、またupdate時はそのサービスごとに表示を更新するようにしました。

選択中のタブの取得
chrome.tabs.getSelected(null,function(tab){
  if (BackGround.site_check(tab.url)) {
    window.close();
    return;
  }
  new PSBM(tab, BackGround.Services);
});

後はpopupがクリックされた際のタブを取得して、PSBMクラスをnewしています。なお、⁠現状では)popupが開くことをキャンセルすることができないので、無効なURLでは即座にwindow.closeでpopupを閉じるようにしています。

まとめ

現在のSBMカウンタのすべてを説明したわけではありませんが、現時点でのExtensionsの作り方は説明できたのではないかと思います。FirefoxのAdd-onなどに比べれば機能不足は否めませんが、アイデア次第では面白いものが作れる土台はできてきています。正式リリースも着々と近づいてきていますので、この機会にExtensionsを作ってみてはいかがでしょうか?

次回はまだ紹介していないAPI(ドキュメントにないものも含めて)を幾つか紹介したいと思います。

おすすめ記事

記事・ニュース一覧