続・先取り! Google Chrome Extensions

第2回User ScriptsとContent Scripts

こんにちは、株式会社ALBERTの太田です。今回はGoogle ChromeのUser ScriptsとContent Scriptsについて、その仕様とGreasemonkeyとの違いを中心に、実際のスクリプトの書き方を交えて解説します。

ユーザースクリプトとは

User Scriptsとは、Google Chrome版のGreasemonkey(ただし、後述の通り互換性はあまり高くありません)です。そもそもGreasemonkeyとは任意のページで任意のJavaScriptを実行し、そのページに機能を追加したり、⁠自分にとって)不要なものを取り除いたり、異なるサービスとの連携をしたり、といったことをJavaScriptファイルひとつで実現できるようにするFirefoxのAdd-onです。その手軽さと、それに見合わぬ強力なカスタマイズ性能から高い人気を得ています。

しかし、Greasemonkeyはその手軽さと引き換えに安全性を犠牲にしている面があります。AMO(addons.mozilla.org)に公開されているMozillaアプリケーション用のAdd-onはAMO(のレビュアー)による品質管理が行われていますが、userscripts.orgに公開されているGreasemonkey用Scriptはそういったチェックを受けていません。このため使用には十分に注意する必要があります(当然ながら、Add-onについてもAMO以外で公開されているのであれば、より注意が必要です⁠⁠。これはChromeのUser Scriptsについても同様です(と、少々脅し気味に書きましたが、実際のところ私自身は悪意あるユーザースクリプトというものを見たことはない、ということを申し添えておきます⁠⁠。

ChromeのUser ScriptsとGreasemonkeyのほかにも、SafariのプラグインのGreaseKitやOperaの標準機能のUserJavaScript、Internet Explorerのコンポーネントブラウザの一部にも同様の機能が実装されています。これらを総称して、ユーザーサイドスクリプトと呼ぶこともあります。

導入

まずは、ChromeのUser Scriptsの動作環境について説明します。ChromeのUser ScriptsはGoogle Chrome 2以降のバージョンで動作します。なお、Chrome 2以前はGreasemetalというプラグインが利用できましたが、Chrome側がネイティブサポートしたためGreasemetalの開発は終了しています。

ChromeのUser Scriptsは起動オプションで --enable-user-scripts というオプションを付けることで使用可能になります。Windows XP, Windows Vistaでのパスは以下の通りです。[username] の部分にはログインしているユーザー名が入ります。

"C:\Documents and Settings\[username]\Local Settings\Application Data\Google\Chrome\Application\chrome.exe" --enable-user-scripts
"C:\Users\[username]\AppData\Local\Google\Chrome\Application\chrome.exe" --enable-user-scripts

オプションを付けて起動すると、ユーザーデータフォルダにUser Scriptsという名前のフォルダが作られます(あらかじめ自分で作成しても問題ありません⁠⁠。

Windowsの場合、ユーザーデータフォルダは下記の場所になります。一部が隠しフォルダになっているため、探しにくい点に注意してください。

Windows XP
C:\Documents and Settings\[username]\Local Settings\Application Data\Google\Chrome\User Data\Default\

Windows Vista

C:\Users\[username]\AppData\Local\Google\Chrome\User Data\Default\

このUser Scriptsフォルダにファイル名が .user.js で終わるスクリプトを置けば、自動でファイルを認識して、@include(もしくは@match)で指定されたページを開いた際にそのスクリプトが実行されます。再起動などは必要なく、ファイルを更新した際も次の読み込み時に即座に反映されます。

User Scriptsの仕様

Greasemonkeyを意識して実装されていますので、動作面ではGreasemonkeyとの互換性があります。

  • スクリプトの実行タイミングはGreasemonkeyと同じく、DOMContentLoaded(DOMの構築後、画像などの読み込み完了前)で起動します。

  • スクリプトの実行コンテキストがサイト側のスクリプトと分かれています。このため意図せずにグローバル変数を使用してしまったり、サイト側のスクリプトの処理を上書きしてしまうようなことはありません。

ただし、機能面ではGreasemonkeyと異なる部分が多くあります。

  • @exclude, @require, @resourceなどのメタデータを使用できません。

  • unsafeWindowに相当するサイト側のグローバルオブジェクトがありません(Chrome 2ではcontentWindowという名前でサイト側のグローバルオブジェクトにアクセスできましたが、Chrome 3以降では廃止されています⁠⁠。

  • サイト側のコンテキストでJavaScriptを実行したい場合、ブックマークレット(location.hrefにjavascriptスキームを代入するなどの方法で実現できます)として実行するか、script要素を作ってコードをテキストノードとして挿入する必要があります。

  • GM_setValue, GM_getValueなどのGM APIもほとんど使用できません。Chrome 2では名前だけ定義だけされていて、実際には使用できない状態で非常に厄介なことになっていましたが、Chrome 3, 4では定義されていない状態に修正されました。

  • GM APIで使用可能なのはGM_addStyle、GM_xmlhttpRequest(ただし、ドメインを超えることはできません⁠⁠、GM_openInTab(実装はwindow.openのラッパー⁠⁠、GM_log(やはりconsole.logのラッパー)の4つです。

また、Chrome UserScript独自の機能もあります。

@matchメタデータ

@matchは@includeの改良版という位置づけで、@includeよりも正確なパターン指定が可能です。@matchはhttpなどのschemeパート、google.comなどのhostパート、/search?などのpathパートに分けてマッチングが行われます。@includeでは http://*.google.com/* というパターンが、http://another.site.com/?.google.com/ にもマッチしてしまうという問題がありますが、こういった問題は@matchでは起こりません。

@includeとは書式が異なる部分が多いので、詳細はMatch patternsを参照してください。特に、Bad patternはやってしまいがちなパターンがまとめられています。

@run-atメタデータ

@run-atはdocument-startと指定することで、ページのDOM構築を待たずに、読み込みを開始したタイミングでスクリプトを実行します。実行タイミングが速いので、例えばショートカットキーなどをこのタイミングで登録すればユーザーの待ちを軽減することができます。ただし、当然ながらDOM要素にアクセスできないため、出来ることは大幅に制限されます。

Content Scriptでも同様のことが可能ですが、run_atとdocument_startという組み合わせであり、ハイフンではなくアンダースコアという違いがあるので注意が必要です。

これらの点に注意すれば、Greasemonkey用のスクリプトをUser Scriptsで動かすことはそれほど難しくありませんが、残念ながらChromeとの互換性を意識して書いていないGreasemonkey ScriptがChromeで動くことは滅多にありません。ただ、OperaやSafariをサポートするスクリプトはChromeでも動作する可能性が高くなります。実際、Greased LightboxはChromeでも動作します。

User Scriptsの作り方

それでは、実際に簡単なスクリプトを書いてみます。今回はGoogleの検索結果にfaviconと番号を表示するUser Scriptを作成してみます。まずはメタデータを記述します。

メタデータの記述
// ==UserScript==
// @name           Google Search Number Favicon
// @match          http://www.google.com/search*
// @match          http://www.google.co.jp/search*
// ==/UserScript==

スクリプト名はGoogle Search Number Favicon(単語を並べただけで恐縮ですが…)としました。@matchでGoogleの検索結果のURLを表現します。matchの部分はhttp://www.google.*/search*のように記述したいところですが、残念ながらhostパートの*は//の直後でしか使えません。

それでは本体のプログラミングに入ります。と、その前にGoogleの検索結果のHTMLを確認しておきます。適当な検索を表示し、右クリックから「要素の検証」を選択します。するとDeveloper Toolsが立ち上がり、右クリックした辺りがハイライトされて表示されます。すると、図1のように検索結果のリンクはclass="l"を持ち、h3要素を親に持つことが確認できると思います。

図1 Web Inspectorによる解析
図1 Web Inspectorによる解析

今回はこれを元に要素にアクセスします。DOM要素を取得する方法は手軽なSelector APIと高機能なXPathの2つがお薦めです。今回はSelector APIを使用します。

要素の取得とナンバリング
var links = document.querySelectorAll('h3 > a.l');
for (var i = 0, len = links.length;i < len; i++) {
   var link = links[i];
   var num = document.createTextNode(i+1 + " ");
   link.parentNode.insertBefore(num,link);
}

querySelectorAllで⁠l⁠というクラス名を持つA要素を取得し、その要素の前に1から順番にテキストを追加しています。本当にシンプルなスクリプトですが、ひとまずナンバリングができました。

しかし、このままでは2ページ目でも番号を1から振り直してしまいます。そこでパラメータから初期値(start)を取得してその値に応じて初期値を変えてみます。

初期値の取得と反映
var links = document.querySelectorAll('h3 > a.l');
var start = 1;
var match = location.search.match(/&?start=(\d+)&?/);
if (match && match[1]) {
   start = parseInt(match[1], 10) + 1;
}
for (var i = 0, len = links.length;i < len; i++) {
   var link = links[i];
   var num = document.createTextNode(String(start + i) + " ");
   link.parentNode.insertBefore(num,link);
}

location.searchからstart=10の数値部分を取り出し、数値が取れればそれを初期値として処理しています。こういったパラメータを使用する場合はその扱いに十分に注意する必要があります。今回は\dにマッチした数字をparseIntで数値に変換し、createTextNodeで文字列ノードにしてページに追加していますので、クロスサイトスクリプトが起こる危険性はありません。しかし、URLから取得した値を不用意にinnerHTMLなどに挿入すれば簡単にクロスサイトスクリプトが可能になってしまいます(もちろん、これはJavaScript一般に言えることです⁠⁠。

続いてfaviconの表示を実装してみます。

faviconの挿入
var links = document.querySelectorAll('h3 > a.l');
var FAVICON = 'http://www.google.com/s2/favicons?domain=';
for (var i = 0, len = links.length;i < len; i++) {
  var link = links[i];
  var favicon = document.createElement('img');
  favicon.src = FAVICON + link.host;
  link.parentNode.insertBefore(favicon,link);
}

faviconの表示にはGoogleのAPIを使用しました。A要素の取得方法はナンバリングの時と全く同じです。A要素はhref属性のほかに、hostやpathnameなどのプロパティを持っているので、それを利用しています。では最後に今までのスクリプトをまとめます。

スクリプトの全体
// ==UserScript==
// @name           Google Search Number Favicon
// @include        http://www.google.com/search*
// @include        http://www.google.co.jp/search*
// ==/UserScript==
(function(){
  var FAVICON ='http://www.google.com/s2/favicons?domain=';
  var start = 1;
  var match = location.search.match(/&?start=(\d+)&?/);
  if (match && match[1]) {
    start = parseInt(match[1], 10) + 1;
  }
  var links = document.querySelectorAll('h3 > a.l');
  for (var i = 0, len = links.length;i < len; i++) {
    var link = links[i];
    var num = document.createTextNode(start + i + " ");
    link.parentNode.insertBefore(num, link);
    var favicon = document.createElement('img');
    favicon.src = FAVICON + link.host;
    link.parentNode.insertBefore(favicon,link);
  }
})();

スクリプト全体を無名関数(function(){})で囲みました。これはChromeのUser Scriptsには必要のない処理です。ただし、OperaのUserJavaScriptではこの無名関数がないとページ側の変数を汚染してしまいます。今回書いたスクリプトはChromeだけでなく、Opera、Greasemonkey、GreaseKitでも動作しますので、そういった点も考慮しておきます。また、これに合わせてメタデータの記述をChrome専用の@matchではなく@includeに変更しています。

これでこのスクリプトは完成です。なお、このスクリプトはサンプル用にシンプルな実装になることを優先しています。このスクリプトの改善点としては、数字の表示はスタイルシートのlist-styleで行ってolタグのstart属性で値を制御する方法や、faviconの表示はCSSのbackground-imageで表示してキャッシュを活用しつつ表示を軽くする方法などが考えられます(特に、元々のリンクの表示位置がずれることがないように実装することで、体感的な表示速度を大幅に改善できると思われます⁠⁠。また、Googleの検索結果のリンク先URLがリダイレクトページを挟む状態になっているとFaviconの表示が正常に機能しません。その点も改善の余地があります。

Content Scriptsとして拡張化

User Scriptsとして作成したスクリプトはそのままContent ScriptsとしてExtensionにすることが可能です。下記のようなmanifest.jsonを用意してスクリプトと同じフォルダに置き、chrome://extensions/ ページのDeveloper ToolでパッケージングするだけでExtensionの出来上がりです。なお、Content Scriptとして実行する場合、user.jsファイルのメタデータは使用されない点に注意してください。

拡張用manifest
{
  "name": "Google Search Number",
  "version": "0.1",
  "content_scripts": [
    {
      "js": [
        "google_search_number_fav.user.js"
      ],
      "matches": [
        "http://www.google.com/search*",
        "http://www.google.co.jp/search*"
      ]
    }
  ]
}

最後に、Greasemonkey ScriptやChrome ExtensionsのAutoPagerizeで継ぎ足されたページにも対応させたスクリプトを公開しておきます。

まとめ

Google ChromeのUser Scriptsについて、Greasemonkeyの概要から実際のスクリプトの作り方を解説しました。同時にChrome Extensionsの作りやすさも実感していただけたのではないかと思います。

また、ユーザーサイドスクリプトはアイディアひとつで役に立つ機能でも、面白い機能でも手軽に実現できるので、プログラミングの勉強の題材としても優れています。この機会に試してみて頂ければ幸いです。

次回は、Page Action ver2やOption PageなどのExtensions APIの解説を予定しています。

おすすめ記事

記事・ニュース一覧