Chrome拡張開発において癖があるのがEvent Pageにかかわる処理になるかと思います。その中でも、ライフスパンの長さがことなるコンテキストメニューを扱う場合の注意点をご紹介します。
Event Pageとは
event page(以下、イベントページ)とはmanifest.json
でpersistent
をfalse
に設定されたbackground pageのことです。
{
...
"background": {
"scripts": ["background.js"],
"persistent": false
}
...
}
ライフサイクル
イベントページは特定のタイミングで読み込まれ、処理が終了すると閉じられます。これは再度読み込まれたときにはグローバル変数を含めてすべてリセットされることを意味します。
イベントページが読み込まれるタイミングは下記になります。
- 拡張のインストールまたはアップデート時
- イベントリスナーを登録したイベントが発火したとき
-
getBackgroundPage
メソッドが他から呼ばれたとき
基本動作としては1でイベントリスナーを登録し、2で処理を行います。
Event Pageでコンテキストメニューを扱う
イベントページについて理解したところで、本題であるコンテキストメニューを扱う上での注意点を紹介します。
コンテキストメニューの生成はonInstalledイベントで
イベントページは先に説明したようにイベントが発生するたびに何度も読み込まれます。
このため、グローバルなコンテキストでメニュー生成を行うと、何度もメニューが生成されることになってしまいます。
これを回避するためには、chrome.runtime.onInstalled
イベントに登録したイベントリスナーでコンテキストメニューの生成を行います。
chrome.runtime.onInstalled.addListener(function () {
chrome.contextMenus.create({
type: 'normal',
id: 'hello',
title: 'Say hallo'
});
});
createProperties.onclickは指定してはならない
コンテキストメニュー作成時に呼び出すchrome.contextMenus.creat
関数の第一引数のcreateProperties
には、コンテキストメニューがクリックされた際のイベントリスナーを登録するプロパティonclick
が存在します。このプロパティーに関してAPIドキュメントには次のような記述があります。
A function that will be called back when the menu item is clicked. Event pages cannot use this; instead, they should register a listener for chrome.contextMenus.onClicked.
要約するとイベントページでは使えないので代わりにchrome.contextMenus.onClick
にイベントリスナーを用いてくださいということです。
イベントページでこの使われないこのプロパティに関数を与えると、一見無視されそうです。
しかし、実際に設定するとコンテキストメニューが表示されません。
もし、イベントページでコンテキストメニューが表示されない場合にはチェックする必要があります。
イベント処理に使う変数をonInstalledイベントで生成してはいけない
前述のようにイベントページでコンテキストメニューを処理する場合、chrome.contextMenus.onClick
イベントを用います。
このイベントは、開発している拡張で作成したコンテキストメニューのどれかがクリックされたタイミングで発火します。
そして、このイベントのイベントリスナーではどのコンテキストメニューが呼ばれたのかをcreateProperties
で指定したid
を用いてディスパッチする必要があります。
さて、ここでディスパッチ処理とコンテキストメニュー生成を分離するために次のクラスを用意しました。
var ContextMenus = function () {
var items = this.items = {};
chrome.contextMenus.onClicked.addListener(function (info, tab) {
items[info.menuItemId].onclick(info, tab);
});
};
ContextMenus.prototype = {
create: function (properties) {
this.items[properties.id] = {
onclick: properties.onclick
};
properties.onclick = null;
chrome.contextMenus.create(properties);
}
};
次に、コンテキストメニューの生成はonInstalled
イベントで行うということだったので次の処理を追加します。
chrome.runtime.onInstalled.addListener(function () {
var contextMenus = new ContextMenus();
contextMenus.create({
type: 'normal',
id: 'hello',
title: 'Say hallo',
onclick: sayHallo
});
});
function sayHallo () {
console.log('hello');
}
このコードを動かしてみるとバックグラウンドページのコンソールに"hello"と表示されます。
さて、しばらくたってからコンテキストメニューの"Say hallo"を選択してみましょう。
すると、なんと言うことでしょう、今度はコンソールに何も表示されないのです。
上記のコードの問題点は次の2点になります。
-
onClicked
イベントへのリスナー登録がonInstalled
イベント時のみ行われる - 各メニューのイベントハンドラを保存している変数の初期化が
onInstalled
イベント時のみ行われる
この結果onClicked
イベントが処理できないのです。
この現象のいやらしいところは、開発中で頻繁に拡張の再読み込みをしているとonInstalled
イベントが発生した直後であることが多く、イベントページが生き残っており、このタイミングに限りonClicked
イベントが処理できてしまうことでしょう。
では、最後に先ほどのコードを修正したものをお見せします。
var ContextMenus = new function () {
var items = {};
var callbacks = {};
this.setItems = function (aItems) {
aItems.forEach(function (item) {
callbacks[item.id] = item.onclick;
item.onclick = null;
items[item.id] = item;
});
};
this.create = function () {
Object.keys(items).forEach(
function (key) {
chrome.contextMenus.create(items[key]);
}
);
};
chrome.contextMenus.onClicked.addListener(function (info, tab) {
callbacks[info.menuItemId](info, tab);
});
};
ContextMenus.setItems([
{
type: 'normal',
id: 'hello',
title: 'Say hallo',
onclick: sayHallo
}
]);
chrome.runtime.onInstalled.addListener(ContextMenus.create);