Life Refactoring

外から見ていると大して変わってないかもしれないけど、生き方とか考え方とか自分の中身が整理されるような。そんなブログにしていきたい。

現在開いているタブの一覧を取得・表示するGoogle Chromeプラグインを作ってみた

東京に引っ越してきて3週間目になります。
ようやく自宅にガステーブルが導入され、自炊ができるようになりました。
最初に作ったのはカレーでした。

とまぁ、どうでもいいことは置いておいて…
現在、自社で作ろうとしているチャットサービスに必要になる、ということでGoogle Chrome Extension(Chromeのプラグイン)を作ることになりました。
最初の課題としては以下の様な機能を作っていきます。

  • Chromeで開いているタブの一覧を取得
  • 取得したタブのリストを指定されたページ内のulに流しこむ
    • 例えば、ここでは www.example.com というページ内の <div id="tabs" /> に流しこむことにする
  • タブの開閉や開いているURLの変化があったら即座に一覧の内容を更新
  • 一覧内のタブタイトルをクリックすると、該当するタブにフォーカス

まずはChrome Extensionの仕組みを調べてみる

プラグインを作ること自体が初めてなので、何はともあれChrome Extensionの公式ページを見ていきます。
Google Chrome Extensions

どうやら、jsをベースにして開発をしていくみたいですね。
内容を見ていくと、Chrome Extensionは↓のような仕組みになっているみたい。
(本当はもっと色々あるんだろうけど今回の実装に必要な部分だけ抜粋)
f:id:tama_d:20121014200306p:plain

  • Background Page:Chrome Extensionのコアとなる部分。ユーザーから見えないところで処理を行うところ。
  • Window, Tab:そのまんまChromeのウィンドウとタブ。ウィンドウ内に複数のタブが開いたり、一度に複数のウィンドウが開いたりする。
  • Content Scripts:タブで開いているドキュメントに紐付きながらjsを実行できるスクリプト。

セキュリティの関係からか、Background Pageからタブ内のドキュメントを直接操作することはできず、代わりにContent Scriptsがタブ内のDOM操作などができるらしい。もちろん、それだけだとDOMから取得したデータをまとめたり、処理の結果をDOMに反映させることができないので、Message Passingという仕組みを使うと、Background PageとContent Scriptsとの間でデータの受け渡しができる…と。なんとも面倒くさそう。

実際に作ってみる


今回は、以下の3つのファイルを作成しました。

  • manifest.json:Chrome Extensionに関する設定ファイル
  • bg.js:Backgrond Page
  • content.js:Content Scripts

manifest.json

{
    "name": "Name of Chrome Extension",
    "version": "0.1",
    "manifest_version": 2,
    "description": "This is a chrome extension.",
    "background": {
	"persistent": true,
	"scripts": ["jquery-1.8.2.min.js", "bg.js"]
    },
    "permissions": [
	"tabs"
    ],
    "content_scripts": [
	{
	    "matches": ["*://www.example.com/*"],
	    "js": ["jquery-1.8.2.min.js", "content.js"]
	}
    ]
}
  • name, version, description: string
    • 拡張機能」タブに表示される名前とバージョン、説明。
  • manifest_version: 2
    • 指定しないと、読み込んだ時にChromeに怒られたので指定しておく。
  • background: object
    • Background Pageに関する設定。
    • persistent: true/false
      • trueにしておくと常にOpenになるらしい。falseにするとEvent Pageってのになるらしいけど詳細は要調査。
    • scripts: array of string
      • Background Pageの処理を記述したスクリプトファイル名の配列。今回はjQueryとbg.jsのみ。
  • permissions: array of string
    • 特別な処理を実装する際に記述する設定。今回はタブ関係の処理を実装するので"tabs"のみ。
  • content_scripts: object
    • Content Scriptsに関する設定。指定したパターンのドメインに対して適用するスクリプトを指定できる。
    • matches: array of string
      • スクリプトを適用するドメインのパターン。今回は www.example.com に対してのみ必要なので "*://www.example.com/*" で指定。
    • js: array of string
      • 適用するスクリプトファイル名の配列。今回はjQueryとcontent.jsのみ。

他にもアイコンとか最低動作バージョンとか指定できるみたいだけど今回は無視。

bg.js

