Home JavaScript Greasemonkey PHP

JavaScript だけでクロスドメインで POST メソッドを送る方法2009-09-23


JavaScript のみを使って、クロスドメインを実現しつつ POST メソッドでリクエストを送信する方法について解説します。
ここで解説する方法にはこんな特徴があります。
(2009-10-30 追記) iframe の unload のタイミングについて、重大な不具合がある可能性に気づきました。Chrome/Firefox において、2度イベントが発生している可能性が高いです。unload イベントを使わない場合は無関係です。結論が分かったら修正版をこのページで公開します。
(2010-01-29 追記) Chrome は大丈夫そうです。Firefox もカウンタ or フラグを使ってイベントを記録すれば大丈夫ぽいです。ちゃんと直せて無くてすいません。
  • XMLHttpRequest では不可能な、クロスドメインによるポストを実現している。
  • 元になるページの文字エンコードの種類にかかわらず、必ず UTF-8 でポストできる。
  • ポストが終わったタイミングをイベントで捕捉できる。
  • JavaScript だけで実現するので、サーバサイドに何らかのスクリプトを用意する必要がない。
  • 必要な HTML は DOM で後から埋め込むので、元になっているページの HTML は修正する必要がない。

さて、話の発端は、はてブ with Twitter になります。
このスクリプトは、元々 Greasemonkey に搭載されている GM_xmlhttpRequest 関数を使って実装されていました。
GM_xmlhttpRequest は通常の XMLHttpRequest に存在するクロスドメインの制限がないので (その分危ないとも言えますが)、 「はてブを追加する前に Twitter につぶやいて、その処理が終わってからはてブを追加する」という事がいとも簡単に実現していました。
ところがこのスクリプトを Google Chrome に対応させるにあたって、Google Chrome では、GM_* 系の関数が存在しないので、それを代替する手段を模索する必要があったのです。

通常、JavaScript によるクロスドメインというと、API 側が、JSONP 対応している前提で、script タグを埋め込んで対応するのが一般的です。
ただ、この方法には制限があり、script タグによるリクエストは必ず GET メソッドになってしまいます。
Twitter の API は大半が GET でアクセスできるのですが、つぶやきを送信する API については、POST でのアクセスが必須となっていて、Twitter でつぶやくために JSONP を使う事は出来ません。

単につぶやいてそれっきり、という動きでよければ、以前 閲覧中のページについてそこから遷移せずTwitterでつぶやくためのブックマークレットでやったのと同じ方法でいいのですが、はてブへの追加をするにあたっては、Twitter API へのアクセス完了を待たないといけないので、そのあたりを解決したのが、Google Chrome 版スクリプトになります。

以下では、はてブ with Twitter (for Google Chrome) のソースから一部を抜粋して、どうやってその動作を実現したかを解説してきます。

var d = document;
var f = /* なんかのフォーム Element */;
var b = /* なんかのサブミットボタン Element */;

// サブミットボタンにイベント登録
b.addEventListener('click', function (e) {
    // クロスドメインポスト用隠し iframe
    var i = d.createElement('iframe');
    i.style.display = 'none';
    d.body.appendChild(i);

    // レスポンスイベント取得用隠し iframe
    var i2 = d.createElement('iframe');
    i2.name = 'postresult';
    i2.style.display = 'none';
    d.body.appendChild(i2);

    // レスポンス時イベント登録
    i2.contentWindow.addEventListener('unload', function(e) {
        f.submit();
    }, false);

    // クロスドメインへの POST メソッド送信
    var iDoc = i.contentWindow.document;
    iDoc.open();
    iDoc.write('<form method="POST" action="http://twitter.com/statuses/update.xml" target="postresult">');
    iDoc.write('<input type="hidden" name="status" value="ポストしたい内容" />');
    iDoc.write('</form>');
    iDoc.write('<script>window.onload = function(){document.forms[0].submit();}</script>');
    iDoc.close();

    // サブミットボタン本来の動作をキャンセル
    e.preventDefault();
}, false);

このソースは、元々ページに存在するフォームに対して、そのサブミットボタンの動作をフックし、サブミットが行われる前に Twitter へのつぶやき (クロスドメインでの POST メソッド送信) をして、それが完了したから本来のサブミットをする、という動作を意図しています。
ソース中の f が元々のフォーム、b が元々のサブミットボタンです。

まず、サブミットボタンの click イベントを追加し、イベントの中で、e.preventDefault() する事で、本来のサブミット動作をキャンセルします。
そしてそのイベントの中で、i と i2 という 2 つの不可視 iframe を追加しています。

i は、POST メソッド送信用の iframe です。
window.write メソッドを使って iframe 内に form を組み立てつつ、window.onload で、読み込みと同時に form.submit() が走るようにしておきます。
ここで、form タグの method に POST を指定し、target に i2 の name を指定するのがポイントです。

i2 はあらかじめ name を指定してある iframe です。
i2 については、中身は空のままでいいですが、unload イベントを登録しておくのがポイントです。
これにより、i の中の POST 処理結果が i2 に送られたタイミングで i2 の unload イベントが発生するので、POST の完了を捕捉する事が出来ます。
ここで f.submit() と本来のサブミット動作を指定する事により、サブミットボタンのクリック動作をフックして別の動作を組み込む事が実現します。

(備考) 後かたづけの必要性

この例では、i2 の unload イベントの中で、親画面を遷移させてしまっているのでそれっきりでいいですが、親画面をそのままにする場合は、i と i2 を削除するなど後かたづけをしておく必要があるでしょう。
JSONP を使う場合もそうですが、テンポラリに生成したエレメントを放置していると、動的な HTML がどんどん汚くなって非常にマナーが悪いので後かたづけもちゃんと考えたいですね。

(備考) なぜ iframe で POST するのか

実は、単に POST メソッドを送信するだけでよければ、1つ目の iframe は不要です。document に直接 form を追加する事でも POST メソッドを送信する事が出来ます。
あえて、iframe を追加し、window.write で form を作っているのは、文字コードの都合になります。
iframe を空っぽの状態から作ると、内部的にその iframe の文字コードが UTF-8 になる事を利用して、POST の内容を UTF-8 にしています。
Web 用に POST メソッドの API を公開しているような最近のサービスは、要求される文字コードが大抵 UTF-8 かと思うので、元のページが Shift_JIS とか、EUC-JP であっても、iframe を利用すればスムーズに API を使えます。

(備考) なぜ iframe が 2 つ必要なのか

iframe を 1 つにして、それ自身の unload イベントを捕捉すればいいじゃないか、と思ったアナタ。その通りです。正しい思考です。
こればっかりは、実際にやって上手くいかなかったから、としか言えません。どうも、POST 後に unload イベントが発生していなかったようなので、2 つめの iframe を用意してやる必要がありました。

Twitter API が返すのが xml だからなのか、クロスドメインの制約によるものか、Firefox だったらそれでも上手くいくのか、理由はよく分かりません。
まあ、このあたりも含めて一種のノウハウなのでそのまま公開します。

(備考) IE の場合

IE の場合は未検証ですが、イベント周りの扱いが標準とかけ離れているので、上記のソースがそのまま動く事はないです。
具体的には、addEventListener のあたりと、preventDefault のあたりは確実に違います。
もし IE も視野に入れてどうにかしたいなら、イベントモデルの差異を吸収してくれているラッパ (jQuery やら prototype.js やら) を使うのが賢明かと思います。

カテゴリ: Development タグ: twitter utf-8 javascript