OXY NOTES

Chrome機能拡張のイベントページについて

イベントページを制するものはChromeの機能拡張を制する

Chrome機能拡張の解説」第5弾。

このページではイベントページという仕組みについて解説します。

うまく導入するとメリットの大きな仕組みなのですが、なかなか癖が強く、挙動を把握すのに骨が折れます。
しかしイベントページの仕組みを理解することは、機能拡張の仕組みを把握するのに大いに役立ちます。そのため、このページではイベントページを通して、メッセージパッシングストレージアラートについて解説します。


目次


イベントページのメリット

前回の解説でバックグラウンドページは機能拡張のロードとともに読み込まれ、常に裏で実行されているということが理解していただけたと思います。

常に実行していることは言い換えると、常にメモリに駐在し続けるということでもあります。

バックグラウンドページは、現在表示しているページのDOM要素やコンテンツスクリプトとは隔絶されています。
それぞれの要素でjQueryなどのライブラリを読み込んでいた場合バージョン違いによるバッティグが起こらないというメリットもありますが、同時にサイズの重いライブラリを複数実行し続けていることになります。

下のイラストは1ページ表示するのに3つのjQueryを読み込んでいる例。

その対策として実装されたのがイベントページです。

イベントページバックグラウンドページと同じようにインストールや起動時に読み込まれますが、一定の時間が経過すると無効になり、メモリを開放します
再度必要になった時にだけ起動して、再び無効になります。

下のイラストはコンテンツスクリプトは実行されて、必要の無くなったページアクションが無効になった例。

詳しくは「イベントページの公式ドキュメント」をご覧ください。

ありがたいことにバックグラウンドページからイベントページへの移行方法を和訳してくれているサイトがあるので紹介します。

よんちゅBlog : Chrome拡張では、Background pages よりも Event pages を使用したほうが良い


バッググラウンドページをイベントページとして機能拡張を作成する

メモリが開放されるので良いことばかりかというと、そうでもありません。

解説を読んだだけでは理解しにくい思うので、実例でイベントページを作成してみます。

manifest.json

Test Extention 6ディレクトリを作成し、バージョンを0.6にしました。
“persistent”: falseがイベントページ用の記述になります。これで「バックグラウンドページがイベントページとして実行されている」という状態になります。

{
	"manifest_version": 2,
	"name": "Test Extention",
	"version": "0.6",
	"background": {
		"scripts": ["background.js"],
		"persistent": false
	},
	"permissions": [
		"contextMenus",
		"tabs"
	],
	"icons": {
		"16": "16.png"
	}
}

background.js

idで「test extention」を指定しています。
コンテキストメニューはイベントページだとIDを指定しないと表示されないので注意してください。

chrome.contextMenus.create({id: "test extention", title: "hoge"}, function(){
	alert("コンテキストメニュー登録完了");
});

chrome.contextMenus.onClicked.addListener( function(){
	alert("onClickedイベントでクリック");
});

Test Extention 6のダウンロード

機能拡張ページでTest Extention 6を読み込みます。
すると通常通り以下のように表示されます。

そのまま十数秒待ってください。すると下の画像のように(無効)と表示されます。

これで、無事にイベントページを実装できた…」とはなりません。
試しに表示が(無効)になった状態で右クリックから「hoge」をクリックしてみてください。
本来であれば「onClickedイベントでクリック」だけが読み込まれるはずですが、「コンテキストメニュー登録完了」も呼び出されてしまいます。

つまりイベントページ全体を実行したことがわかります。このようにドキュメントを一読しただけでは理解しにくい不思議な挙動をするので、安易にイベントページを導入すると落とし穴にハマってしまいます。


イベントページではまりやすいポイント

更にイベントページの特性をわかりやすくするためにgetBackgroundPageを使って解説します。

getBackgroundPageは名前から分かる通り、バックグラウンドのWindowオブジェクトを取得できるメソッドです。
取得したオブジェクトは普通のWindowオブジェクトと同じように利用できます。

manifest.json

Test Extention 7を作り、バージョンを0.7へ変更。

{
	"manifest_version": 2,
	"name": "Test Extention",
	"version": "0.7",
	"browser_action": {
		"default_popup": "popup.html",
		"default_icon": "19px.png",
		"default_title": "Hello Worldを表示するよ"
	},
	"background": {
		"scripts": ["background.js"],
		"persistent": false
	},
	"permissions": [
		"contextMenus",
		"tabs"
	],
	"icons": {
		"16": "16.png"
	}
}

