先取り! Google Chrome Extensions

第2回Chrome Extensionsの作り方#1

この記事で取り上げているAPIは現在と使い方が異なっていたり、使用できなくなったものを含んでいます。

特にToolstrips APIは最新のChromeでは使用できなくなっています。詳しくは続・先取り! Google Chrome Extensionsをご覧ください。

前回はChromeのバージョンの違いとExtensionsの導入と概要について説明しました。今回はExtensionsの作り方からドキュメント、開発ツールについて紹介します。なお、今回の解説はChrome 3系をベースとします。4系で変更された部分や機能追加があった部分は適宜補足を入れています。

【2009/9/17追記】本稿の執筆時点ではChrome 3でExtensionsを試すことができましたが、9月16日のChrome 3の正式リリース以降、Chrome 3系統ではExtensionsを有効にすることができないように仕様変更されています。ご了承ください。

最初のExtension

まず始めにHello worldをExtensionで実装してみます。Extensionの最小構成は manifest.json というExtensionの定義ファイルとhtmlファイル1つです。

manifest.json
{
   "name": "Hello World",
   "version": "1.0",
   "toolstrips": ["toolstrip.html"]
}

manifest.json の必須プロパティはnameとversionの2つです。nameは任意の名前を付けることができます。versionは . で区切った数値(1.1.2.12 のように . を3つまで使えるので、数値とは少し違います)である必要があります。

続いて、toolstrips はステータスバーのようなExtensions用の表示領域です。このtoolstripの中身となるHTMLファイルを用意します。

toolstrip.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div class="toolstrip-button">
    <span>Hello, World!</span>
</div>
</body>
</html>

toolstripには専用のCSSが自動的に挿入されるので、今回はそのスタイルを利用しています。スタイルを変えたい場合は通常のHTMLと同じくCSSで指定できます。

