Webアプリを公開しよう! Chrome Web Store/Apps入門

第8回 Webアプリを作ろう#5――Content Scripts

この記事を読むのに必要な時間:およそ 6 分

Content Scripts

Content Scriptsは,対象のWebサイトで任意のJavaScriptを実行することや,任意のCSSを挿入することができます。挿入されたJavaScriptは,対象のWebサイトのDOMツリーに自由にアクセスすることができます。ただし,挿入されたJavaScriptから元のWebサイトのJavaScriptへ直接アクセスすることはできません。例えば,元のWebサイトであるライブラリを読み込んでいるからといって,挿入されたJavaScriptからそのライブラリを利用することはできません。そのライブラリを利用するためには,改めてContent Scriptsで読み込む必要があります。また,挿入されたJavaScriptからはクロスドメイン通信などの特権を必要とするものも利用することができません。その場合,バックグラウンドページと通信してバックグラウンドページから呼び出す必要があります。バックグラウンドページとその通信方法については,過去の連載記事で解説していますのでそちらを参照してください。

図2 Content Scriptsの仕組み

図2 Content Scriptsの仕組み

Content Scriptsのセキュリティ

Content Scriptsは,非常に強力な機能であるがゆえに,その適用範囲やセキュリティには留意しなければなりません。例えば,すべてのWebサイトでContent Scriptsが適用されるのであれば,その必要性と機能について詳細な説明がなければユーザーは不安に思います。もしかしたらあなたのWebアプリをインストールしないかもしれません。そのため,Content Scriptsを利用する場合は,適用範囲を必要最小限にとどめ,その上で十分な説明をするようにしてください。また,Content Scriptsで挿入するJavaScriptに脆弱性があると,自身のWebアプリだけではなく対象のWebサイトにも被害が及ぶため,セキュリティには十分に注意してください。

Chrome 14からは,マニフェストファイルでスクリプトを実行できる場所を制御できるようになります。例えばマニフェストファイルで次のように指定すると,スクリプトの読み込み元を自身のサイトに制限することができます。

マニフェストファイルのサンプル

"content_security_policy": "default-src 'self'"

Googleマップにボタンを追加してOdometerを呼び出す

今回は,Googleマップの検索ボタンの隣にOdometerの「目的地設定」ボタンを追加します。Googleマップのサイトを表示した場合,content_script.jsが挿入されますので,スクリプト内でサイトのページ内に要素を追加しています。

content_script.js(ボタンの追加)

(function(){
    
    // Google Mapsのドメイン上で対象のノードがあればボタンを埋め込む
    var insertTarget = document.querySelector('table.cntrltable>tbody>tr');
    if ( !insertTarget ) {
        return;
    }
    
    // Odometer目的地設定ボタン追加
    var img = document.createElement('img'),
        button = document.createElement('button'),
        innerDiv = document.createElement('div'),
        outerDiv = document.createElement('div'),
        td = document.createElement('td');
    
    img.src = chrome.extension.getURL('icon_16.png');
    img.style.cssText = 'margin-right:5px; padding-bottom:8px;';
    button.className = 'kd-button kd-button-submit';
    button.style.cssText = 'cursor:pointer;min-width:105px;height:30px;padding:5px 5px 0 0;';
    button.appendChild(img);
    button.appendChild(document.createTextNode('目的地設定'));
    
    // ~省略~

    innerDiv.className = 'q-inner';
    innerDiv.appendChild(button);
    outerDiv.className = 'q-outer';
    outerDiv.appendChild(innerDiv);
    td.appendChild(outerDiv);
    insertTarget.appendChild(td);
})();

