prototype.js で Ajax してみた

※ このエントリはすぐ使えるサンプルを提供することを目的としていません。自分の試行錯誤を記録しているだけです。PHP と JavaScript をまーだいたい分かっている人間が実際に Ajax を書く際にうまくいかなかった点の実例としてご覧下さい。

まとまった情報を紙でほしかったので WEB+DB Press Vol.31 を Amazon のマーケットプレイスで購入して GW の宿題としてチャレンジ。とは言っても事前に @IT とかあれこれ情報は手に入れてるのでこの特集だけの知識で書いたわけじゃない。

しかし、思ったより難しいなっていうのが感想。

まず XMLHttpRequest そのものの理解がいまいち足りてない。つかいつも思うが JavaScript って調べにくいったらありゃしない。Ecma262 の部分はよくまとまってるんだけどな。どうにか作ったあとにつらつら見てたら

Dynamic HTML and XML: The XMLHttpRequest Object

これはコンパクトで見やすかった。こういう概観できるページってなんであんまないんだろ。

あとやっぱ prototype.js ドキュメント足りなすぎ。ほんとに便利な機能が入ってるのに、いちいち中身見なきゃ存在が分からないのはちょっと勘弁してほしい。

で、何を作ったかっていうと

24 ways: Edit-in-Place with Ajax

こんなやつ。イメージとしては del.icio.us の編集部分。

  1. サーバ上に一つファイルを用意して、onLoad でその内容を受け取る1
  2. その部分をダブルクリックしたらそのテキストを編集する画面を出す
  3. 編集結果を save ボタンで送信してそのファイルに保存する
  4. cancel したら編集内容をすべて破棄して元に戻す

ま、これ作るだけならトライ&エラーでなんとかなる。escape() 使わないで encodeURI() にしてくれってところさえ気をつければほとんどコピペだし。2

もともと JavaScript が HTTP クライアントになるだけっていうのはイメージついてたので、やっぱり気になるのは

JavaScript からのアクセスでない場合の対処が必要じゃん?

ってところ。prototype.js は HTTP リクエストヘッダにサインを入れてくれるけど、同じものを入れた普通のブラウザからのリクエストとはもう区別つかないわけでしょ? JavaScript からのリクエストに対して XML や JSON やプレーンテキストなどの断片の情報が返るのはアリだけど、普通にブラウザでアクセスした場合どうする、ってのを考えなきゃまずいんじゃないかなぁ。誰もいたずらしないならいいけど。

そうすっとやっぱ session とか URL の設計とかって話になると思うんだけど、そういう参考になる記事って全然見ない。まぁあんまり現実に即しすぎてても面白くはないんだろうけど。あと、

「アプリケーション」じゃなくて「サイト」には適用しにくい

これ、実は最近ずっと感じていることなんだけど、世のフレームワークってほぼ全部「アプリケーション」を指向している。当然「プログラマ」から見ると Web というプラットフォームで動くアプリケーションなんだろうけど、その考え方だけではサイトとしてイマイチ。

JavaScript は geek の間では復権しているかもしれないが、セキュリティ上の問題で普段 off にしてるって人も一定程度いるだろうし、何より 「JavaScript が使えないと情報が手に入らない」という状況はユーザビリティ的にも SEO 的にもまずい。そうするとどこでどう使うかをよく考える必要がある。ま、楽しみを残してもらってるとも言えるけど。

リクエストヘッダの Accept に JavaScript 関係の情報が載れば便利なのになぁ。JavaScript が有効だったら JavaScript 用の情報を返す、そうでない場合は HTML を返すってのをクライアントサイドの JavaScript を使わずに判別できれば、JavaScript 用の分と <noscript> の分と両方返す必要がなくなる。どーーーにかうまいこといかんかなぁ、この辺。

今度は MochiKit 使ってみるかな。prototype.js だけだとバグ見つけにくい気がする。あと、当たり前だけどいくら prototype.js 使ってもイベントハンドラをセットする際の制限とか超えることはできない。prototype.js 使えば楽ちん!て思えるのは prototype.js 以前の苦労を知ってこそだなと思った。

成否の判定と PHP の error_reporting()

[2006-05-05 追記]

読み出し、更新についてわざと失敗させ3、それへの対応を作り込んでみた。ちなみに今回はライブラリは prototype.js のみ利用し、PHP 側は何も利用せずに全部手で書いて済ませた。便利なフレームワークを使えば PHP 側と JavaScript 側、という風に分けて考える必要はないかもしれないのだけれど、とりあえず練習ってことで。