なお、toolstrip はブックマークバーと統合することが検討されていますChromium-extensionsグループでのAaron Boodman氏の発言より⁠。また、今見ているページに対して何かしらのアクションをするための機能として、PageActionというAPIも存在します(詳細は後述します⁠⁠。この辺りはまだまだ仕様も実装も固まりきっていないので、今後大きな変更がある可能性もあります。なお、バージョン 4.0.206.1からは、Ctrl+Alt+bでToolstripの表示・非表示の切り替えができるようになっています。

さて、上記の2つのファイルを適当な(ここではCドライブ直下のHelloWorld)というフォルダの中に保存します。

HelloWorld
C:\HelloWorld
   manifest.json
   toolstrip.html

あとはChromeにload-extensionオプションを付けて起動します。

chrome.exe --load-extension=C:\HelloWorld

起動に成功していれば、chrome://extensions/ に読み込まれたExtensionが表示され、ウィンドウの下にToolstripが表示されます。

図1 chrome://extensions/ ページと Hello World Extension
図1 chrome://extensions/ ページと Hello World Extension

なお、dev版の4.0.206.1からは、chrome://extensions/ の Load unpacked extension ボタンからExtensionを読み込むことができるようになっています。

Packaging

Extensionsの作成(Packaging)には、Google Chrome本体を使用します。Chrome 3では本体の起動中にはPackagingができない点に注意が必要です。

Packagingには pack-extension という起動オプションを使用します。さきほどのHelloWorldをパッケージするコマンドは下記の通りです。

chrome.exe --pack-extension=C:\HelloWorld

これでHelloWorld.crxというファイルと、HelloWorld.pemというファイルが出来上がります。なお、crxファイルのファイル名を決めるのはフォルダ名であって、manifest.jsonのnameなどではありません。このcrxファイルが拡張本体で、こちらを配布することになります。pemファイルはプライベートキーファイルで、拡張を更新する際に使用して、同じ拡張のアップデートであることを保証します。なりすましを防ぎ、安全に自動更新(Chrome 4で有効になる予定です)を行うための仕組みですので、pemファイルの扱いには注意が必要です。

pemファイルは下記のようにpack-extension-keyオプションとして指定します。

chrome.exe --pack-extension=C:\HelloWorld --pack-extension-key=HelloWorld.pem

やはり、dev版の4.0.206.1からは、chrome://extensions/ の Pack ExtensionボタンでGUIによるパッケージングを行えます。

最後に --enable-extensions を付けて起動した状態のChromeにcrxファイルをドラッグ&ドロップすればインストールダイアログが出て、Installボタンを押せばextensionがインストールできます。なお、4.0.206.1以降のdev版ではデフォルトでExtensionsが有効になるようになっており、--enable-extensionsに代わって、--diable-extensionsというExtensionsを無効にするオプションが追加されています。

Extensionsのドキュメント

Chromium(Chrome)Chromium Developer Documentationに豊富なドキュメントが存在します。中でも、Design Documents (Chromium Developer Documentation) には技術者向けの設計仕様があり、その中にExtensionsのドキュメントがまとめられています。

2009年9月前半の時点で、Extensionのドキュメントは全面的な改定が行われており、最新のドキュメントは Google Chrome Extensions: Developer Documentation に存在します。このドキュメントはさらに充実しており、常に最新の情報が載っています。Extensionを作り始めて、わからないことがあった場合はまずこちらのドキュメントに当たってみるのが近道といえます。

本稿では公式のドキュメントを参照できるように、ドキュメントで使われている名称はそのまま使用します。

Extensions API

それでは、ExtensionsのAPI仕様を見ていきます。

Content Scripts

Content Scriptsは 対象ページを表示する際に、そのページのコンテキスト上でJavaScriptを実行するAPIです。FirefoxのAdd-onのGreasemonkeyと同じような挙動で、DOMContentLoadedのタイミングで実行、ページ側のwindowオブジェクトに直接触れることはできないといった特徴があります。ただし、このContent Scriptsから直接クロスドメイン通信などを行うことはできません。Content Scriptsは後述のToolstripsやBackground Pagesとメッセージのやり取りをすることができますので、それを介してクロスドメイン通信などを行うことになります。

対象ページと実行するスクリプトの組み合わせは、manifest.jsonファイルで定義します。

www.google.com用のContent Scripts定義サンプル
  "content_scripts": [
    {
      "css": [ "google.css" ],
      "js": [ "google.js" ],
      "run_at": "document_start",
      "matches": [ "http://www.google.com/*" ]
    }
  ]

"run_at": "document_start" はScriptの実行タイミングをdocumentの読み込みを開始したタイミングにするための指定です。より速くスクリプトを実行したい場合に指定します。デフォルト値は"document_end"で、通常は省略します。

Toolstrips

ToolstripsはExtensions用のツールバー領域です。中身はHTML(とCSS,JavaScript)で記述します。ボタンを設置したり、簡単なメッセージを表示することができます。現在はステータスバーのような領域に表示されますが、将来的にはブックマークバーと統合される予定となっています。

Background Pages

Background PagesはChromeの起動中、バックグラウンドでJavaScriptを実行させておくことができるAPIです。manifest.jsonにバックグランドで読み込むHTMLを指定します。

Page Actions

Page Actionsはアドレスバーの中にボタンを表示するAPIで、⁠今見ているページについて~~する」といった機能を提供することができます。manifest.jsonでアクションを定義し、Background Pagesなどで対応するアクションを実装します。

Page Actions定義サンプル
  "page_actions": [
    {
      "id": "bookmark",
      "name": "このページをブックマークする",
      "icons": ["favicon.png"]
    }
  ]
Cross-Origin XHR

クロスオリジン通信(クロスドメイン通信)を行うAPIです。manifest.jsonで通信を許可するドメインを指定します。

Chrome 3ではこの指定は機能していないため、permissionsを指定してもしなくても Extensionsのhtmlからはクロスドメイン通信を行うことが可能となっています。セキュリティの問題に注意が必要です。

permissions定義のサンプル
  "permissions": [ "http://www.google.com/" ],
Tabs

Chromeのタブを操作するAPIです。Chrome 4 ではpermissionsでtabsと記述されていないと、このAPIを使用することができません。なお、Chrome 3ではtabsを指定することができません。そのため、Chrome 3とChrome 4の両方で動くExtensionを作る場合には注意が必要です。

permissions定義のサンプル
  "permissions": [ "tabs" ],
Windows

Chromeのウィンドウを操作するAPIです。Tabsと同じく、permissionsでtabsと記述されていないとこのAPIを使用することができません。permissions 定義はTabsと共有しています。

Bookmarks

Chromeのブックマークを操作するAPIです。やはり、Chrome 4 ではpermissionsで "bookmarks" と記述する必要があります。

これら以外にも、履歴や、ダウンロードなどにアクセスするAPIも仕様策定が進められています。

Google Chrome の開発ツール

ここで、ChromeのFirebugといえる、Web Inspectorについて解説します。Web InspectorはWebKitに実装されているデバッグツールで、Droseraというツールの後継にあたります。

このWeb Inspectorは高機能で、HTML・CSSの確認と通信状況の確認等のツール、JavaScriptのコンソール、プロファイルにデバッガ、さらにデータベースのViewer機能などが搭載されています。

ChromeのWeb Inspectorもほぼ同等の機能を実現できていますが、2009年8月末時点では、データベース関連の機能、デバッグウィンドウをメインウィンドウに統合する機能などが未実装になっています。

Web Inspectorを使用する方法はいくつかあり、右クリックメニューの「要素の検証」から起動する方法、ウィンドウ右上のページメニューから「開発/管理⁠⁠→⁠JavaScriptコンソール(Chrome 4ではDeveloper Tools⁠⁠」から起動する方法、ショートカットキーのCtrl+Shift+jから起動する方法があります。Ctrl+Shift+jはほかの方法が無効になっていても使用できることがあるので、こちらを覚えておくと便利です。

図2 Web Inspector
図2 Web Inspector

画面左下のShow ConsoleをクリックするとJavaScriptのコンソールが表示され、スクリプトのデバッグが行えます。Firebugのようにオブジェクトを中身をクリックで追っていくことができるので、開発時は大変重宝します。

図3 Web Inspector With Console
図3 Web Inspector With Console

また、ExtensionのToolstrip、Background Pagesに指定しているHTMLについては、chrome://extensions/ のページにあるInspectボタンでWeb Inspectorを開くことができます。Toolstripについては、Toolstrip上で右クリックをするだけで起動することもできます(これは開発者向けの機能なので、将来的に異なる動作になる可能性も高いと思われます⁠⁠。

実用的なExtensionの作成

ここまででExtensionの開発に必要な知識は一通り学びました。ここで復習も兼ねて、少し実用的なExtensionを1つ作成してみます。

どういったExtensionが実用的であるかは悩ましいところですが、手軽さとの兼ね合いからソーシャルブックマークサービスでのブックマーク数を表示するExtensionを作成してみます。

FirefoxのAdd-onではSBMカウンタなどがあります。また、すでにChrome Extensionとして、Google Chrome 拡張(Chrome Extension) はてなブックマークのエントリー数を表示する Chrome拡張を作った - 忘れないようにメモBig Sky :: 被はてなブックマーク数を表示するGoogle Chrome拡張書いた。など幾つかの実装例があります。今回はこれらの実装をよりExtensionの特徴を生かして実装しつつ、はてなブックマークに加えてdeliciousにも対応してみます。

ToolstripのHTML/CSS

まずは、Toolstripによるインターフェース部分を作成します。ブックマーク数の表示のほか、ブックマーク詳細ページへのリンクとブックマーク追加画面へのリンクを設置することにします。

ToolstripのHTML(一部)
<ul id="sbmlist">
  <li class="add">
    <a id="add_hatena" target="_blank" title="はてなブックマークに追加">
      <img src="hatena.favicon.gif" id="icon_hatena">
    </a>
  </li>
  <li class="counter">
    <a id="text_hatena" target="_blank"> </a>
  </li>
  <li class="add">
    <a id="add_delicious" target="_blank" title="Save this bookmark">
      <img src="delicious.small.gif" id="icon_delicious">
    </a>
  </li>
  <li class="counter">
    <a id="text_delicious" target="_blank"> </a>
  </li>
</ul>
ToolstripのCSS(一部)
#sbmlist{
  display:table;
  margin:5px 3px;
  list-style-type:none;
  height:18px;
  min-width:8em;
}
#sbmlist li{
  display:table-cell;
  vertical-align:middle;
}
#sbmlist li.counter{
  min-width:2.5em;
  text-align:center;
  padding:0 3px 0 0;
}