background.js

backgroundFunctionという名前の関数を作ります。

function backgroundFunction() {
	alert("バックグラウンドの関数を実行!");
}

myscript.js

getBackgroundPageでバックグラウンドページのwindowオブジェクトを取得して、backgroundFunctionメソッドを実行。

var bgWindow = chrome.extension.getBackgroundPage();
console.log(bgWindow.backgroundFunction());

機能拡張ページでTest Extention 7を読み込み、ツールバーのアイコンをクリックしてください。
すると「バックグラウンドの関数を実行!」というアラートが表示されるはずです。

このようにバックグラウンドページと、ブラウザアクションページは分離されていますが、APIを利用すれば相互にやり取りすることができます。

では、そのまましばらく待って機能拡張のバックグラウンドページに(無効)が表示されるまで待ちます。
(無効)が表示されたら再びツールバーのアイコンをクリックしてください。ポップアップが表示されるだけでアラートが表示されないと思います。

これはイベントページが無効になったため、getBackgroundPageでWindowオブジェクトが取得できないためです。

ではどうすればいいでしょうか?

公式ドキュメントのConvert background page to event pageの項目にruntime.getBackgroundPageに変えるとあります。

そこでruntimeのgetBackgroundPageメソッドの解説を見ると「コールバック関数を実行時にバックグラウンドページが読み込まれていることを確認する」とあります。

試してみましょう。

myscript.js

extensionからruntimeに置き換えてコールバックで引数に入ったWindowオブジェクトのメソッドを実行しています。

var bgWindow = chrome.runtime.getBackgroundPage(function( backgroundPage ){
	backgroundPage.backgroundFunction();
});

バックグラウンドページが(無効)の状態でツールバーのアイコンをクリックしてください。今度は「バックグラウンドの関数を実行!」と表示されると思います。

これでイベントページとして読み込んだバックグラウンドページが(無効)になっても必要に応じてイベントページの関数を読み込むことができるようになりました。

実はもうひとつはまりやすいポイントがあります。

background.js

関数の外に「関数の外にあるもの」というアラートを追加します。

function backgroundFunction () {
	alert("バックグラウンドの関数を実行!");
}

alert("関数の外にあるもの");

機能拡張をリロードします。
すると初回の読み込みなので「関数の外にあるもの」のアラートが表示されます。では再び(無効)の表示が出た後に、ツールバーのアイコンをクリックしてください。

すると「関数の外にあるもの」のアラートが表示されます。これはruntime.getBackgroundPageを実行するとイベントページ全体を読み込んでしまうからです。

ページアクション側でruntimeを使って工夫したように、イベントページでも工夫する必要があります。

background.js

機能拡張のインストール時に実行されるruntime.onInstalledと、起動時に実行されるruntime.onStartupを利用して、アラートを関数内で実行するようにします。

function backgroundFunction () {
	alert("バックグラウンドの関数を実行!");
}

chrome.runtime.onInstalled.addListener(outsideAlert);
chrome.runtime.onStartup.addListener(backgroundFunction);

function outsideAlert() {
	alert("関数の外にあるもの");
}

Test Extention 7のダウンロード

機能拡張をリロードすると「関数の外にあるもの」がアラート表示され、バックグラウンドページが(無効)になったあとに、ツールバーのアイコンをクリックしても「バックグラウンドの関数を実行!」だけが実行されます。

このように初回起動時の処理を関数にまとめてイベントリスナーに登録することで、イベントページを呼び出した時に連動して実行されなくなります

このonInstalledonStartupの組み合わせはよく使うのでセットで覚えておくと便利です。

もう1つ、はまりやすいポイントである「データの渡し方」も見ていきます。


メッセージパッシングの仕組みと、イベントページで実装するポイント

バックグラウンドページコンテンツスクリプトやアクションページとの間でデータを相互にやりとりする方法を解説します。
合わせてはまりやすいイベントページでの実装方法を解説します。

データの送受信はMessage Passing(メッセージパッシング)という仕組みで実装します。

Message Passingの公式ドキュメント

詳細は公式ドキュメントに委ねるとして、簡単に説明すると、メッセージパッシングでの送受信にはJSON形式を用い、データの型は「null, boolean, number, string, array, object」を渡すことができます。

メッセージの送信にはchrome.runtime.sendMessageか、chrome.tabs.sendMessageを使い、メッセージの受信は共にchrome.runtime.onMessage.addListenerを使う。という仕組みです。

