Hatena::Groupjavascript

JavaScriptで遊ぶよ

 | 

2010-03-29

ユーザースクリプトとページ側とのやりとり

01:54

というのを作った。

最初は Opera だけで動けばいいかなと思ったんだけど、せっかくなので Greasemonkey も Chrome も対応したいと思って頑張った。

面倒だったところは、JSONP を使う必要があって、どうしてもページ側のグローバル領域に関数を定義しないといけなかった。

  • GM_xmlhttpRequest は Opera と Chrome に無いので却下。
  • YQL には "Access-Control-Allow-Origin: *" が付いていたので、Chrome と Firefox で XMLHttpRequest が使えるかと思ったんだけど、どういうわけか Chrome では security violation が出た。(たぶん UserJS の自動変換だったので permission が付いていないため)どうやら思い違いだったみたい。これでもいける。
  • やっぱり JSONP しかない。

まあ @include で厳しく指定してるし、グローバルで何でも好きにやりたきゃやれるんだけど、そこは意地でもページのコンテクスト侵食は最低限にしようと思った。(ただし Opera では別の部分で侵食しまくり)

ここでちょっと Opera と Chrome と Greasemonkey のスクリプトのおさらい。

  • Opera
    • スクリプトはグローバルで実行。
      • なので基本はスクリプト全体をクロージャにする。
      • ただし、.js で保存した場合には opera.addEventListener('BeforeEvent.load',...) などのリスナーが使えて、これはページのスクリプトからは使われることはないので、上記のクロスドメイン GET などが使える。
  • Greasemonkey
    • スクリプトはサンドボックスで実行。
      • unsafeWindow がページの window への参照。
  • Chrome
    • スクリプトはサンドボックスで実行。
      • unsafeWindow はサンドボックスの window への参照。
      • ページ内に関数を定義する方法は、location.href='javascript:〜' とするか、script.textContent='〜', document.body.appendChild(script) とするしかない。
      • ページ内で発火したイベントにオレオレプロパティを付けてもサンドボックスからは見えない。

Chrome は徹底してサンドボックス化してある。一番厄介。


さて、結論から言うと、JSONP のコールバック関数は MessageEvent を使うことにした。(os0x さんの助言と nanto_vi さんの記事が参考になりました)

MessageEvent と言っても、ページ側で window.postMessage(...) してスクリプト側で window.onmessage = ... (または unsafeWindow) とする方法だと Chrome では動かないと思うので (試してないけど、同じ window オブジェクトではないからこれも使えたみたい。)、 document.createEvent('MessageEvent') して document で発火させる。document は Chrome でも Greasemonkey でもページ側と共有されるので。

ただし Opera では 10.50 になっても document.createEvent('MessageEvent') が Not Supported Error を出すので、仕方なく document.createEvent('Event') してオレオレプロパティを付けることにした。

つまり、ページ内で定義するのは以下のような関数。

function myCallback(obj) {
  if (window.opera) {
    var ev = document.createEvent('Event');
    ev.initEvent('MyJSONPReady', true, false);
    ev.mydata = obj;
  } else {
    var ev = document.createEvent('MessageEvent');
    ev.initMessageEvent('MyJSONPReady', true, false,
      JSON.stringify(obj), // data
      location.protocol + '//' + location.host, // origin
      '', // lastEventId (Server-sent Event に使われるもの)
      window // source
    );
  }
  document.dispatchEvent(ev);
}

これを文字列として script.textContent にして、document.body.appendChild(script) する。

受け手のほうは

document.addEventListener('MyJSONPReady', function(ev) {
  var data = ev.mydata || JSON.stringify(ev.data);
}, false);

とやれば JSONP で要求したオブジェクトが得られるというわけ。

MessageEvent で文字列だけじゃなくてオブジェクトが送れるようになるのは最近の環境だけなので、JSON にする。Opera だけ別にしたことによって、ネイティブ JSON のない Opera 10.10 でも使える。


ハイハイ、バッドノウハウバッドノウハウ。

もっとブラウザ分岐が少ない方法があれば知りたい。(自分としてはこれで満足だけど)


ちゃんと確認してみた。

"Access-Control-Allow-Origin: *"が付いている場合は、Firefox と Chrome では XMLHttpRequest で OK。

Opera では、.js で保存したスクリプト (.user.js ではなくて) の実行時のみ (それ以後のイベント内ではなくて) において、opera.addEventListener が使える。

こんな感じ。(実際には文字コードが違うとうまく動かなかったりするけど)

(function() {
  var scripts = [];
  var callbacks = [];

  opera.addEventListener('BeforeScript', function(e) {
    var s = e.element;
    var index = scripts.indexOf(s);
    if (index >= 0) {
      callbacks[index].call(null, s.text);
      scripts.splice(index, 1);
      callbacks.splice(index, 1);
      e.preventDefault(); // スクリプトの実行をキャンセル
      s.parentNode.removeChild(s); // スクリプトを削除
    }
  }, false);

  // クロスドメイン GET 関数
  function xGet(url, callback) {
    var s = document.createElement('script');
    s.src = url; 
    document.body.appendChild(s);
    scripts.push(s);
    callbacks.push(callback);
  }

  // このクロージャ内では安全にクロスドメイン GET が使える
  window.addEventListener('load',function() {
    xGet('http://query.yahooapis.com/v1/public/yql?q=show%20tables&format=json', alert);
  }, false);
})();

os0xos0x2010/03/30 03:41> ページ側で window.postMessage(...) してスクリプト側で window.onmessage = ... (または unsafeWindow) とする方法
は動きますよ。この前のと同じ理由であまりオススメはできませんが…。
あと、scriptを挿入するとき、関数をtoStringする方法がお薦めです。文字列リテラルで書くのは面倒なので。
function myFunction(){
//
}
script.textContent = '(' + myFunction + ')();';

> どういうわけか Chrome では security violation が出た
これ、こちら(5.0.360.0 dev/WinXP )では再現しないっぽいです。Content ScriptsからYQLにアクセスできました。
http://gyazo.com/6e1d91f5e40c3f840201a70a52753a9f.png
元々、Content Scriptsはmanifest.jsonのpermissionsとは関係ありません。
あとで他のバージョンやMacでも検証してみます。

edvakfedvakf2010/03/30 04:35http://f.hatena.ne.jp/edvakf/20100330043156
動きました。何が間違っていたんだろう。お恥ずかしい。

それにしても Chrome は同じ UserJS を何回もインストールしたら後からインストールしたほうを尊重してくれてもいいのにと思いました。
Opera だとファイル編集→Refresh display アクションでいけるので UserJS 書くのがラクでラクで。

 |