HTMLはulとliのリストで構成し、CSSで display:table;、display:table-cell; を指定することで(少々強引ですが)レイアウトを調整しました。Chrome(WebKit)で正常にされれば良いので、-webkit系の拡張スタイルも使用できます。角丸なども -webkit-border-radius で実現できます。

APIを処理するJavaScriptの実装

続いて、JavaScript部分を実装します。はてなブックマークもdeliciousもブックマーク数を返すAPIが用意されていますので、クロスドメイン通信でブックマーク数を取得することにします。

ページのURLの取得⇒リクエスト⇒アップデート処理と、これらはクラス(ライク)にまとめると見通しがよくなりそうなので、SBMというクラスを作成してみます。

SBMクラスの実装
function SBM(service){
  this.text = document.getElementById('text_' + service.id);
  this.icon = document.getElementById('icon_' + service.id);
  this.add = document.getElementById('add_' + service.id);
  this.api_get = service.api_get;
  this.api_link = service.api_link;
  this.api_add = service.api_add;
  this.responceFilter = service.responceFilter;
  this._cache = {};
}
SBM.prototype = {
  request:function _sbm_request(only_request){
    var self = this, xhr = new XMLHttpRequest();
    var api_url = this.replace(this.api_get);
    xhr.open('GET', api_url, true);
    xhr.onload = function(){
      var count = self.responceFilter(xhr.responseText);
      self._cache[self.url] = {count:count};
      if (!only_request) self.update(count);
    };
    xhr.send();
  },
  set:function _sbm_set(url, force_request, only_request){
    if (!only_request) {
      this.text.textContent = '-';
    }
    this.url = url;
    this.encoded_url = encodeURIComponent(url);
    if ((force_request || only_request) || !this._cache[url]) {
      this.request(only_request);
    } else {
      this.update(this._cache[url].count);
    }
  },
  update:function _sbm_update(count){
    this.text.textContent = count;
    this.text.href = this.replace(this.api_link);
    this.add.href = this.replace(this.api_add);
  },
  replace:function _sbm_replace(str){
    var self = this;
    return str.replace(/#\{([^}]+)\}/g, function(_, _$){
      return self[_$] || '';
    });
  }
};