要素の追加は,通常のDOM操作で行います。ここでは,document.createElementメソッドとappendChildメソッドなどを使って元の検索ボタンと同じ見た目になるように,同じ構成でボタンを作成しています。クラス名やスタイルなどは,元の検索ボタンで使われているものをそのまま指定しています。また,この目的地設定ボタンはOdometerの機能だと分りやすいようにOdometerのアイコンを追加しています。また,目的地設定ボタンをクリックされた際にGoogleマップの座標を取得するにはページのJavaScriptにアクセスする必要があります。ここでは,あまりスマートではありませんが次のようにスクリプト要素を追加してJavaScriptを実行して,sessionStorage経由で座標を取得しています。sessionStorageなどのWeb Storageについては過去の連載記事で解説しています。

content_script.js(ボタンの追加)

// Odometer上で目的地を設定する
button.addEventListener('click', function(evt){
    evt.preventDefault();
    
    // スクリプト要素を挿入して挿入したスクリプトから座標をsessionStorageに格納する
    var script = document.createElement('script');
    script.textContent = 'var p = gApplication.getMap().getCenter(); sessionStorage.lat = p.lat(); sessionStorage.lng = p.lng();';
    document.head.appendChild(script);
    
    // 挿入したスクリプトから座標がsessionStorage経由で格納されるのを監視する
    var polingCount = 5;
    function getLatLng(){
        console.log(polingCount);
        
        // 座標が格納されている場合,バックグラウンドページへリクエストを送信する
        if ( sessionStorage.lat && sessionStorage.lng ) {
            chrome.extension.sendRequest({
                action: 'set_destination_from_websites',
                lat: sessionStorage.lat,
                lng: sessionStorage.lng
            });
            
            // 後処理
            sessionStorage.removeItem('lat');
            sessionStorage.removeItem('lng');
            document.head.removeChild(script);
            return;
        }
        
        if ( polingCount-- ) {
            setTimeout(getLatLng, 500);
        }
    }
    setTimeout(getLatLng, 500);
    
}, false);

挿入するスクリプト要素の中で,ページのgApplicationオブジェクトを操作して座標を取得し,sessionStorageに格納しています。元のスクリプトでは,挿入後にsessionStorageを監視して座標が取得できた場合にchrome.extension.sendRequestメソッドを使ってバックグラウンドページへ目的地設定のためのリクエストを送信しています。監視はsetTimeoutメソッドを使って0.5秒ごとにgetLatLngメソッドを呼び出して最大5回まで確認するようにしています。バックグラウンドページへは,アクションの内容を "set_destination_from_websites" として緯度,経度を渡しています。

background.js(目的地設定)

/*
 * メッセージを受信し,各処理へ振り分ける
 */
chrome.extension.onRequest.addListener(
    function(request, sender, sendResponse) {
        
        switch ( request.action ) {
        
            // ~省略~

            case 'set_destination_from_websites':
                setDestinationOnApp(request.lat, request.lng);
                sendResponse({});
                break;
            
            default:
                sendResponse({});
                break;
        }
    }
);

/*
 * アプリ上で目的地設定を行います
 */
function setDestinationOnApp(lat, lng){
    selectOrCreateTab(APP_URL, function(tab){
        
        var view = getView(APP_URL);
        if ( !view ) {
            return;
        }
        
        if ( tab.status === 'loading') {
            view.destination = {
                lat: lat,
                lng: lng
            };
        } else {
            view.setDestination(lat, lng, true);
        }
    });
}

バックグラウンドページでは,アクション内容の "set_destination_from_websites" をcase句で判断してsetDestinationOnAppメソッドを呼び出しています。setDestinationOnAppメソッドでは,前回と同じようにOdometerのメイン画面が既に開いているか確認し,開いていればメイン画面のWindowオブジェクトを取得してsetDestinationメソッドを実行しています。

著者プロフィール

吉川徹(よしかわとおる)

普段は,普通のSIer。Webからローカルアプリケーション,データベースからインフラ周りに至るまで,何でも担当する雑食系。主にHTML5開発者コミュニティ「HTML5-developers-jp」で活動中。同コミュニティ主催の「HTML5とか勉強会」のスタッフを務め,HTML5の最新動向を追っている。