2012年9月5日水曜日

だれでも分かるAJAXとJSONPの仕組みと解説

ウェブブラウザのアドレス欄にウェブページのURLを入力すると、該当ファイルがダウンロードされる。さらにHTML中に記述された画像等のファイルがあれば、すべてダウンロードされる。すべてダウンロードすると、ページの読み込みが完了となる。これが普通のウェブページの動作だ。
完全にダウンロードしてページを表示してしまった後に、ページ中の何かをクリックする等のイベントで、サーバーから別の情報をダウンロードしてきて、ページの表示へ反映させるには、非同期通信を使う。
非同期通信はJAVA Scriptを使うことで可能になる。現在のインタラクティブなウェブサイトにおいては常識的な技術のひとつと言えるだろう。
単にクリックで情報を切り替えて表示するだけの、例えばタブ切り替え機能であれば、サーバーから非同期で情報を取得せずとも、最初にHTMLページをロードしたときに、すべてのタブページの情報を含んだHTMLページを取得しておいて、イベントの発生時に表示内容を切り替えるようにすればよいことだ。だが、タブボタンをクリックしたタイミングで、はじめてサーバーから取得するには、非同期通信を使う。
タブ切り替えのような、些細な機能であれば、非同期通信を使って遠回りなことをしなくてもいいことが多いのだが、もう少し高度なことをしはじめると、やはり非同期通信が必要になってくる。

非同期通信は、任意のタイミングでウェブブラウザからサーバーへリクエストを投げて、サーバーからレスポンスを返す仕組みが必要になる。これにはPHP、CGI+Perl、Ruby等に代表されるサーバーサイドプログラミングが求められる。
先ほどのタブページの例で言えば、ウェブブラウザが何番のページの情報が欲しいというリクエストをサーバーへ送信し、サーバーが該当するページの情報をデータベースやファイルから取得して、ウェブブラウザへ返すようなイメージとなる。

ウェブブラウザ側の非同期通信は、AJAXとJSONPという二つの方法がある。どちらもJAVA Scriptで実現される。

・AJAXとは
AJAXは、XMLHttpRequest()関数やActiveXObject()関数を用いて、サーバーと通信する方法である。その実はただのHTTP通信にすぎない。サーバー側ファイルへGETまたはPOSTのリクエストとして、通信が行われる。ちなみに、ソケット通信をするわけではない。
リクエスト時のパラメータはURLパラメータの形式で行う。具体的には「para1=abc&para2=def」のようにだ。これであれば、サーバー側のプログラムはGETやPOSTとして普通にパラメータを受け取れる。サーバー側は受け取った情報に基づいて、通常のHTTP出力と同様にレスポンステキストを作成して返す。
ウェブブラウザは受け取ったレスポンステキストをXML形式とみなして処理することもできるし、テキストとみなして処理することもできる。
XMLというのはタグでくくるデータのフォーマットだが、そのパース(解析)はウェブブラウザに用意されたJAVA Scriptの関数を利用するだけでできる。

var value = xhr.responseXML.getElementsByTagName("response")[0].firstChild.nodeValue;

このようにするとXMLの中からresponseという名称のタグのひとつの値を取得できる。
だが、見ての通りたったひとつの値を取り出すだけなのに長ったらしい。XMLは階層構造と配列を持つので、複雑な記述になるのだと思われる。
コードの記述を短くするために、テンポラリ変数に移し替えながら使うこともできるが、そのようにしなければならないこと自体がまどろっこしい。
XMLは以下のようなタグでくくられる形式だ。

<DATA1>abc</DATA1>
<DATA2>123</DATA2>

タグなので、ひとつのデータごとに、前後に同じタグ名を書く。たとえば<DATA1>と書いて、終端に</DATA1>と記述するが、同じようなことを2つ書くので冗長だ。abcという3バイトをレスポンスするために、前後のタグ名に16バイト費やす。これひとつのことであれば、問題ないが、数千個の配列で同じ事だとすると、転送量はユーザーの体感にも影響がでかねない。
同じタグ名を前後に書くから不経済なのは分かっている。終端は終わりであることが分かればよいのだから、単なる記号一つにでも置き換えれば、かなり削減できるのだが、そうなっていないものは仕方ない。
また、タグと同じ文字列をデータ中に含めることはできない。

