続・先取り! Google Chrome Extensions

第5回Chrome ExtensionのAPI#2

こんにちは、Google Chrome ExtensionsのAPI Expertになりました太田です。今回は、これまで使用していないAPIを中心に使った新しいExtensionを作成してみます。

が、その前に大きなニュースとして、Extensionsが有効になったbeta版とExtensionsギャラリーが公開されました。Macのbeta版も公開されましたが、こちらは拡張を使うことはできませんのでご注意ください。

Extensionsを作られた方はDeveloper DashboardからExtensionを登録できます。登録時の注意などはブログにまとめていますので、そちらをご参照いただければと思います。また、beta版のリリースに合わせて、Google Chrome拡張について日本語での情報交換ができるGoogle準公式コミュニティのChromium-Extensions-Japanもスタートしました。

さて、beta版で使用可能なAPIのなかで、このシリーズで取り上げていないのは(NPAPIを除いて⁠⁠、Override Pagesと、Bookmarks APIの2つです。

Override PagesとNew Tabページ

まずNew Tabページとは、Chromeで新規タブを開いた際にデフォルトで表示されるページです。このページにはよく閲覧しているページや最近閉じたタブなどが表示されます。New TabページはExtensionで直接カスタマイズすることはできませんが、Override Pagesでまるごと別のページに差し替えることができます。

早速、New TabページをBlank Pageにするサンプルを見てみます。

Override Pagesのmanifest.json
{
  "name": "Blank new tab page",
  "version": "0.1",
  "chrome_url_overrides": {
    "newtab": "blank.html"
  }
}

2009年12月時点で、chrome_url_overridesのプロパティはnewtabのみです。このnewtabにはNew Tabページを置き換えるHTMLファイルを指定します。

ちなみに、blank.htmlのソースは下記の通りごくシンプルです。新規タブを開くのがより軽くなるので、これはこれで有力な選択肢と言えます。

blank.htmlのソース
<html>
 <head>
  <title>Blank New Tab</title>
  <style>
  div {2009/12/09 3:04
    color: #cccccc;
    vertical-align: 50%;
    text-align: center;
    font-family: sans-serif;
    font-size: 300%;
  }
  </style>
 </head>
 <body>
  <div style="height:40%"></div>
  <div>Blank New Tab&trade;</div>
 </body>
</html>

上書きしたページはExtensionsのページとして扱われますので、permissionsの設定次第でTabs APIやクロスオリジン通信などができますし、localStorageで設定を保存することも可能です。ただし、元々のNew Tabページで使われている情報(よくアクセスしているページとそのサムネイル、テーマの背景など)にアクセスすることはできません。これに関してドキュメントには4つの注意書きがあります。

速く、小さく作る
New Tabページは頻繁に使用するので、パフォーマンスが重要です。同期的にネットワーク通信やデータベースへの接続などをすることは避けましょう。
タイトルを設定する
タイトルを設定ないとURLがタブに表示されてしまいます。<title>New Tab</title> のようにしましょう
キーボードのフォーカスがあることに依存させない
新しいタブを開いたときは常にOmnibox(アドレスバー)にフォーカスします。
デフォルトのNew Tabページを再現しようとしない
デフォルトのNew Tabページとよく似たページを作るために必要なAPI(よく見るページ、最近閉じたページ、チップス、テーマの背景画像)(今のところ)存在しません。今のところは、デフォルトのページとはまったく違ったページにしたほうがよいでしょう。

Bookmarks API

続いて、Bookmarks APIを解説します。まず、Bookmarks APIを使用するにはmanifest.jsonでpermissionsを記述する必要があります。

manifest.jsonでpermissionsにbookmarksを指定
{
  "permissions": [
    "bookmarks"
  ],
}

さて、Bookmarks APIでできることは大きく分けて3つあります。1つはBookmarkTreeNodeと呼ばれるブックマークのデータを収めたオブジェクトの操作(取得・追加・削除など⁠⁠、2つ目はBookmarkの検索(やはり取得するのはBookmarkTreeNode⁠⁠、3つ目はBookmarksの操作を監視してのイベント処理、以上のAPIが用意されています。

まず、BookmarkTreeNodeについて解説します。ブックマークに関するデータはフォルダ情報もブックマーク情報もこのBookmarkTreeNodeで表現されます。idとtitleは共通で必ず持っており、フォルダ情報の場合はchildrenというプロパティに配列形式でさらなるBookmarkTreeNodeを、ブックマーク情報の場合はURLを持っているのが大きな特徴です。詳細はBookmarkTreeNodeのドキュメントでご確認ください。

このBookmarkTreeNodeでフォルダもブックマークも表現するという点が少々わかりにくいかと思います。次に例として全ブックマークを走査するコードを挙げてみます。

全ブックマークを走査するサンプルコード
chrome.bookmarks.getTree(function(roots){
  var bookmarks = [];
  roots.forEach(parser);
  function parser(node){
    if (node.children) {
      node.children.forEach(parser);
    } else if(node.url) {
      bookmarks.push(tree);
    }
  }
  console.log(bookmarks);
});

chrome.bookmarks.getTreeはブックマークのルートノード(の配列)を返します。そのルートノードを起点として、nodeがchildrenプロパティを持てばそれはフォルダであると判断できるので、そのchildrenをさらにパースし、一方でurlを持つのであればそれがブックマークなので、配列に詰めています。なお、ユーザーによってはブックマークの数が数万件にもなっている可能性があるので、安易に上記のようなコードを使うべきではない点に注意してください。

このように、ブックマークのデータは1つのツリー構造になっています。このツリーに対して、chrome.bookmarks.createで新しいbookmark nodeを追加したり、chrome.bookmarks.moveやchrome.bookmarks.updateでブックマーク情報を更新したりできます。

ブックマークの検索
chrome.bookmarks.search("テスト", function(results) {
    console.log(results);
});

名前の通り、chrome.bookmarks.searchはブックマークを検索します。対象はブックマークのみ(フォルダは非対象)で、第一引数に文字列で検索条件を渡し、第二引数に渡す関数でその結果を受け取ります。日本語は問題なく使用でき、スペースで区切れば複数条件で絞り込むこともできます。ただし、対象をURLにするかタイトルにするかや、特定フォルダ以下から検索するなど細かい条件は今のところできません。

New Tabページを書き換える拡張

ではNew TabページをカスタマイズするExtensionを作成してみます。まず、例によってmanifest.jsonを記述します。

Start Tileのmanifest.json
{
  "name": "Start Tile",
  "description": "replace newtab page",
  "version": "0.0.1",
  "chrome_url_overrides": {
    "newtab": "tile.html"
  },
  "permissions": [
    "tabs",
    "bookmarks"
   ]
}

Tabs APIとBookmark APIを使用するので、permissionsは "tabs" と "bookmarks" を指定しました。chrome_url_overridesにはtile.htmlというHTMLを指定しています。

では、このtile.htmlを用意していきます。

tile.htmlのソース
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Start Tile</title>
  <link href="tiles.css" rel="stylesheet" type="text/css">
</head>
<body>
<div id="box-table">
  <div id="box-row-single">
    <section>
      <h2>Tab</h2>
      <ul id="tab_list"></ul>
    </section>
    <section>
      <h2>Bookmark</h2>
      <ul id="bookmarks_list"></ul>
    </section>
    <section>
      <h2>History</h2>
      <ul id="history_list"></ul>
    </section>
  </div>
</div>
<ul id="tmpl"><li><a></a></li></ul>
<script src="tiles.js"></script>
</body>
</html>

まずは骨となるHTMLです。<ul id="tmpl">の部分はテンプレート用で、JavaScriptからcloneNode(true)をするために記述し、CSSで#tmplにdisplay:noneを指定して非表示にしています。

続いて、CSSを見ていきます。

tile.htmlのCSS
#tmpl{
display:none;
}
#box-table{
display:table;
border-collapse:separate;
border-spacing:0.5em;
}
#box-row-single{
display:table-row;
}
#box-row-single > section{
display:table-cell;
border-top:3px solid #0066ff;
border-right:3px solid #99ccff;
border-bottom:3px solid #99ccff;
border-left:3px solid #0066ff;
-webkit-border-top-left-radius:1.5em;
-webkit-border-bottom-right-radius:1.5em;
padding:0.5em;
width:33%;
overflow:hidden;
}
/*以下省略*/