runtime.sendMessageとtabs.sendMessageの使い分けですが、コンテンツスクリプトに送信するときだけtabs.sendMessageを使います

では、実際に作成してみます。

manifest.json

Test Extention 8を作り、バージョンを0.8へ変更。

{
	"manifest_version": 2,
	"name": "Test Extention",
	"version": "0.8",
	"browser_action": {
		"default_popup": "popup.html",
		"default_icon": "19px.png",
		"default_title": "Hello Worldを表示するよ"
	},
	"background": {
		"scripts": ["background.js"],
		"persistent": false
	},
	"permissions": [
		"tabs"
	]
}

myscript.js

まずは一番単純な方法でメッセージを送信。

runtime.sendMessagemsmessageという名前のメッセージを送信します。

chrome.runtime.sendMessage({ msmessage: "myscriptからのメッセージ!" });

background.js

runtime.onMessage.addListenerでメッセージを受けるイベントリスナーを作成。
if( message.asmessage )の部分は、メッセージの名前がmsmessageの場合は実行するという仕組みです。複数のメッセージがある場合はよく使う方法なのでセットで覚えておいてください。

chrome.runtime.onMessage.addListener(function( message, sender, sendResponse ) {
	if( message.msmessage ){
		console.log(message.msmessage);
	}
});

Test Extention 8のダウンロード

機能拡張ページでリロードし、ビューを検証の「バックグラウンド ページ」をクリックしてコンソールを表示
その状態でツールバーのアイコンをクリックしてください。

するとバックグラウンドページ用のコンソールで「myscriptからのメッセージ!」とログが流れます。

このように本来であれば、バックグラウンドページとコンテンツスクリプトなどはデータのやり取りができないところをやり取りが可能になります。


バックグラウンドページからページアクション側へメッセージを送信する

今度は逆にバックグラウンドページからページアクションへメッセージを送信してみます。
実はイベントページとして動作しているバックグラウンドページからメッセージを取得するのは厄介です。

manifest.json

Test Extention 9を作り、バージョンを0.9へ変更。

{
	"manifest_version": 2,
	"name": "Test Extention",
	"version": "0.9",
	"browser_action": {
		"default_popup": "popup.html",
		"default_icon": "19px.png",
		"default_title": "Hello Worldを表示するよ"
	},
	"background": {
		"scripts": ["background.js"],
		"persistent": false
	},
	"permissions": [
		"tabs"
	]
}

myscript.js

chrome.runtime.onMessage.addListenerでイベントリスナーを登録。bgmessageという名前のメッセージの場合、コンソールに表示します。

バックグラウンドページを呼び出すにはchrome.runtime.getBackgroundPageを使います。
また、ページが表示される前に実行されるとうまくコンソールに表示されないためwindow.addEventListener(‘load’, function() {})で実行のタイミングを指定しています。

chrome.runtime.onMessage.addListener(function( message, sender, sendResponse ) {
	if( message.bgmessage ){
		console.log(message.bgmessage);
	}
	return true;
});

window.addEventListener('load', function() {
	chrome.runtime.getBackgroundPage(function( backgroundPage ){
		backgroundPage.backgroundFunction();
	});
});

background.js

myscriptから呼び出せるようにbackgroundFunction()という関数を作成してメッセージを送信しています。

function backgroundFunction(){
	chrome.runtime.sendMessage({ bgmessage: "バックグラウンドページからのメッセージ!" });
};

Test Extention 9のダウンロード

機能拡張ページで読み込んでツールバーのアイコンを右クリックして、「ポップアップを検証」をクリック。

すると「バックグラウンドページからのメッセージ!」とコンソールログに流れます。
これでバックグラウンドページのメッセージをブラウザアクションで表示することができました。
組み合わせれば、コンテンツスクリプトから動的にデータを渡して、バックグラウンドページで処理、ポップアップで受け取って表示を変えるといった複雑な処理もできるようになります。


チャンネルを作成して双方向の通信を行う

sendMessageonMessageは一方通行でしたが、チャンネルを作成して永続的な通信を行うこともできます。

これも「Message Passingの公式ドキュメント」で解説されているのですが、サンプルを見ただけだと理解しにくいと思うので段階を追って解説します。

Message Passingの公式ドキュメント

まず、チャンネルを作ってmyscript.jsからバックグラウンドページにメッセージを送る方法を理解します。

manifest.json

Test Extention 10を作り、バージョンを1.0へ変更。ブラウザアクションとバックグラウンドページを利用します。