まずsetメソッドでURLを設定し、すでにキャッシュがあればupdateのみ、キャッシュしてなければrequestを行います。タブの切り替え時はキャッシュを使用して、リロードなどの更新時は改めて取得を行うことができるようにリクエストを強制するオプションを、またバックグラウンドで開いた際に先にキャッシュしておけるように、キャッシュを行うだけのオプションを実装しました。

APIのレスポンスの形式は各サービスごとに異なるので、responceFilterというメソッドで処理を行います。responceFilterはServicesというオブジェクトで定義しました。またServicesは各APIのURLなどサービス固有の部分を定義しています。

Servicesの定義
var Services = [
  {
    id:'hatena',
    api_get:'http://b.hatena.ne.jp/entry.count?url=#{encoded_url}',
    api_link:'http://b.hatena.ne.jp/entry/#{url}',
    api_add:'http://b.hatena.ne.jp/add?&url=#{encoded_url}',
    responceFilter:function(text){
      if (/\D/.test(text)) {
        return '';
      } else {
        return text;
      }
    }
  },
  {
    id:'delicious',
    api_get:'http://badges.del.icio.us/feeds/json/url/blogbadge?url=#{encoded_url}',
    api_link:'http://delicious.com/url/#{hash}',
    api_add:'http://delicious.com/save?v=5&jump=close&url=#{encoded_url}',
    responceFilter:function(text){
      try {
        var r = JSON.parse(text);
        if (r.length === 0) {
          return '';
        }
        this.hash = r[0].hash;
        return r[0].total_posts;
      } catch (e) {
        console.error(e);
        return '';
      }
    }
  }
];
var services = Services.map(function(service, i){
  return new SBM(service);
});

ChromeではJSON.parse、JSON.stringifyが実装されていますので、JSONを扱う際は安全のためevalではなくJSON.parseを使用するべきです。