display:table、display:table-row、display:table-cellを使い、CSSでいわゆるテーブルレイアウトを実現しています。もちろん、tableタグを使用しても同じ見た目にできますが、今後のカスタマイズなどを考えるとtableタグを使わないメリットは十分にあります。

図1 CSSによるテーブルレイアウト
図1 CSSによるテーブルレイアウト

最後に、メインとなるJavaScriptを記述します。

初期化処理とリスト作成関数
var tab_list= document.getElementById('tab_list'),
 bookmark_list= document.getElementById('bookmarks_list'),
 history_list= document.getElementById('history_list'),
 tmpl= document.getElementById('tmpl').firstChild,
 FAVICON= 'http://www.google.com/s2/favicons?domain=';

まず、HTMLの各要素を取得しておきます。続いて、リストを作成する関数を定義します。

リスト作成関数
function list_creater(datas, parent, callback){
  datas.forEach(function(data){
    if (data.url && data.title &&
        data.url.indexOf('javascript:') !== 0) {
      var li = tmpl.cloneNode(true);
      var a = li.firstChild;
      a.href = data.url;
      a.textContent = data.title;
      var icon = document.createElement('img');
      icon.src = data.favIconUrl || FAVICON + a.host;
      a.insertBefore(icon, a.firstChild);
      parent.appendChild(li);
      if (callback) {
        callback(data, a);
      }
    }
  });
}

