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

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

前回はWebアプリの検索ボックスとコンテキストメニューの拡張を通して、OmniboxとContext Menusについて詳細を解説しました。今回は、任意のWebサイトでJavaScriptを実行できるContent Scriptsについて解説していきたいと思います。

ほかのWebサイトと連携する

みなさんは、Webアプリを使っていてほかのWebサイトと連携して欲しいと思ったことはありませんか。例えば、あるWebサイトから情報を抜き出して欲しかったり、もしくはユーザーインフェースを変更・追加して直接Webアプリを操作したかったりといった内容です。それらは、Content Scriptsの仕組みを利用することで実現可能です。Content Scriptsは、対象のWebサイトで任意のJavaScriptを実行することができます。

今回の機能追加では、GoogleマップのWebサイトと連携して目的地を設定できるようにしてみます。具体的には、Googleマップの検索ボタンの隣にOdometerの「目的地設定」ボタンを追加します。⁠目的地設定」ボタンをクリックすると、Googleマップで表示している緯度・経度を使ってOdometerの目的地を設定します。

図1 Googleマップとの連携
図1 Googleマップとの連携

Webアプリの構成

今回は、Webアプリを構成するファイルにcontent_script.jsを追加しています。これは、Googleマップのサイト上で実行されるJavaScriptになります。そのほかにもodometer.jsとbackground.js、そしてmanifest.jsonを変更しています。

画像
manifest.json
{
  "name": "Odometer",
  "description": "距離計",
  "version": "0.5",
  "app": {
    "launch": {
      "local_path": "main.html"
    }
  },
  "icons": {
    "16": "icon_16.png",
    "48": "icon_48.png",
    "128": "icon_128.png"
  },
  "omnibox": {
    "keyword": "Odometer"
},
  "options_page": "options.html",
  "background_page": "background.html",
  "content_scripts": [
    {
      "matches": ["*://maps.google.co.jp/*", "*://maps.google.com/*"],
      "js": ["js/content_script.js"]
    }
  ],
  "permissions": [
    "geolocation",
    "notifications",
    "background",
    "contextMenus",
    "tabs"
  ]
}

Content Scriptsを利用するには、マニフェストファイルで "content_scripts" 以下の項目を設定します。"matches" で、対象のWebサイトを指定します。ここでは、Googleマップのドメインを2つ指定しています。また、実行するJavaScriptファイルとして "js" に今回追加したcontent_script.jsを指定しています。ここで設定した項目以外にもJavaScriptの実行タイミングの指定などさまざまな設定項目があります。

表1 マニフェストファイルのContent Scripts関連項目
フィールド名説明
content_scripts対象のWebサイトで任意のJavaScriptやCSSを適用する設定項目をオブジェクト配列で指定
  matches対象のWebサイトのURLを配列で指定
"http://example.com/*"マッチパターン
  css挿入するCSSファイルを配列で指定
  js実行するJavaScriptファイルを配列で指定
  run_atJavaScriptを実行するタイミングを指定
"document_start", "document_idle"(デフォルト), "document_end" の3つが指定可能
  all_framesすべてのフレーム要素でスクリプトを実行するかどうかを指定(デフォルト:false)
  include_globs"matches" からさらに適用するパターンを指定
  exclude_globs"matches" からさらに除外するパターンを指定

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メソッドを実行しています。

odometer.js(目的地設定)
/*
 * 目的地設定
 */
function setDestination(lat, lng, focus){
    var latLng = new google.maps.LatLng(lat, lng);
    if ( destMarker ) {
        destMarker.setMap(null);
    }
    destMarker = new google.maps.Marker({
        title: '目的地',
        position: latLng,
        map: map
    });

    //目的地情報表示
    showDestinationPosition(lat, lng);

    /*
     * Background Pageで目的地を設定
     */
    chrome.extension.sendRequest(
        //リクエストデータ
        {
            action: 'set_destination',
            lat: lat,
            lng: lng
        },
        //レスポンスコールバック
        function(response) {
            if ( response.type && response.type == 'distance' ) {

                //目的地までの距離を表示
                showDistance(response.distance);

            } else {
                alert('目的地の設定に失敗しました');
            }
        }
    );
    
    if ( focus ) {
        map.setCenter(latLng);
    }
}

メイン画面のsetDestinationメソッドでは、地図上のマーカーの作成など表示に関わる部分を更新しています。以降は、従来の操作プロセスと 同様の動きになるので解説は割愛します。

図3 Googleマップに目的地設定ボタンを追加する
図3 Googleマップに目的地設定ボタンを追加する
図4 目的地設定ボタンからOdometerを呼び出す
図4 目的地設定ボタンからOdometerを呼び出す

動的にJavaScriptを挿入する

Content Scriptsのようにあらかじめ対象としたURLに対してJavaScriptファイルを挿入するのではなく、任意のタイミングで動的にJavaScriptやCSSファイルを挿入する方法もあります。chrome.tabs以下のexecuteScriptメソッドとinsertCSSメソッドを利用します。これらのメソッドを利用するには、マニフェストファイルで以下のような指定が必要です。

マニフェストファイルのサンプル
"permissions": [
  "tabs",
  "http://*/*"
]

"permissions" に、"tabs" と対象のURLをあらかじめ指定する必要があります。サンプルではすべてのhttpサイトを指定していますが、Webアプリに合わせて個別に指定してください。

サンプルコード
// JavaScriptの実行
chrome.tabs.executeScript(null,
    { code: "alert(1)" });

// JavaScriptファイルの挿入
chrome.tabs.executeScript(null,
    { file: "js/execute_script.js" });


// スタイルの適用
chrome.tabs.insertCSS(null,
    { code: "body { backround-color: white; }" });

// CSSファイルの挿入
chrome.tabs.insertCSS(null,
    { file: "css/insert.css" });

実際にJavaScriptやCSSを挿入する場合のサンプルコードです。executeScriptメソッドとinserCSSメソッドは、第1引数で対象のタブIDを指定しています。タブIDはnullを指定すると現在表示されているタブを対象とします。第2引数でcode、もしくはfileを指定することができます。

表2 chrome.tabs
メソッド/プロパティ説明
executeScript(tabId, details, callback)指定のタブにJavaScriptを挿入する
insertCSStabId, details, callback指定のタブにCSSを挿入する
表3 details
プロパティ説明
codeJavaScriptコード、もしくはスタイルの文字列を指定
fileJavaScriptファイル、もしくはCSSファイルのURLを指定
allFramesすべてのフレーム要素でスクリプトを実行するかどうかを指定(デフォルト:false)

まとめ

これで、Googleマップのサイトから直接Odometerの目的地を設定できるようになりました。Content Scriptsを使えば、今回紹介したGoogleマップ以外にもほかの地図サイトやいろいろなサイトとも連携することができるようになります。Content Scriptsは自由度が非常に高いので面白いアイディアを実現しやすいと思います。是非活用してみてください。

今回は、Content Scriptsの詳細を解説しました。次回も引き続きさまざまなAPIを解説していきたいと思います。

crxファイルはzipファイルですので、右クリックからのダウンロード後に拡張子をzipに変えていただければ中身を参照できます。

おすすめ記事

記事・ニュース一覧