// 1. タブの開閉、変更があったら、www.example.comを開いているタブに通知する
chrome.tabs.onCreated.addListener(function(tab){
    sendTabs();
});
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab){
    sendTabs();
});
chrome.tabs.onRemoved.addListener(function(tabId, removeInfo){
    sendTabs();
});
function sendTabs(){
    chrome.windows.getAll({populate: true}, function(windows){
	var receiverIds = new Array();
	var tabs = new Array();

	jQuery.each(windows, function(win_idx, win){
	    jQuery.each(win.tabs, function(tab_idx, tab){
                // Chrome専用のタブは無視する(「拡張機能」とか)
		if(tab.url.indexOf("chrome://") != -1){
		    return;
		}

		if(tab.url.indexOf("www.example.com") != -1){
		    receiverIds.push(tab.id);
		}else{
		    tabs.push(tab);
		}
	    });
	});

        // 2. www.example.comを開いているタブにメッセージを送信
	jQuery.each(receiverIds, function(idx, tabId){
	    chrome.tabs.sendMessage(tabId, {tabs: tabs}, function(response){
		console.log(response.message);
	    });
	});
    });
}

// 5. content.jsから受け取ったタブにフォーカスする
chrome.extension.onMessage.addListener(function(request, sender, sendResponse){
    chrome.tabs.update(request.tabId, {active: true});

    sendResponse({message: "ok"});
});

content.js

// 3. bg.jsからタブデータを受け取ったら、<div id="tabs" /> 内に一覧を生成
chrome.extension.onMessage.addListener(function(request, sender, sendResponse){
    var tabList = $("#tabs").empty().append('<ul></ul>');

    jQuery.each(request.tabs, function(idx, tab){
	var item = $('<li>');
        // faviconがあったら表示する
	if(tab.favIconUrl){
	    item.append($('<img src="'+tab.favIconUrl+'" width="16" />'));
	}
	item.append($('<a>' + tab.title + '</a>')
		    .attr("href", "#" + tab.id)
		    .click(activateTab));
	tabList.append(item);
    });

    sendResponse({message: "ok"});
});

// 4. クリックされたタイトルのタブにフォーカスするようにbg.jsにメッセージを送る
function activateTab(){
    chrome.extension.sendMessage(
	{
	    tabId: parseInt($(this).attr("href").substring(1))
	},
	function(response){
	    console.log(response.message);
	}
    );
    return false;
}

以下は処理の流れ

1. タブイベント(開閉、更新)の処理(bg.js)
タブイベントのハンドラは

chrome.tabs.on[イベント名].addListener([ハンドラ]);

という形で指定できる。

今回は、どんなイベントが起きてもタブ情報を更新するという同じ処理しかしないので、 sendTabs というメソッドにまとめちゃいました。そして、 sendTabs では、現在開かれている全ウィンドウ内の全タブを www.example.com を開いているタブとそうでないタブに分け、前者のタブに後者のタブ情報のリストを送ることをしています。

2. Content Scripts へタブ情報を送信(bg.js)
Background Page である bg.js から直接 DOM を操作できないので、前者のタブに紐付いてる Content Scripts(ここでは content.js)に対して、↓のメソッドを使ってデータをメッセージとして送信する。

chrome.tabs.sendMessage([タブID], [データ], [レスポンスの処理]);

3. bg.js から受け取ったタブ情報をもとにHTMLのリストを生成(content.js)
bg.js から送信されたメッセージは、↓のようにすることで受信できるようになります。

chrome.extension.onMessage.addListener([ハンドラ]);

また、ハンドラは以下の様な書式です。

function([メッセージ], [送信元のタブ情報], [送信元への返信のためのメソッド名])

生成しているのは単純な ul ですが、タブタイトルを a タグで囲んで、次に繋がるイベントハンドラを追加しています。

4. クリックされたタイトルに対応するタブのIDを bg.js に送信(content.js)
bg.js の時と似ているけど、↓のようにして bg.js に対してメッセージを送信。

chrome.extension.sendMessage([データ], [レスポンスの処理]);

