14
17

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エクステンションを作ろう:バックグラウンドページ編(+GoogleAnalytics)

Last updated at Posted at 2015-09-22

説明で使用した拡張機能はこちらからインストールできます. 一人でも多くの方に利用いただければ幸いです.

改版

ソースコードはこちらから閲覧できます.

最新版のソースコードでは、GoogleAnalyticsを動作しないように変更しました.
ソースコードはv2.1.0を閲覧してください.

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

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

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

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

詳細は公式サイトを参照してください.

プラグインで実装したこと

  • GoogleAnalyticsの統計情報収集処理のオプトアウト切替
  • GoogleAnalyticsの統計情報収集処理の実行
  • マニフェストファイルのバージョン番号を取得

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

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

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'"

詳細は公式サイトを参照してください.

scripts

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

プラグインでは、background.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では、次のような実装により必要な処理の呼び出しを制御しています.

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

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

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

content_security_policy

GoogleAnalyticsを利用する場合は、前述した値を指定するそうです.
公式サイトのチュートリアルに記載されていました.

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

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

ような場合には、メッセージ送受信の仕組みを利用します.

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

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

補足

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

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

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

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

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

順番に解説します.
ソースコードは解説しやすいように抜粋しますので、詳細は説明で紹介したプラグインソースを参照してください.

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

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

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

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

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);
    }
}();
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を読み込ませます.
このJavascriptによりグローバル変数_gaqが初期化(というより設定追加)されます.

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

background.js
!function() {
    !function() {
        // GoogleAnalyticsの準備
        lazyApp.GaFuncs.injectGaScript();
    }();
}();
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'
            });
        },
    };
}();
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を呼び出してバックグラウンドページとの接続を確立します.
接続には、機能毎に名前をつけています.

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

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

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

    lazyApp.GaFuncs.pushGaTrackEventOnBackground(GA_CATEGORY_ISSUE_ENTRY, '初期表示');
}();
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を利用しているため、マニフェストファイルにコメントを含めることはできません.

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(プロパティ) ... 開発中のプラグインの統計情報を管理

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

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

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

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

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

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

説明で紹介したプラグインのソースコード

14
17
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
14
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?