で、失敗させてるんだけど、いつまで経っても失敗したときの status が返ってこない。4ブラウザ上では何が起きているのかさっぱり分からないのでサーバ側で簡単なログを吐く仕組みを用意し、狙ったメソッドに処理が移っているのかどうかを確認。うむ、確かに失敗している。でも livehttpheaders で確認すると 200 OK が返ってきている。あ、そうか。error が 200 OK の HTML で返っちゃってるんだ。うーむさすが PHP. てことは Ajax 作り込むときはテスト環境であっても PHP の error は全部 log に吐くようにしないとダメなんだな。

で、実際に読み出しに失敗させてみると、読み込む領域に「最初からは」編集可能ですよと目で分かるスタイルは設定しない方がよいことに気づいた。

どういうことかというと、

.editable {
  border: 1px solid #fefefe;
}
.editable:hover {
  border: 1px solid yellow;
}

こんな風に編集可能な領域はマウスカーソルを合わせると border に色がつくようにしておいたんだけど、このままだと内容がなくてもこの上をマウスが通過すると色が変わる。これはまずいので、読み出しのリクエストのときにこうしてみた。

$('box').setAttribute( 'class', 'editable' );
Event.observe( 'box',
               'dblclick',
               function() { make_editable($('box')) },
               false );

これを request の option の onSuccess にセットする5。つまり

  1. 読み出しが成功したら
  2. 編集可能な領域であることを示すスタイルを追加し、
  3. 編集モードにスイッチするためのイベントをセットする

box ってのは今回試しに用意しておいた、ここに編集可能な内容が入りますよっていう目印の div 要素。これはついでに

$('box').setAttribute( 'title', 'ダブルクリックすると編集できます。' );

こんな風にしておくと親切かなと思った。マウスが合ったときにここは編集できますよと教えてくれる。色が変わるだけだと意味が分からないもんね。

次、書き込みの失敗。書き込み時は

  1. 内容を送る前に見た目上は編集結果が反映される
  2. 成功したら黙ってそれが定着する
  3. 失敗したら
    1. 「失敗した!」ってメッセージを出して
    2. 送信前の編集状態に戻す

という処理にしてみた。このためには

  • 編集用の textarea
  • 通常の HTML として内容を表示
  • 更新に失敗した!ってメッセージ

の3つの表示を切り替える必要がある6。ここで上の edit-in-place のサンプルを読みながら感心したことは、

JavaScript で HTML を書き換えるときは DOM を変数として扱っちゃえばいいんだ

ってこと。生の JavaScript で書いていたときはこの感覚に至ることはできなかったけど、prototype.js のおかげで

Element.show( ID )
Element.hide( ID )
Element.remove( ID )
new Insertion.After( ID, 内容 )

などの超便利な書き方ができると、内容を変数に持つんじゃなくて、DOM そのものが変数なのねという心境になってくる。あーこれはものすごい楽だ。面白いぞ JavaScript.

ちなみに今回

  1. 編集に失敗した!ってメッセージを出し、
  2. ちょっと経ってから
  3. 編集画面に戻る

という処理をするためにどうしても sleep() がほしくなり、あちこち探しまわったが汎用の sleep() で使いたくなるものがなかったんだけど、これは

var failure_notice = '<div id="' + obj.id + '-failure">失敗しますた!</div>';
Element.hide( obj );
new Insertion.After( obj.id, failure_notice ); // メッセージを表示
setTimeout( function() {
  Element.show( obj.id + '-edit' );            // 編集画面を表示
  Element.hide( obj.id + '-failure' );         // メッセージを消去
}, 1500 );

こんな書き方ができることに気がついた。更新に失敗したら

  1. 今まで表示してた内容を消して
  2. 失敗しましたってメッセージを Insertion.After で作り、
  3. 1.5秒後に編集画面を on にしてメッセージを消す

sleep() 要らない。無名関数最強すぎ。

参考

  1. ファイル直読みじゃなくて PHP で開いて返す 

  2. PHP のマルチバイト関数がなんかどうも腐ってて変なハマり方したけど。 

  3. ファイルの permission をちょちょいといじるだけ 

  4. 今回は試しに 406 Not Acceptable を返すようにしてみた。500 Internal Server Error とかの方がいいのかな? 

  5. onSuccess ってのは prototype.js 依存なので他のライブラリを使う、あるいは生書きのときには通じないイディオム。 

  6. 実際には更新を諦めて cancel したときに元に戻せるようにしなくちゃいけないので4つの切り替えが必要。 

More