<DATA1>ab</DATA1>c</DATA1>

"ab</DATA1>c" という文字列をDATA1としているつもりであるが、うまくいかない。不等号記号等をエスケープして回避する必要がある。

もうそれなら、レスポンスにXMLを使うのはあきらめて、もうひとつのテキスト形式を選ぶのはよい方法だ。テキストは、文字通りただのテキストなので、そのままウェブブラウザに表示するような用途にも使えるし、CSV等の独自フォーマットでレスポンスを受け取ることに使うこともできる。だが、実際はテキストがJSONと呼ばれる形式になっているとみなして取り扱われることが少なくない。

・JSON形式とは
JAVA Scriptで配列変数を初期化するには、以下のように記述する。

var a1 = [123, 'ABC', 3.14];

純粋なJAVA Scriptのコードである。これで、a1[1]には文字列"ABC"が設定される。
連想配列の書き方は、以下のようになる。

var a2 = {'name':'Taro', 'age':13, 'address':'Hokkaido'};

a2['age']またはa2.ageには13という値がセットされている。
JAVA Scriptで、[]は配列、{}は連想配列のことだ。
配列と連想配列を組み合わせたり、階層構造も記述できる。

var a3 = {'name':'Taro', 'age':13, 'address':['Hokkaido', 'Sapporoshi', 'Kitaku']};

a3['address'][2]には"Kitaku"が入っている。

JAVA Scriptにはeval()という関数がある。引数に文字列を指定すると、それを式として評価した結果を返す。

var a = eval("3+4");

これは変数aに7が入る。
これを応用すると

var a1 = eval("[123, 'ABC', 3.14]");

のように書ける。文字列"[123, 'ABC', 3.14]"が式として評価され、変数a1へ代入される。この文字列は配列の初期化式であるから、a1は配列で初期化される。
このような配列のフォーマットをした文字列をJSON形式と呼ぶ。

このJSON形式をAJAXのレスポンスとして活用できる。
XMLでレスポンスするのと比較して、バイト数は圧倒的に少なくなる。エスケープするのも'または"だけで良い。
なにより受け取ったウェブブラウザ側はeval()関数でもとに戻すだけで、後は普通の配列変数としてレスポンスデータにアクセスできる。

・JSONPとは
非同期通信については、以上AJAXとJSONを使えば実現できるが、ひとつだけどうしてもできないことがある。
HTMLをダウンロードしたドメイン以外にはAJAX通信できないということだ。
http://aaa.com/1.htmlのJAVA Scriptからは、http://bbb.com/2.phpにAJAX通信できない。これはクロスサイトスクリプティングというセキュリティ上の制約に引っかかるからだ。この制約を回避する方法としてJSONPがある。JSONにP(Padding)をつけてJSONPである。
セキュリティと言えば、何か脆弱性があるのではないかと気になるかもしれないが、分かって使う分には問題ない。

AJAXによる他サイトへのアクセスはできないわけだが、他の代替方法として、例えば<IMG>タグであればできる。

<IMG SRC="http://bbb.com/image.jpeg">

これは普通に可能だ。画像ファイルimage.jpegを他サイトbbb.comからダウンロードして表示できる。
となれば、

<IMG SRC="http://bbb.com/s.php?p1=abc&p2=def">

とも書けるので、他サイトへリクエストパラメータを送信できる。
<IMG>タグをJAVA Script内で動的に生成すれば、他サイトから画像をロードするふりをして、任意のタイミングで他サイトにパラメータ送信することが可能になる。
しかし、リクエストを送信できてもレスポンスを受け取ることはできない。
ならば、<SCRIPT>タグを使う。

<SCRIPT SRC="http://bbb.com/s.php?p1=abc&p2=def"></SCRIPT>