ここまでは一般的なJavaScriptの実装です。ここからは本題であるExtensions APIの使い方を見ていきます。

tabs APIの使い方

今回のケースはタブの更新時とタブの切り替え時に、現在選択しているタブのURLを使ってToolstripを更新するという処理になります。

tabs APIにはタブ選択の変更(onSelectionChanged⁠⁠、タブの更新(onUpdated)といったAPIがあります。これらのAPIはaddListenerというメソッドが定義されており、リスナー関数を設定することができます。また、タブのURLの取得はTabオブジェクトから行えます。このTabオブジェクトの取得は chrome.tabs.get で行うことができます。

まずは、Tabオブジェクトから各serviceを実行する処理を定義します。このとき、httpで始まらないURLや長いURL(ここでは短めに255としました)を弾くようにしておきます。

run_on_tabの定義
var run_on_tab = function(tab, force_request, only_request){
  var url = tab.url;
  if (!/^http/.test(url)) return;
  if (url.length > 255) return;
  services.forEach(function(service){
    service.set(tab.url, force_request, only_request);
  });
};

続いて、onSelectionChangedを登録します。onSelectionChangedのリスナーはtabidを返すので、そのIDを元にTabオブジェクトを取得します。

onSelectionChangedの使い方
chrome.tabs.onSelectionChanged.addListener(function(tabid){
  chrome.tabs.get(tabid, function(tab){
    run_on_tab(tab);
  });
});

最後に、onUpdatedを登録します。onUpdatedのリスナーもtabidを返しますが、updateの場合は、そのタブが選択状態であるとは限りません。そこで、chrome.tabs.getSelectedで現在選択しているタブを取得します。ただし、getSelectedには現在の(Toolstripが存在する)windowのidが必要となります。そのため、予めchrome.windows.getCurrentでwindowidを取得しておく必要があります。

current_windowの取得とonUpdated、getSelectedの使い方
var current_window;
chrome.windows.getCurrent(function(window){
  current_window = window;
});
chrome.tabs.onUpdated.addListener(function(tabid){
  chrome.tabs.getSelected(current_window.id, function(tab){
    if (tab.id === tabid) {
      run_on_tab(tab, true);
    } else {
      run_on_tab(tab, true, true);
    }
  });
});

onUpdatedが発生したタブと現在選択されているタブが同じ場合、フォアグラウンドで更新されたとして、force_requestを有効にしています。またその逆であれば、バックグラウンドでタブが開かれたとして、only_requestを有効にしています。

以上でExtensionの実装は完了しました。仕上げとしてmanifest.jsonを作成します。

SBM Counterのmanifest.json(Chrome 3仕様)
{
   "name": "SBM Counter",
   "description": "SBM Counter",
   "toolstrips": [ "toolstrip.html" ],
   "version": "1.0"
}

toolstripsしか使用していないため、非常にシンプルです。しかし、先述のとおり、Chrome 4ではtabs API、クロスドメイン通信を使用するにはpermissionsの記述が必要になります。

SBM Counterのmanifest.json(Chrome 4仕様)
{
   "name": "SBM Counter",
   "description": "SBM Counter",
   "permissions": [ "http://*/" ,"tabs" ],
   "toolstrips": [ "toolstrip.html" ],
   "version": "1.0"
}

本来であれば、ドメインの記述は全体を許可する "http://*/" ではなく、"http://b.hatena.ne.jp/" のように個別に設定するべきですが、今回テストに使用したChromeのバージョン(4.0.207.0)では個別指定が機能していなかったため、"http://*/" を使用しています。

また、Chrome 4用にpermissionsに "tabs" を記述すると、Chrome 3ではExtensionのインストール時に不正なpermissionsとして弾かれてしまいます。そのため、tabs APIなどを使用するExtensionはChrome 3用と4用でパッケージを分ける必要がある点に注意が必要です。

以上をパッケージしたファイルは下記にあります。

SBM Counter for Chrome 3
SBM Counter for Chrome 4

まとめ

今回はExtensionsの作成からAPI仕様の概略とデバッグツール、そしてより実用的なExtensionについて説明しました。次回はExtensionsの各種APIを実際に試しながら解説をしていきたいと思います。

おすすめ記事

記事・ニュース一覧