Chromeエクステンションを作ろう：バックグラウンドページ編(＋GoogleAnalytics)


説明で使用した拡張機能は[こちら](https://chrome.google.com/webstore/detail/redmine-lazy-applicant/dofmgcleeckgckfapnhookpocglpbmgp)からインストールできます. 一人でも多くの方に利用いただければ幸いです.

#### 改版
~~ソースコードは[こちら](https://bitbucket.org/dhun-h/lazy-applicant)から閲覧できます.~~

最新版のソースコードでは、GoogleAnalyticsを動作しないように変更しました.
ソースコードは[v2.1.0](https://bitbucket.org/dhun-h/lazy-applicant/src/66414c7fe261c4862a5851a40b52fd1268b9aedd/?at=v2.1.0)を閲覧してください.


## バックグラウンドページとは

Chromeに拡張機能がインストールされてから、常駐し続けるページになります.
バックグラウンドページでは、[Chrome-Javascript-API](https://developer.chrome.com/extensions/api_index)の(恐らく)すべてを利用することができます.

Chromeの起動中は常駐し続けるアーキテクチャのため、リソース使用量を最適化する目的から[イベントページ](https://developer.chrome.com/extensions/event_pages)が追加されていました.
これから拡張機能でバックグラウンド機能を実装される方は、イベントページを選択された方が良さそうです.


プラグインでは、バックグラウンドページで実装しました.
主にGoogleAnalyticsを利用した統計情報収集処理に利用しています.

詳細は[公式サイト](https://developer.chrome.com/extensions/background_pages)を参照してください.

## プラグインで実装したこと
* GoogleAnalyticsの統計情報収集処理のオプトアウト切替
* GoogleAnalyticsの統計情報収集処理の実行
* マニフェストファイルのバージョン番号を取得

コンテントスクリプトの対象Webページから、GoogleAnalyticsの統計収集処理を呼び出すことはできません.
これはバックグラウンドページからGoogleAnalyticsの統計収集処理を呼び出すことで対処できます.



## マニフェストファイルの設定 (抜粋)

```json:manifest.json
   ,"background": {
        "scripts": [
            "lib/jquery-ui-1.11.0/external/jquery/jquery.js"
           ,"js/env.js"
           ,"js/common/define.js"
           ,"js/common/functions.js"
           ,"js/common/db-functions.js"
           ,"js/common/ga-functions.js"
           ,"js/background/background.js"
        ]
   }

   ,"content_security_policy": "script-src 'self' https://ssl.google-analytics.com; object-src 'self'"
```

詳細は[公式サイト](https://developer.chrome.com/extensions/background_pages#manifest)を参照してください.

### scripts

バックグラウンドページで利用するJavascriptファイルを指定します.
これは指定した順番に適用されます.

プラグインでは、background.js以外は次のような実装で振る舞いの定義だけをしています.

```js
var lazyApp = lazyApp || {};
!function() {
    'use strict';
    
    // 外部呼び出しのあるメソッド
    lazyApp.xxx = {
        hoge: function() {
        	foo();
        },
        fuga: function() {
        	this.hoge();
        }
    };
    
    // 外部呼び出しのない内部関数
    function foo() {
    }
    function goo() {
    	lazyApp.xxx.fuga();
    }
}();
```


最後に読み込まれるbackground.jsでは、次のような実装により必要な処理の呼び出しを制御しています.

```js:background.js
!function() {
    'use strict';

    !function() {
        // フォアグラウンドとの通信用ポートを準備
        chrome.extension.onConnect.addListener(listenPort);

        // GoogleAnalyticsの準備
        lazyApp.GaFuncs.injectGaScript();
    }();
```


### content_security_policy

GoogleAnalyticsを利用する場合は、前述した値を指定するそうです.
公式サイトの[チュートリアル](https://developer.chrome.com/extensions/tut_analytics#toc-installing)に記載されていました.

## メッセージ送受信の仕組み

* コンテントスクリプトから、バックグラウンドページの処理を呼び出す
* オプション画面から、バックグラウンドページの処理を呼び出す

ような場合には、[メッセージ送受信](https://developer.chrome.com/extensions/messaging#connect)の仕組みを利用します.

処理手順は次のようになります.

1. 処理を受信する側が、拡張機能内の通信用ポートにイベントハンドラを追加する
2. 処理を要求する側が、通信用ポートに対して処理要求メッセージを送信する
3. 処理を受信する側のイベントハンドラが実行される

### 補足

~~処理要求のたびに接続するのは無駄なので、接続したポートは変数に保持します.~~
~~チケット登録画面を開いた時、およびオプション画面を開いた時に１回だけ接続するイメージです.~~

このプラグインでは、バックグラウンドページとの接続処理コストを最適化するつもりで通信用ポートを変数に保持していました.
しかしこの実装ではメモリリークすることに気づいて（記事を書いている時に気づいた orz）、通信後に接続を切断する単純な手続きに変更しました.
このような単純なメッセージのやりとりには[chrome.runtime.sendMessage](https://developer.chrome.com/extensions/messaging#simple)を利用したほうが良さそうです.

## メッセージ送受信を利用したGoogleAnalyticsによる統計収集

次のような処理手順で実装しました.

1. バックグラウンドページで、通信要求に対するイベントハンドラを定義する
2. バックグラウンドページで、GoogleAnalyticsが提供するJavascriptを読み込む
3. コンテントスクリプトやオプション画面で、バックグラウンドページにメッセージを送信する

順番に解説します.
ソースコードは解説しやすいように抜粋しますので、詳細は[説明で紹介したプラグインソース](#%E8%AA%AC%E6%98%8E%E3%81%A7%E7%B4%B9%E4%BB%8B%E3%81%97%E3%81%9F%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E3%81%AE%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89)を参照してください.

### 通信要求に対するイベントハンドラの定義

バックグラウンドページで[chrome.runtime.onConnect.addListener](https://developer.chrome.com/extensions/runtime#event-onConnect)を呼び出します.
これは、コンテントスクリプトなどのメッセージを送信する側からの接続要求に対するイベントハンドラになります.

後述する接続要求処理では機能毎にポート名をつけているため、イベントハンドラでは接続要求のあったポート名毎にメッセージを監視しています(`port.onMessage.addListener`).

また接続が切断された時には、ポートからイベントリスナを破棄しています(`port.onDisconnect.addListener`).
バックグラウンドページは常駐し続けるため、このようにしないと接続要求側のインスタンスが開放されないはずです.

```js:background.js
!function() {
    !function() {
        // フォアグラウンドとの通信準備
        chrome.extension.onConnect.addListener(listenPort);
    }();
    
    function listenPort(port) {
        var messageListener;
        switch (port.name) {
        case PORT_GA_TRACK_EVENT:
            messageListener = function(param) {
                pushGaTrackEvent(param);
            };
            break;

        // 中略

        default:
            lazyApp.Funcs.logError('unknown port. name=[' + port.name + ']');
        }

        var disconnectListener = function() {
            // イベントリスナを破棄
            port.onMessage.removeListener(messageListener);
            port.onDisconnect.removeListener(disconnectListener);
            lazyApp.Funcs.logDebug('リスナを削除しました. ' + port.name);
        };
        
        // 通信を監視
        port.onMessage.addListener(messageListener);
        port.onDisconnect.addListener(disconnectListener);
    }
    
    function pushGaTrackEvent(param) {
        if (!_agree) {
            return;
        }

        lazyApp.GaFuncs.pushGaTrackEvent(param.category, param.action);
    }
}();
```

```js:ga-functions.js
var _gaq = _gaq || [];
!function() {
    lazyApp.GaFuncs = {
        pushGaTrackEvent: function(category, action) {
            action = category + '/' + action;
            var label = null;
            var value = null;
            var opt_noninteraction = true;
            _gaq.push(['_trackEvent', category, action,
                       label, value, opt_noninteraction]);
        },
    };
}();
```


### GoogleAnalyticsが提供するJavascriptを読み込み

バックグラウンドページで、[GoogleAnalyticsが提供するJavascript](https://ssl.google-analytics.com/ga.js)を読み込ませます.
このJavascriptによりグローバル変数`_gaq`が初期化(というより設定追加)されます.

後続する処理要求に備え、GoogleAnalyticsのトラッキングIDをグローバル変数`_gaq`に設定しておきます.

```js:background.js
!function() {
    !function() {
        // GoogleAnalyticsの準備
        lazyApp.GaFuncs.injectGaScript();
    }();
}();
```

```js:ga-functions.js
var _gaq = _gaq || [];
!function() {
    _gaq.push(['_setAccount', GA_TRACKING_ID]);

    lazyApp.GaFuncs = {
        injectGaScript: function() {
            lazyApp.Funcs.injectElement('script', {
                type: 'text/javascript',
                async: true,
                src: 'https://ssl.google-analytics.com/ga.js'
            });
        },
    };
}();
```

```js:functions.js
!function() {
    lazyApp.Funcs = {
        injectElement: function(element, options) {
            element = document.createElement(element);
            for (var key in options) {
                element[key] = options[key];
            }
            (document.head||document.documentElement).appendChild(element);
        }
    };
}();
```

### バックグラウンドページにメッセージを送信

コンテントスクリプトやオプション画面で、[chrome.runtime.connect](https://developer.chrome.com/extensions/runtime#method-connect)を呼び出してバックグラウンドページとの接続を確立します.
接続には、機能毎に名前をつけています.

続いてメッセージを送信します(`port.postMessage`).
この処理によって、バックグラウンドページ側のイベントハンドラ(`messageListener`)が実行されます.

最後に接続を切断します(`port.disconnect`).
この処理によって、バックグラウンドページ側のイベントハンドラ(`disconnectListener`)が実行されます.

```js:post-load.js
!function() {
    'use strict';

    lazyApp.GaFuncs.pushGaTrackEventOnBackground(GA_CATEGORY_ISSUE_ENTRY, '初期表示');
}();
```

```js:ga-functions.js
var _gaq = _gaq || [];
!function() {
    'use strict';

    lazyApp.GaFuncs = {

        pushGaTrackEventOnBackground: function(category, action) {
            postMessage(PORT_GA_TRACK_EVENT, {
                category: category,
                action: action
            });
        }
    };

    function postMessage(portName, param) {
        var port = chrome.extension.connect({ name: portName });
        port.postMessage(param);
        port.disconnect();
    }
}();
```


## tips

### マニフェストファイルからバージョン番号の取得方法

プラグインでは、次のようにマニフェストファイルからバージョン番号を取得しています.
`JSON.parse`を利用しているため、マニフェストファイルにコメントを含めることはできません.

```js:background.js
resolveManifestVersion(function(manifest) {
    _version = manifest.version;
});

function resolveManifestVersion(callback) {
    var url = '/manifest.json';
    var xhr = new XMLHttpRequest();
    xhr.onload = function() {
        callback(JSON.parse(xhr.responseText));
    };
    xhr.open('GET', url, true);
    xhr.send(null);
}
```


### GoogleAnalyticsのプロパティについて

GoogleAnalyticsでは、アカウント＞プロパティのような階層構造で統計情報を管理します.
プラグインでは次のようにしました.

* LazyApplicant(アカウント)
	* product(プロパティ) ... Webストアで公開したプラグインの統計情報を管理
	* develop(プロパティ) ... 開発中のプラグインの統計情報を管理

このプロパティには１から始まる連番が割り当てられます.
商用環境は１番で管理したくなりますが、避けたほうが良さそうです.

プラグインの開発中に２度ほどアカウントごと作りなおしましたが、１番や２番など若い番号のプロパティには **ロシアからの謎のアクセス** がありました(プラグインはまだ未公開ｗ)

### バックグラウンドページからのページビューのトラッキング

バックグラウンドページには、Chromeが_generated_background_page.htmlという名前のHTMLを生成します.
紹介した処理方式で単純にページビューをトラッキングすると、すべてこのページが閲覧されたことになります.

プラグインでは`_gaq.push(['_trackPageview'])`にパラメータを追加することで、閲覧されたページを特定しています.
ただしドメインを特定するような情報は収集しない方針のため、次のようにURLを加工をしています.

```js
var pageUrl = location.pathname;
pageUrl = '/v' + _version + pageUrl;

var match = pageUrl.match(/.+\/projects\/.+\/(issues(\/new.*)?)/, '');
if (match) {
    pageUrl = '/projects/***/' + match[1];
}

_gaq.push(['_trackPageview', pageUrl]);
```

#### 収集されるページビュー

![analytics.png](https://qiita-image-store.s3.amazonaws.com/0/74102/5c28861f-ab8a-6175-6e95-bf8bfb7e8140.png "analytics.png")


## 説明で紹介したプラグインのソースコード
* [manifest.json](https://bitbucket.org/dhun-h/lazy-applicant/src/f371e1be0e9d66311bf81555e098b71c25886eb7/src/manifest.json?at=master&fileviewer=file-view-default)
* [background.js](https://bitbucket.org/dhun-h/lazy-applicant/src/f371e1be0e9d66311bf81555e098b71c25886eb7/src/js/background/background.js?at=master&fileviewer=file-view-default)
* [ga-functions.js](https://bitbucket.org/dhun-h/lazy-applicant/src/f371e1be0e9d66311bf81555e098b71c25886eb7/src/js/common/ga-functions.js?at=master&fileviewer=file-view-default)
* [functions.js](https://bitbucket.org/dhun-h/lazy-applicant/src/f371e1be0e9d66311bf81555e098b71c25886eb7/src/js/common/functions.js?at=master&fileviewer=file-view-default)