これは<IMG>タグと同じアイディアで、パラメータを送信している。
<SCRIPT>タグは元来、外部JAVA Scriptを読み込むためのものである。C言語でいうところのinclude文に相当する。通常は<HEAD>タグの中に記述し、HTMLファイルがダウンロードされたとき、最初に一回実行されるものだ。そのためダウンロードされたHTML中にあるJAVA Script内から動的に<SCRIPT>タグを生成することができない。
しかし、インラインフレームは動的に生成できる。インラインフレームを動的に作り、その中に動的にドキュメントを書き込むことは可能だ。そのドキュメントに<SCRIPT>タグとソースファイルが記述されていたなら、ウェブブラウザはインラインフレーム内のドキュメントを表示するために、<SCRIPT>タグに書かれたソースファイルをダウンんロードする。インラインフレームは動的に任意のタイミングで生成できるので、<SCRIPT>タグでも、任意のタイミングで通信させることができることになる。その際にインラインフレームを非表示で行えば、ユーザーから一部始終も見えない。

以下のような文字列をインラインフレームにドキュメントとして流し込めば、

<SCRIPT SRC="http://bbb.com/s.php?p1=abc&p2=def"></SCRIPT>

サーバーのs.phpにはGETリクエストが送信され、パラメータとしてp1=abcとp2=defが渡される。受け取ったs.phpはそのパラメータを処理し、ウェブブラウザにレスポンスをテキストとして送信する。
ウェブブラウザはレスポンステキストがJAVA Scriptだと思っている。<SCRIPT>タグのSRCに書かれたファイルだからだ。
ウェブブラウザはそのテキストをJAVA Scriptとして実行しようとする。
それが以下のようなテキストだとすると、

callback("{'name':'Taro', 'age',:13}");

これはcallback()という関数を"{'name':'Taro', 'age',:13}"という文字列を引数にして呼び出していることになる。
以下のように、インラインフレームに流しこむドキュメントにcallback()関数を用意しておけば、それがコールされることになるだろう。

<SCRIPT SRC="http://bbb.com/s.php?p1=abc&p2=def"></SCRIPT>
<SCRIPT>
 function callback(x)
 {
 }
</SCRIPT>

あとは、この関数が受け取った引数を親フレームに渡せば、最終的に親フレーム側でレスポンスを受け取れたことになる。
このように<SCRIPT>タグを応用(悪用?駆使?)することで、パラメータの送信と、レスポンスの受信が実現可能だ。

・JSONPはクロスサイトスクリプティングも可能なので、それができないAJAXは必要ない?
JSONPでできるクロスサイトスクリプティングをAJAXはできない。大は小を兼ねるということで、できる方のJSONPを使っておけばいいのだろうか。
まず、JSONPはGETのみ可能で、POSTでリクエストできない。それなら、GETしか使わないという手もある。
それでも、私はJSONPより、できればAJAXの方がいいのではないかと思う。
その理由は、ご覧の通りJSONPは、やり方が少々強引だ。動的にインラインフレームを生成し、ウェブブラウザにJAVA Scriptを実行させているものだと誤解させている。特に二回、同じリクエストをすると、ウェブブラウザはキャッシュを使おうとすることがある。ウェブブラウザはJAVA Scriptだと思っているので、ロードされるスクリプトがそうそう改変されると思っていないからだ。リクエストに時刻や乱数等のダミーパラメータを付加することで、ファイル名が毎回変わるようにして、キャッシュさせないようにすることはできる。
しかし、私はJSONPの仕組みが泥臭い感じがして、できれば避けたい気持ちがある。実感としてもJSONPはまれにうまく動かないことを経験している。これは再現性に確証があるわけではないし、ウェブブラウザによって安定性が異なる可能性がある。実は私のプログラムの問題だったのかもしれない。だから断言できないが、私はJSONPを100%信用していない。

Yahoo!やGoogleに、JSONPで情報を取得できるサービスがある。そのようなものを使うときにJSONPを使うのはよい方法だが、通常はAJAXを使うのが良いのではないかと思っている。
AJAX+JSONがベスト。別ドメインにアクセスするときにJSONPを使うとよいだろう。

0 件のコメント: