チュートリアル: シーサイド・ランデブー
2007/03/30 (Fri) 22:07:46 JST
※この文書は "Tutorial: A Walk on the Seaside" (http://beta4.com/seaside2/tutorial.html) の和訳です。今となっては多少バージョンが古めになってます。よければSeasideの元ネタでもあるWebObjectsについても見てってください。
このチュートリアルでは、Webアプリケーションフレームワークの Seaside (version 2.5) を紹介します。その前に次の準備をしておいてください。:
- Squeakのインストール。http://www.squeak.orgからダウンロードしてください。
- Squeak向けのComanche WebサーバとSeasideのインストール。どちらも?SqueakMapからダウンロードできます。Squeakのデスクトップでワールドメニューを開き、「開く」を選択して「?SqueakMapパッケージローダ」を選択します。パッケージローダから次の順にインストールしてください。
- SmalltalkとHTMLに関する基本的な知識。
Seasideをインストールできたら、ユーザ名とパスワードの入力を要求されます。これはSeasideの設定アプリケーションで使われるもので、後々触れます。
すべてインストールしたら、Seasideを起動してみましょう。SeasideにはComanche WebサーバのインターフェースとなるWAKomクラスを用意しています。Seaside用の設定されたComancheのインスタンスを起動するには、ワークスペースで次のコードを評価してください。
WAKom startOn: 9090
これでSeasideは9090番ポートでリクエストを受け付けるようになりました。この状態でイメージを保存してから再起動しても、Seasideは自動的にサービスを開始します。
基本的なコンセプト: セッションとコンポーネント
Seasideが起動しているかどうかを確認するため、ブラウザでhttp://localhost:9090/seaside/counterにアクセスしてください。最も小さいSeasideアプリケーション、増減を行うだけのシンプルなカウンタが登場するはずです。これからしばらく、この小さなアプリケーションを試していきますが、ちょっと確認してみましょう。"++"のリンクをクリックすると数値が増加し、"--"のリンクをクリックすると数値が減少します。
カウンタを試しながら、ブラウザに表示されているURLに注目してください。Seasideに与える重要なパラメータが二つ、URLのクエリに含まれています。一つは _s=?ZXsNyDAuodddtmEG のようなランダムな英数字の長い並びで、アプリケーションを使っている間は変化しません。これはユーザーセッションの (ユニークな) IDです。セッションはSeasideアプリケーションの中核を担っています。ほとんどのWebアプリケーションフレームワークと異なり、Seasideではリクエスト・レスポンスサイクルを越えて状態を維持するためにセッションオブジェクトを使います。Seasideはセッションをスレッドやプロセスのように扱います。あるタイミングでセッションが生成されると、アプリケーションはそこから処理を開始します。Webページを表示したり、フォームの入力を待つたびに処理は中断されます。この継続的な処理については、後で制御の流れを説明するときに詳しく触れます。
ページ下のグレーのツールバーにある "New Session" と書かれたリンクを見てください。このツールバーはアプリケーションの開発時に表示されます。"New Session" のリンクをクリックすると、現在のプロセス (セッション) を捨てて新しいプロセスを開始します。カウンタが0に戻り、セッションIDが変更されます。
次の短いパラメータ (_kで始まるクエリ) もランダムな英数字の並びです。このパラメータは何らかのアクションを実行させるものでも、セッションの状態をエンコードしたものでもありません。すべての状態はサーバ側で管理されており、Seasideはセッションを常に最新の状態に保っていますが、いつも何らかのアクションを実行しているわけではありません。Seasideのセッションはリクエストを追跡するので、ユーザの行動を把握できるようになります。また、Seasideサーバにデータを送信する必要のあるページでは、リンクやフォームのフィールドにもそれぞれIDが割り振られます。ただし、URLが複雑になり、しかも一度きりしか使えません。ブックマークすることを考えるとかなり不便ですが、それだけに大きな柔軟性があります。
通常、Seasideではページやアクションの代わりに「コンポーネント」を組み合わせてアプリケーションを構築します。コンポーネントはユーザーインターフェースを管理する状態とロジックをまとめたオブジェクトです。セッションが始まるとコンポーネントのインスタンスが一つ生成され、そのインスタンスはレスポンスループに移行します。つまり、コンポーネントをWebページとしてユーザに表示し、ユーザからの入力 (リンクをクリックしたり、フォームを送信する) を待つことを繰り返します。ユーザ入力はコンポーネントの特定のメソッドと結びついていて、入力があるとコンポーネントのメソッドが実行され、再びそのコンポーネントがWebページとして表示されます。これらのメソッドはアプリケーションやコンポーネントの状態を変更したり、他のコンポーネントのインスタンスを生成して制御を移したり (ページ遷移) したあと、再びレスポンスループに戻ります。
カウンタアプリケーションで使っているコンポーネントのクラスは"WACounter"です。次はこのクラスを見ていきましょう。
状態、アクション、表示: コンポーネントの真髄
コンポーネントには三つの仕事があります。ユーザーインターフェースの状態を維持すること、ユーザ入力に反応すること、コンポーネントをHTMLとして表示することです。ここではWACounterを例に、それぞれの処理がどのように行われるのかをざっと見ていきます。
状態
アプリケーションの状態がビジネスオブジェクトやデータベースにきちんと保存されていても、ユーザーインターフェースが状態を持つことがあります。例えば、フォームのフィールドに入力された値、表示され続けているデータベースレコード、展開されたツリー構造などがあります。いずれもユーザーインターフェースを構成しているコンポーネントのインスタンス変数に保持されます。
WACounterの場合、維持する必要があるのはカウントのみです。セッションが開始され、WACounterのインスタンスが生成されると、インスタンス変数"count"には0が代入されます。"++"や"--"のリンクをクリックすると、そのインスタンス変数の値が変化します。これを調べるには、ツールバーにある"Toggle Halos"をクリックするといいでしょう。ページ上部に表示される目のアイコンをクリックすると、Web用のインスペクタが開きます。WACounterが、WAComponentなどのスーパークラスから継承したインスタンス変数を持っていることがわかります。"count"は最後に表示されているはずです。インスペクタで表示されている"count"の値は、先程のページで表示されたカウント数と常に一致しています。
さらに調べてみたいなら、インスタンス変数のリンクをクリックすれば、オブジェクトの中身を表示する新しいインスペクタが開きます。オブジェクトの階層を調べるにはもってこいでしょう。調べ終えたら右上の"close"をクリックして、インスペクタウィンドウを閉じます。コンポーネントの周りのアイコンを消したければ、再度"Toggle Halos"をクリックします。
アクション
カウンタアプリケーションにはユーザのできる操作が二つあり、WACounterはそれぞれの操作に対応したメソッドを持っています。"++"リンクをクリックすれば#increase
が呼ばれます。その実装はごく簡単なものです:
increase count := count + 1
思った通りの実装でしょう?countの値を一つ増やすだけです。メソッドの評価が終わると再びレスポンスループに戻り、コンポーネントは新しいカウンタ値を表示します。
では、ちょっと遊んでみましょう。"Toggle Halos"をクリックしてコンポーネントの周りにアイコンを表示し、目のアイコンの左にあるレンチのアイコンをクリックすると、Webベースの高機能なシステムブラウザが開きます (Lukas Renggli氏に感謝します) 。現在のコンポーネントクラス"WACounter"が選択されているはずですので、#increase
メソッドを選択して、カウントを2つずつ増やすように変更します。変更後、Acceptボタンをクリックするのを忘れないでください。Acceptできたらシステムブラウザを閉じて、"++"リンクをクリックしてみましょう。
表示
Seasideがコンポーネントを表示するとき、コンポーネントには #renderContentOn:
メッセージが送られ、引数には?WAHtmlRendererクラスのインスタンスが渡されます。この流れはJavaアプレットやSqueakのMorphに似ています。引数の?WAHtmlRendererオブジェクトはキャンバスであり、コンポーネントは自身をキャンバスに描かなくてはなりません。ただし、このキャンパスには図形や線ではなくHTMLの要素を描き込んでいきます。?WAHtmlRendererを含む描画オブジェクトはテキストやHTML要素を追加するメソッドを持ち、ストリームのように使うことができます。WACounterの #renderContentOn:
メソッドは次のようになっています。
renderContentOn: html html heading: count. html anchorWithAction: [self increase] text: '++'. html space. html anchorWithAction: [self decrease] text: '--'.
描画オブジェクトには三つのメッセージが送られ、それぞれ異なるHTML要素を生成します。最初の #heading:
は見出しを生成します。countの値が42であれば、 <h1>42</h1>
が生成されます。#space
は名前の通りスペースを生成します。#anchorWithAction:text:
が興味深いメッセージで、"++"と"--"のリンクを生成します。text:
キーワードの引数はリンクとして表示するテキストです。しかし、このリンクは何を表し、私達をどこへ導くのでしょうか?
いえ、何も心配することはありません。Seasideのリンクはどこかのページに導くのではなく、コンポーネントを呼び戻すためにあります。ここで生成したリンクやボタンは、ブロックと共に動作します。リンクやボタンをクリックすると、そのブロックが評価されます。上記のコードの場合、"++"のリンクをクリックすると、自身に #increase
を送るブロックが評価されます。
ここでちょっとだけ変更してみましょう。カウントを増減するリンクの代わりに、送信ボタンを使うことにします。システムブラウザで WACounter>>renderContentOn:
を開き、次のように変更してください。
renderContentOn: html html form: [ html heading: count. html submitButtonWithAction: [self increase] text: '++'. html space. html submitButtonWithAction: [self decrease] text: '--' ].
主な変更は #anchorWithAction:text:
を #submitButtonWithAction:
に変更したことです。どちらのメソッドもカウントを増減する点は同じです。必要に応じて、インターフェースをリンクにもボタンにも簡単に変更することができます。ただし、送信ボタンはフォームの中でしか動きません。そこで #form
メソッドにブロックを与え、送信ボタンを囲むフォームを生成します。このブロックは #anchorWithAction:text:
に渡すブロックとは違い、リンクや送信ボタンから呼ばれるものではありません。このブロックを評価したときに生成されるHTMLの要素を囲むように、フォームの要素が生成されることになります。同じようにブロックを受け取るメソッドに、テーブルの要素を生成するメソッド #table:
, #tableRow:
, #tableData:
があります。
レスポンスループ
ここまでコンポーネントの仕事を紹介してきましたが、それぞれの処理はどう関連しているのでしょうか?各コンポーネントはレスポンスループという枠組みの中で動作します。レスポンスループでは、コンポーネントを表示することから始まり、ユーザからの入力を待ち、受け取った入力を処理し、再びコンポーネントを表示するという流れになります。カウンタアプリケーションを例にとると、次の流れになります。
- WACounterのインスタンスが生成され、インスタンス変数 count の値が 0 に初期化されます。
-
#renderContentOn:
が呼ばれ、HTMLを生成します。count の値を表示し、クリックすると指定したブロックを評価するリンクを生成します。生成したHTMLはブラウザに送られます。 - ユーザがリンクをクリックすると、
#renderContentOn:
内で指定したブロックが評価されます。ブロックでは#increase
か#decrease
のメッセージがコンポーネント自身に送られます。これで count の値が変化し、アクションメソッドは終了します。 - カウンタが再び表示されます。
さらに詳しく: コンポーネント間の通信
他のコンポーネントに制御を移すには、WAComponentの #call:
メソッドを使います。このメソッドは引数にコンポーネントを受け取り、即座にそのコンポーネントのレスポンスループを開始、表示します。?WAFormDialogコンポーネントクラスで試してみましょう。#decrease
メソッドを、カウントを0以下にしようとするとメッセージを表示するように変更します。
decrease count = 0 ifFalse: [count := count - 1] ifTrue: [self call: (WAFormDialog new addMessage: 'Let''s stay away from negatives.'; yourself)]
countが0であれば?WAFormDialogのインスタンスを生成し、メッセージを与え、最後に呼び出します (=#call:) 。新しいセッションを生成して、"--"のリンクをクリックしてください。カウンタは表示されず、大きな文字でメッセージが表示されるはずです。
WAComponentには、これに似たような簡単なダイアログを表示する #inform:
メソッドがあります。これはObjectの同名メソッドをオーバーライドしたものです。今度は #onfrom:
を使って、 #decrease
を次のように変更してみましょう。
decrease count = 0 ifFalse: [count := count - 1] ifTrue: [self inform: 'Let''s stay away from negatives.']
このメッセージと共に"OK"と書かれたボタンが表示されることがわかるでしょう。そのボタンをクリックするとどうなりますか?そう、メッセージが消えてカウンタが再び表示されます。舞台裏では、?WAFormDialogのインスタンスが #call:
メソッドと組み合わせて使われる #answer
メソッドを評価し、制御を呼出元のコンポーネント (WACounter) に戻します。つまり、?WAFormDialogは一時的な処理を行うためだけに呼ばれたことになります。#call:
が新しいコンポーネントをスタックに積むとすれば、 #answer
は古いコンポーネントをスタックから一つ取り出すと言えます。
実際にはスタックが積まれているわけではありませんが、 #answer
の動作は少しばかり複雑です。アプリケーションの制御を #call:
が呼ばれたところまで戻し、そこから処理を再開します。#decrease
内でダイアログが呼ばれたあとは、メソッドの評価を終えてレスポンスループに戻ります。
あまりうまく説明できませんでしたが、本当はもっとたくさんの説明が要ります。次の処理をきちんと説明するのはどうか勘弁してください。
- WACounter>>decrease が WAComponent>>inform: を呼びます。
- WAComponent>>inform: は?WAFormDialogのインスタンスを生成し、WAComponent>>call: に渡します。この処理を「呼び出し地点 (the call point) 」と呼びます。
- ?WAFormDialogはレスポンスループを開始し、ブラウザにメッセージを表示します。
- ユーザが"OK"ボタンをクリックすると、?WAFormDialogの WAComponent>>answer メソッドが起動されます。
- ここからちょっとした処理が始まります。WAComponent>>answer は制御を
#call:
が呼ばれた直後の「呼び出し地点」まで戻してしまうため、決して終了しません。 - WAComponent>>call: が終了し、 WAComponent>>inform: に制御を戻します。
- WAComponent>>inform: が終了し、 WACounter>>decrease. に制御を戻します。
- WACounter>>decrease が終了し、カウンタアプリケーションのレスポンスループに制御を戻し、ブラウザに表示します。
#call:
が本当にすごいのは、 (#call:
から) 呼び出されたコンポーネントが #answer:
に引数を与えると、その引数が (呼出元の) #call:
の戻り値になることです。言い替えれば、呼ばれたコンポーネントが結果を作ることになります (※コンポーネントがブロックのような働きをするということだと思う) 。スタックを使ってコンポーネントを出し入れするよりも、ずっとシンプルでパワフルです。例えば、 #confirm:
でユーザに真偽を問い合わせ、その結果によって #inform:
を実行するような処理も簡単に実装できます。#decrease
を次のように変更してください。
decrease count = 0 ifFalse: [count := count - 1] ifTrue: [(self confirm: 'Do you want to go negative?') ifTrue: [self inform: 'Ok, let''s go negative!'. count := -100]].
カウンタを触ってみると、3つのページが表示されるようになったことに気づくでしょう。確認のダイアログが増え、"Ok, let's go negative"と表示されてからカウンタに戻るようになります (戻るボタンの押したらどうなるかも試してみてください) 。これはSeasideアプリケーションでは一般的な構造です。関連する複数のページを管理するのではなく、各ページが前後のページを知っています。各ページはユーザから情報を集め、処理し、返すという一連の作業をこなします。このおかげで、縦横無尽に再利用のできるようなコードを書けるようになります。
コンポーネントを再利用する方法は #call:
と #answer:
だけではありません。まだわからないかと思いますが、これまでに少なくとも二つのコンポーネントが使われています。一つはWACounterですが、もう一つは何でしょうか?実は、その答えはすでに目にしているはずです。WACounterの周りにあるツールバーを表示する?WAToolFrameが、もう一つのコンポーネントです。?WAToolFrameはWACounterと別々に扱われているのではなく、?WAToolFrameがWACounterを内包する構造になっています。Seasideではよくある構造で、各コンポーネントはネストしていてもそれぞれは独立しています。コンポーネントのネストはこのチュートリアルの範疇を超えますが、簡単な例である?WAMultiCounterを用意してあります (http://localhost:9090/seaside/multi) 。このサンプルアプリケーションでは、複数のWACounterを使っています。"Toggle Halos" をクリックしていろいろと試してみてください。
さあ始めよう、Seasideアプリケーションを
もうSeasideがどんなものかわかってきたでしょうか?Seasideでアプリケーションを作り始める前に、ぜひ設定アプリケーションを試してみてください。設定アプリケーションのURLは http://localhost:9090/seaside/config です。Seasideのインストール時に入力したユーザ名とパスワードが必要です。このアプリケーションでは新しいアプリケーションを追加したり、アプリケーションで使われるコンポーネントクラスを選ぶことができます。コンポーネントを設定アプリケーションで使えるようにするには、#canBeRoot
クラスメソッドを実装し、trueを返すようにしてください。
このチュートリアルへの意見はavi@beta4.comまでどうぞ (※訳はsuzuki@spice-of-life.netまで) 。またはSeasideのメーリングリスト (http://lists.squeakfoundation.org/listinfo/seaside) に投稿してください。
※訳者追記: ユーザ名とパスワードの取得
Seasideのインストール時に入力したユーザ名とパスワードを忘れてしまったら、次のコードで取得できます。
| app | app := WADispatcher default entryPoints at: 'config'. app preferenceAt: #login. "ログイン名" app preferenceAt: #password. "パスワード"
Inverse Pages: Seaside