{
	"manifest_version": 2,
	"name": "Test Extention",
	"version": "1.0",
	"browser_action": {
		"default_popup": "popup.html",
		"default_icon": "19px.png",
		"default_title": "Hello Worldを表示するよ"
	},
	"background": {
		"scripts": ["background.js"],
		"persistent": false
	}
}

myscript.js

background.jsへメッセージを送ります。
まずchrome.runtime.connecthogeChannelという名前のチャンネルを作成してportに代入します。
そのチャンネルに対してpostMessageでメッセージを送信します。

var port = chrome.runtime.connect({name: "hogeChannel"});
port.postMessage({msMessage: "myscript.jsからのメッセージ!"});

background.js

まずはmyscript.jsで送信されたチャンネルをchrome.runtime.onConnect.addListenerで待ち受けます。
コールバックで引数にチャンネルが入っているので名前を確認して分岐します。
続いてチャンネルに入ったmessageで分岐してメッセージを受けます。

chrome.runtime.onConnect.addListener(function(port) {
	if(port.name == "hogeChannel"){
		port.onMessage.addListener(function(msg) {
			if(msg.msMessage == "myscript.jsからのメッセージ!"){
				console.log(msg.msMessage);
			}
		});
	}
});

機能拡張を読み込んでバックグラウンドページのコンソールを開いた状態で、ツールバーのアイコンをクリックしてmyscript.jsを実行します。

するとコンソールログに「myscript.jsからのメッセージ!」とログが流れます。

これでチャンネルとメッセージの二段階で設定してメッセージの送受信を行う流れを理解していただけたと思います。

これだけだとただ面倒になっただけですが、Connectを使うとバックグラウンドページからの応答をmyscript.js側で受信することができます。

myscript.js

port.onMessage.addListenerを追加して応答のメッセージを待ち受けます。

var port = chrome.runtime.connect({name: "hogeChannel"});
port.postMessage({msMessage: "myscript.jsからのメッセージ!"});

port.onMessage.addListener(function(msg) {
	console.log(msg.answer);
});

background.js

port.postMessageでメッセージを受信したことを作成したチャンネル宛に返します。

chrome.runtime.onConnect.addListener(function(port) {
	if(port.name == "hogeChannel"){
		port.onMessage.addListener(function(msg) {
			if(msg.msMessage == "myscript.jsからのメッセージ!"){
				console.log(msg.msMessage);
				port.postMessage({answer: "メッセージ届いたよ!"});
			}
		});
	}
});

Test Extention 10のダウンロード

再び機能拡張をリロードして、今度はツールバーのアイコンを右クリックして「ポップアップを検証」をクリックします。
するとバックグラウンドページからの応答である「メッセージ届いたよ!」がログに流れます。

myscript.js側ではチャンネルの分岐をしていないこともポイントです。上で作成したportのチャンネルに対する応答は、port.onMessage.addListenerとすることで限定して受信することができます。

このようにConnectを利用すれば、バックグラウンドページやコンテンツスクリプトの特性を活かしつつ、メッセージを相互に送受信することができます。

では、早速グローバル変数なんかを登録して…」となると思いますが、メッセージパッシングはそういった用途には向いていません。

イベントリスナーを実行し続けるのでいつまで経っても(無効)になりません。また、変数を取得するたびにバックグラウンドページが実行されるため、イベントページのメリットである、終了してメモリを開放することができなくなります。

単純に変数を渡すにはもっと良い方法が用意されています。


ストレージを利用して変数をやり取りする

名前から分かる通りメモリ上にデータを保存するstorageのAPIです。
通常のstorageはバックグラウンドページとコンテンツスクリプト間でのやり取りはできませんが、chrome.storage APIなら保存したデータを相互にやり取りできます。
また値には文字列だけでなく、オブジェクトなんかも渡せます。

また、セットしておいた値を取得するだけならバックグラウンドページを呼び出す必要もないため、イベントページとの相性も良くメモリ上に展開されるので速度も高速です。

chrome.storage APIの公式ドキュメント

manifest.json

Test Extention 12を作り、バージョンを1.2へ変更。
公式ドキュメントにある通り、permissionsにstorageを追加。

{
	"manifest_version": 2,
	"name": "Test Extention",
	"version": "1.2",
	"browser_action": {
		"default_popup": "popup.html",
		"default_icon": "19px.png",
		"default_title": "Hello Worldを表示するよ"
	},
	"background": {
		"scripts": ["background.js"],
		"persistent": false
	},
	"permissions": [
		"storage"
	]
}