5. クリックされたタブにフォーカス(bg.js)
content.js の時と似ているけど(ry

指定されたIDのタブにフォーカスするには、

chrome.tabs.update([タブID], {active: true});

とする。 update メソッド自体は色々な使い道があるみたいだけど、今回はタブにフォーカスしたいだけなので "active: true" のみ。

試してみる

  • Chromeの「設定」→「ツール」→「拡張機能
  • 画面右上の「デベロッパーモード」をチェック
  • 「パッケージ化されていない拡張機能を読み込む」→作成したファイルを格納したフォルダを選択
  • 一覧の中に manifest.json の name が表示されればOK
  • コードに変更を加えた場合は「拡張機能」タブを表示→リロード

www.example.com を開いた状態で、別のタブを開いたり、閉じたりすると一覧が自動的に更新され、さらにタブタイトルをクリックすると、そのタブへ移動するようになりました!…いや、まぁこれだけで何ができるっていうわけでは無いんですけどね。

会社とブログ、始めました!

この記事を読んでくださっているアナタ!はじめまして、土田 貴裕といいます。先日の10月1日に石戸谷 顕太朗(id:kent013)さんと自分が社員となって設立したエングラフィアという会社の、CTOという肩書きを背負っている人間です。会社設立を機に、自分が感じたり、考えたりしたり、作ってみたりしたことをもっと積極的にオープンにしていくことが必要だと思い、ブログを始めた次第でございます。あまりブログを書いた(書き続けた)経験の少ない身なので、失礼なことや馬鹿なことなど書いてしまうかもしれないですが、厳しくツッコミを入れていただけたら幸いです。


株式会社エングラフィアを起業しました。 - Paradigm Shift Design
↑石戸谷さんのエングラフィアに関する記事


最初の記事はやっぱり自己紹介だろう!ってことでプロフィールを簡単にずらっとまとめてみました。

  • 経歴
    • 1982年12月10日:誕生(本籍:秋田県)
    • 幼少期は目黒にて過ごす
      • お客さん用に出していたお菓子をつまみ食いして体型に丸みが帯びだしてくる
    • 1994年:小6の時に2度の転校
      • 自分ではどうにもできない現実を突きつけられる
    • 1995年3月:名古屋市立東山小学校 卒業
    • 1998年3月:名古屋市立東星中学校 卒業
      • おそらく今までの中で最も濃かった頃。あだ名は「こゆげ(濃い眉毛の略)」
    • 2001年3月:名古屋市立名東高等学校 卒業
    • 2005年3月:名古屋大学 工学部 電気電子・情報工学科 卒業
      • 部活はバドミントン部に所属。部員からは「機敏なデブ」と呼ばれる
    • 2007年3月:名古屋大学大学院 情報科学研究科 博士前期課程修了
    • 2011年3月:名古屋大学大学院 情報科学研究科 博士後期課程修了 博士号取得
    • 2011年4月〜2012年9月:株式会社リオ 開発ディレクター
  • プログラミング経験
    • パソコン自体に初めて触れたのが大学入ってから
    • 研究室入るまではCとJavaを少々
    • 研究室に入ってからはJavaがメイン、時々PerlPHP、あとマイナーなところでSilverlight
    • 会社入ってからはだいたいPHP、社内でRubyも書き始めたけどあまり実装しておらず
  • 過去に開発?してたもの
    • 過去の当選番号をもとにロト6かなんかの予想をするのプログラムの欠片
    • 開くたびに変化する迷路の中をゴール目指してキャラクターを動かすだけのト◯ネコの大冒険とか風来のシ◯ンとかを意識しているのかしていないのか分からないようなゲーム(Javaアプレットで)
    • 研究室内で日記を書くことが半義務化していたので、当時勉強していた技術を使って、データはXML、HTMLへの変換はXSLT、データ更新はCGIで行う日記(日記の内容自体が黒歴史
    • 会議風景をカメラやマイクで撮影しつつ、Wiiリモコンを使って動画のインデックス情報を入力できるミーティングルーム
    • Dropboxみたいにサーバ上に画像やPowerPointスライドなどをアップし、他のアプリが利用できるようなWebシステム

ざっと列挙するとこんな感じでしょうか。新しく設立したエングラフィアは新しいWebサービスを立ち上げることが目標なので、当然プログラミングも必要になってきます。なので、このブログでもちょいちょい勉強したこと、トライしたことをどんどん出していければいいなぁなんて思っています。おそらくゆるーいブログになるかと思いますが、今後もお付き合いくださいませ。