この関数を使って、ブックマークからリストを作成します。

ブックマークの走査とリストの作成
chrome.bookmarks.getTree(function(roots){
  var bookmarks = [];
  roots.forEach(parser);
  function parser(node){
    if (node.children) {
      node.children.forEach(parser);
    } else if(node.url) {
      bookmarks.push(node);
    }
  }
  list_creater(bookmarks, bookmark_list);
});

続いて、Tabs APIで開いているタブを取得し、それもリストにしてみます。

ブックマークの走査とリストの作成
function TabUpdate(){
  chrome.tabs.getAllInWindow(null,function(tabs){
    while (tab_list.firstChild) {
      tab_list.removeChild(tab_list.firstChild);
    }
    list_creater(tabs, tab_list, function(tab, link){
      link.addEventListener('click',function(evt){
        evt.preventDefault();
        evt.stopPropagation();
        chrome.tabs.update(tab.id,{selected:true});
      }, false);
    });
  });
}
TabUpdate();

タブの場合、クリックした際はそのタブに移動したいので、デフォルトのアクションをキャンセルしてchrome.tabs.updateでタブ移動するようにしました。list_createrでcallbackをするようにしていた点がポイントです。また、タブを閉じたり開いたりした際の変化を反映する必要があるので、TabUpdateという関数にして何度も呼び出せるようにしています。

タブの監視
chrome.tabs.onUpdated.addListener(function(tabid, info){
  if (info.status === 'complete') {
    TabUpdate();
  }
});
chrome.tabs.onRemoved.addListener(function(tabid){
  TabUpdate();
});
chrome.tabs.onDetached.addListener(function(tabid){
  TabUpdate();
});
chrome.tabs.onAttached.addListener(function(tabid){
  TabUpdate();
});
chrome.tabs.onMoved.addListener(function(tabid){
  TabUpdate();
});

タブのイベントAPIをフルに使用してタブの変更を監視し、都度TabUpdateを呼び出しています。あまり効率的な方法ではありませんが、ブラッシュアップは今後の課題とします。

最後に、chrome://extensions/ などの特殊なページは通常のaタグによるリンクでは無効なリンクとして処理されてします。そこで、chrome.tabs.updateで処理する必要があります。この時、リンクひとつひとつに処理を入れる方法とページ全体のクリックを監視してhttpで始まらないリンクがクリックされたときに処理をする方法があります。今回は後者を選択しました。

クリックの監視
const LEFT_CLICK = 0;
const CENTER_CLICK = 1;
document.addEventListener('click',function(evt){
  var target = evt.target;
  if (target instanceof HTMLAnchorElement &&
      target.href &&
      target.href.indexOf('http') !== 0) {
    evt.preventDefault();
    chrome.tabs.getSelected(null,function(tab){
      switch (evt.button) {
        case LEFT_CLICK :
          chrome.tabs.update(tab.id,{url:target.href});
          break;
        case CENTER_CLICK:
          chrome.tabs.create({
            url:target.href,
            selected:false
          });
          break;
      }
    });
  }
},false);

左クリックの場合、現在のタブを更新、センタークリックの場合は新規タブにバックグラウンドで開くようにしています。

今回作成したExtensionはこちらからインストールできます。

まとめ

今回はBookmark APIやOverride Pagesの使い方を解説しました。

次回は今回作成したExtensionをブラッシュアップしながら拡張の開発ノウハウなどを紹介したいと思います。

おすすめ記事

記事・ニュース一覧