Google Chromeでも動くユーザスクリプトを書くためのメモ

僕が公開しているTwitterをGoogle風にするCustomTwitter とういうユーザスクリプトをGoogle Chromeに対応させるために行った内容を備忘録としてまとめておこうと思います。

GreasemonkeyとGoogle Chromeは何が違うの?

Google Chromeはネイティブでユーザスクリプトに対応しているようですが、Greasemonkey独自のGM関数には(ほとんど)対応していません。また@機能にもほとんど対応していません。以下のページで対応していない機能を確認できます。
User Scripts - The Chromium Projects

  • @require
  • @resource
  • unsafeWindow
  • GM_registerMenuCommand
  • GM_setValue , GM_getValue , GM_listValues , GM_deleteValue
  • GM_getResourceText , GM_getResourceURL
  • GM_xmlhttpRequest(同じオリジンなら使える)

要するにGM関数は使えないと思ったほうが良いですね。(Google Chrome v4での話し)

GM_*Value

まずはGM_getValue , GM_setValueなどのGM_*Value系を何とかします。
使うのはHTML5で策定中のWebStorage::localStorageという機能です。これはjavascriptを使ってブラウザに永続的にデータを残す仕組みです。
W3C - 『Web Storage』日本語訳 - HTML5.JP
詳解! HTML 5と関連APIの最新動向 - Webアプリ開発編 (8) Web Storage | マイナビニュース
ただ、注意しなければならないのはWebStorageは同じオリジン(プロトコル、ドメイン、ポート)ごとにデータを保存するということです。オリジンを超えてデータを共有したりするのは無理ということです。その点GM_*Value系はスクリプトごとにデータを保存するので少し使い勝手が落ちてしまいますが、まあ良しとしましょう。

function NGM_getValue(key , defaultValue)
{
  /*
  Greasemonkeyで動いている場合window.localStorage.key
  では値はとれません
  */
  var value = window.localStorage.getItem(key);

  //keyに対応する値が無いとnullが返ってきます
  if(value != null){ return value; }
  else{ return defaultValue || null; }
}

function NGM_setValue(key , value)
{
  window.localStorage.setItem(key , value);
}

function NGM_deleteValue(key)
{
  window.localStorage.removeItem(key);
}

function NGM_listValues()
{
  var list = [];
  var key;
  try
  {
    /*
    Greasemonkeyで動いている場合localStorage.lengthが使えないので、
    keyがnullになるまで繰り返す。
    しかし実際は存在しないkeyにアクセスすると例外が発生するので例外処理を行う。
    */
    for(var i = 0 ; ; i++)
    {
      key = window.localStorage.key(i);
      if(key == null){ break; }
      list.push(key);
    }
  }
  catch(e){}

  return list;
}

以下の点に注意する必要があります。

  • Greasemonkey上で動いている場合、localStorage.keyで値をとることができません
  • localStorageにbooleanやnumberを保存してもstringになってしまいます。(将来はどうなるんだろうか?)
  • Greasemonkey上で動いている場合、localStorage.lengthが使えないのでtry-catchで処理する必要がある

オリジンを越えたサーバから文字を読み取る

以前のCustomTwitterではスクリプトインストール時に指定のサーバからバージョン番号が書かれたファイルを一緒にダウンロードしてきていました。そして毎回サーバのバージョンファイルの内容をGM_xmlhttpRequestで取得してバージョンチェックを行っていました。簡略して書くとこんな感じです。

// ==UserScript==
// @name           CustomTwitter
// @namespace      h13i32maru
// @include        http://twitter.com/*
// @resource       version http://h13i32maru.sakura.ne.jp/gm/custom_twitter/version.txt
// ==/UserScript==
(function()
{
  function checkVersion()
  {
    var thisVersion = parseInt(GM_getResourceText("version"));

    function check(res)
    {
      var latestVersion = parseInt(res.responseText);
      if(thisVersion < latestVersion){ alert("最新バージョンがリリースされています"); }
    }

    GM_xmlhttpRequest({method:"GET" , url:"http://h13i32maru.sakura.ne.jp/gm/custom_twitter/version.txt" , onload:check});
  }

  checkVersion();
})();

もちろんオリジンを越えるので、この方法はGM_xmlhttpRequestでしか実現できません。(2010-06-07追記 XMLHttpRequest Level 2を使えばいいのか><)これではGoogle Chromeに対応できないので、違う方法で実装することになります。そこで思いついたのが、scriptタグとEventを使いサーバ上から任意のテキストデータを読み取る方法です。ただし、任意のサーバというわけにはいきません。自分が自由に作業できるサーバ限定です。
考え方はこんな感じです。

  • サーバ側
    • 「任意の文字列をもった要素をbodyにappendして、その後にEventを発生させるjsファイル」を設置
  • ユーザスクリプト側
    • Eventをリッスン
    • サーバ上のjsファイルをscriptタグで読み込む
    • Eventを捕捉したらbodyの要素から文字列を読み取る

サーバ側

(function()
{
 var version = "20100523";

 var elm = document.createElement("span");
 elm.id = "custom-twitter-latest-version";
 elm.textContent = version;
 elm.style.display = "none";
 document.body.appendChild(elm);

 var ev = document.createEvent("Event");
 ev.initEvent("CustomTwitterCheckVersion" , true , false);
 document.dispatchEvent(ev);
})(); 

ユーザスクリプト側

// ==UserScript==
// @name           CustomTwitter
// @namespace      h13i32maru
// @include        http://twitter.com/*
// ==/UserScript==
(function()
{
  function checkVersion()
  {
    var thisVersion = "20100520";

    document.addEventListener("CustomTwitterCheckVersion" , check , false);

    var s = document.createElement("script");
    s.type = "text/javascript";
    s.src = "http://h13i32maru.sakura.ne.jp/gm/custom_twitter/version.js";
    document.body.appendChild(s);

    function check()
    {
      var latestVersion = document.getElementById("custom-twitter-latest-version").textContent;
      if(thisVersion < latestVersion){ alert("最新のバージョンがリリースされています"); }
    }
  }

  checkVersion();
})();

かなり限定的な使い方だと思うので、どこまで役に立つかはわかりませんが。実際僕もバージョンチェックにしか使っていません。

@resource , GM_getResourceText

@resource , GM_getResourceTextを使う場合、僕は主にCSSを外部ファイルから読み込むために使っています。なのでこれらが使えない場合はlink要素で対応しました。

function NGM_addCSS(name , url)
{
  var e;

  if(typeof(GM_getResourceText) == "function")
  {
    e = document.createElement("style");
    e.textContent = GM_getResourceText(resourceName);
  }
  else
  {
    e = document.createElement("link");
    e.rel = "stylesheet";
    e.type = "text/css";
    e.href = url;
  }

  document.getElementsByTagName("head")[0].appendChild(e);
}

残りは分かりません

以下はどうすれば良いのか分からないのであきらめました。Google Chromeでも動くユーザスクリプトを書く場合、僕は使わないようにしています。

  • @require (スクリプト貼り付けでおk?)
  • unsafeWindow (なるべく使わないようにするべき?)
  • GM_registerMenuCommand (DOMでメニュー作ればおk?)
  • GM_getResourceURL (imgタグでおk?)
  • GM_xmlhttpRequest (オリジン越えは諦めましょう)

終わり

間違いやもっといい方法などがあれば教えてください。
@h13i32maru




次はSafari + Greasekitに対応したいけど、Mac持って無い。。。これを機会に買うにはちょっと高いな(´・ω・`)