23
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Chrome拡張のEventPageでchrome.contextMenusを正しく動かす

Last updated at Posted at 2015-02-13

Chrome拡張開発において癖があるのがEvent Pageにかかわる処理になるかと思います。その中でも、ライフスパンの長さがことなるコンテキストメニューを扱う場合の注意点をご紹介します。

Event Pageとは

event page(以下、イベントページ)とはmanifest.jsonpersistentfalseに設定されたbackground pageのことです。

{
    ...
    "background": {
        "scripts": ["background.js"],
        "persistent": false
    }
    ...
}

ライフサイクル

イベントページは特定のタイミングで読み込まれ、処理が終了すると閉じられます。これは再度読み込まれたときにはグローバル変数を含めてすべてリセットされることを意味します。

イベントページが読み込まれるタイミングは下記になります。

  1. 拡張のインストールまたはアップデート時
  2. イベントリスナーを登録したイベントが発火したとき
  3. 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);
23
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?