background.js

chrome.storage.local.setで名前と値をセット。

chrome.storage.local.set({foo : "fooVal"});

myscript.js

chrome.storage.local.getでオブジェクトを取得します。第一引数はnullにすると全てのストレージオブジェクトを取得できます。
コールバックで処理も行えるのでコードの見通し良いですね。

chrome.storage.local.get("foo", function(items) {
	console.log(items.foo);
});

Test Extention 12のダウンロード

機能拡張ページで読み込んだら(無効)の表示まで十数秒待ち、ポップアップを右クリックし「ポップアップを検証」をクリック。

バックグラウンドページが(無効)のまま、変数が取得できているのがわかると思います。
このように単純に変数を渡すだけならchrome.storage APIを利用するのが最も簡単です。chrome.storage APIには他の機能拡張とデータのやり取りを行う機能もあるので気になる方は公式ドキュメントをご覧ください。


アラームを利用して長時間のディレイを行う

実行するタイミングの制御ですが、普通のJavaScriptであればsetTimeout()を使いますが、イベントページの場合は10数秒で(無効)になってしまうため、長時間のディレイが必要な場合はchrome.alarms APIを利用します。

chrome.alarms APIの公式ドキュメント

manifest.json

Test Extention 11を作り、バージョンを1.1へ変更。
公式ドキュメントにある通り、permissionsalarmsを追加。長時間の時間差を試すだけなのでバックグラウンドページのみ。

{
	"manifest_version": 2,
	"name": "Test Extention",
	"version": "1.1",
	"background": {
		"scripts": ["background.js"],
		"persistent": false
	},
	"permissions": [
		"alarms"
	]
}

background.js

chrome.alarms.createでmyAlarmという名前のdelay時間が1分のアラームを作成。
chrome.alarms.onAlarm.addListenerでアラームのイベントリスナーを作り、アラームネームで分岐しています。
一応解説したメッセージも合わせて実行できるということでsendMessageを使ってアラート表示。

chrome.alarms.create("myAlarm", {delayInMinutes: 1} );

chrome.alarms.onAlarm.addListener(function(alarm) {
	if ( alarm.name == "myAlarm" ){
		chrome.runtime.sendMessage({ bgmessage: "1分後に実行!" });
	}
});

chrome.runtime.onMessage.addListener(function( message, sender, sendResponse ) {
	if( message.bgmessage ){
		alert(message.bgmessage);
	}
});

Test Extention 11のダウンロード

機能拡張ページで読み込んみ、しばらく待つと(無効)表示になると思います。
その後、1分経つとアラートが表示され、再びバックグラウンドページが有効になります。

ちなみにアラートでバックグラウンドページ全体が読み込まれるので再び1分経つとアラートが表示されます。
普通はchrome.alarms.createalarmInfoに第2引数としてperiodInMinutesを指定してインターバルを指定したり、runtime.onInstalled等で一度だけ実行という形になると思います。


長々解説しましたが、バックグラウンドページ(イベントページ)、コンテンツスクリプト、ページアクションといった異なる空間でも関数や変数などのやり取りができることが理解していただけたと思います。

イベントページは少し癖がありますが、Chrome全体のメモリ消費が抑えられるというユーザーにとって大きなメリットがあるので、積極的に取り入れていきましょう。


Chromeのエラーの話し

急に解説の流れと関係ない話ですが、なぜか「Test Extention 8」という名前のディレクトリでbackgroundPage.postGoikenurlを使うと以下のエラーが出て動作しません。

Error in response to runtime.getBackgroundPage: TypeError: backgroundPage.backgroundFunction is not a function
    at chrome-extension://cgfhkbnhooimbjamenbkpfgjfjnkkini/myscript.js:39:17
    at chrome-extension://cgfhkbnhooimbjamenbkpfgjfjnkkini/myscript.js:13:16
    at chrome-extension://cgfhkbnhooimbjamenbkpfgjfjnkkini/myscript.js:38:31
    at chrome-extension://cgfhkbnhooimbjamenbkpfgjfjnkkini/myscript.js:13:16handler @ extensions::uncaught_exception_handler:8


何らかのChrome側のエラーと思われます。
絶対に動作するはずのスクリプトが動作しない。といった場合にはディレクトリ名を変更するのもいいかもしれません。

次の投稿では「コンテンツスクリプト」について解説します。