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