9月
23
2009
2

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

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 やら) を使うのが賢明かと思います。

Written by Otchy in: Development | タグ: , ,
4月
16
2009
2

JavaScript だけで Shift_JIS/EUC-JP のページから UTF-8 に変換して POST する方法

1 個前のエントリで、JavaScript だけを用いて、任意の文字コードのページから、UTF-8 エンコーディングで任意のサーバに (クロスドメインして) POST する方法を編み出しました。
その部分だけで需要があるのではないかと思ったので、関数として取り出して公開します。

案外スマートに書けたのでご機嫌です。

function postUtf8(param)
    if (!param) return;
    param = param.replace(new RegExp('&', 'g'), '&amp;');
    param = param.replace(new RegExp('"', 'g'), '&quot;');
    param = param.replace(new RegExp('<', 'g'), '&lt;');
    param = param.replace(new RegExp('>', 'g'), '&gt;');
    var d = document;
    var i = d.createElement('iframe');
    i.style.display = 'none';
    d.body.appendChild(i);
    var iDoc = i.contentWindow.document;
    iDoc.open();
    iDoc.write('<form method="POST" action="http://server/path/file">');
    iDoc.write('<input type="hidden" name="param" value="' + param + '" />');
    iDoc.write('</form>');
    iDoc.write('<script>window.onload = function(){document.forms[0].submit();}</script>');
    iDoc.close();
    setTimeout(function() {
        d.body.removeChild(i);
    }, 5000);
}

こんな感じです。
http://server… と、param の部分を書き換えれば、好きなサーバに好きなパラメータを渡せますね。
POST 先のサーバが重い時は、setTimeout の 5000 の値をもう少し大きめに取った方がいいかもしれません。

簡単に技術的な説明もしておきましょうか。
基本的には、iframe を新規で作成すると、その文字コードがデフォルトで UTF-8 として処理される事を利用しています。

iframe の中身は当初 DOM で構築しようとしたのですが、うまくいかず、document.write で構築しています。

iframe 内で window.onload しているのは、これまた直接 script タグ内で submit しようとすると、その時点では iframe の DOM 構築が完了しておらず、submit 出来ないためです。
iDoc.close(); しないと、DOM が構築されないのではないかと予想されます。

Written by Otchy in: Development | タグ: ,
1月
28
2009
2

PHP で日本語のひらがなとカタカナと漢字を判別する方法 [UTF-8編]

ちょっと思うところあって調べたのでメモしておきます。
結局使わない事になったんですが。

mb_regex_encoding('UTF-8');
if (preg_match("/^[ぁ-んー]+$/u", $str)) {
    // ひらがな
} else if (preg_match("/^[ァ-ヶー]+$/u", $str)) {
    // カタカナ
} else if (preg_match("/^[一-龠]+$/u", $str)) {
    // 漢字
}

mb_regex_encoding(‘UTF-8′); は正規表現の基準となる文字コードの指定です。
php.ini がいじれる環境なら、[mbstring] セクションに、mbstring.internal_encoding = UTF-8 とした方が良いかと思います。

[あ-ん] 等の指定は、Unicode 上でひらがなやカタカナを表す文字コードの最初と最後の文字を、範囲指定しています。
また、厳密に言うと漢字の最後は “龠” では無いのですが、一般的に漢字と見なされる範囲の中で、最後にある JIS 第二水準の漢字であるため、この字を採用しています。(この漢字の範囲には、中国の漢字やハングルなどが含まれます)
全然異なる文脈ですが、このページで詳しく考察されています。

preg_match の u フラグは、UTF-8 を正しく扱えるようにするためのフラグです。これがないと、文字コードの範囲指定などが正しく動作しないようです。

Written by Otchy in: Development | タグ: ,

Powered by WordPress | Aeros Theme | TheBuckmaker.com