ラベル 翻訳 の投稿を表示しています。 すべての投稿を表示
ラベル 翻訳 の投稿を表示しています。 すべての投稿を表示

2010年1月11日月曜日

Decorating With Style!

私たちのアプリは機能的には完璧なんですが、見た目がショボいです。そこで外観を改良しましょう。一つの方法はキャスケーディング・スタイル・シートを使う事です。スタイル・シートはwebページの見た目を良くします。例えば、段落全てを緑色にしたい場合、レスポンスを変更する為に以下のスタイル宣言を追加します。


'(style ((type "text/css")) "p { color: green }")


html-responseにこのスタイル情報を直接埋め込んでも結構です。しかしながら、ソースファイルは既にかなりゴチャゴチャしてきています。そこでしばしば、アプリの論理表現を表示部分から切り離すことをします。HTMLレスポンス部分に.cssを直接埋め込む代わりに、分離した.cssファイルへとリンク指定を行うのです。

今までは、webアプリのコンテンツの全ては、レスポンス生成ハンドラによって生成されてきました。もちろん、全てが動的に生成される必要はありません:変更の必要がないファイル内の共通部分がある筈です。つまり、これら静的リソース(写真、文章、.cssファイルなど)をwebアプリとはまた別に提供すべきなんです。

そのために、これらのファイルを保存したパスを指定し、ウェブサーバーにディレクトリがどこにあるか教えます。static-files-path

static-files-path : (path-string? -> void)

はウェブサーバが静的リソースへのリクエストがあるURLを受け取った時、指定されたパスを覗く機能です。

Exercise.以下のコンテンツを含む"test-static.ss"と名づけられた簡単なwebアプリを書いてください。

#lang web-server/insta
(define (start request)
'(html (head (title "Testing"))
(link ((rel "stylesheet")
(href "/test-static.css")
(type "text/css")))
(body (h1 "Testing")
(h2 "This is a header")
(p "This is " (span ((class "hot")) "hot") "."))))

(static-files-path "htdocs")

"test-static.ss"ソースファイルが置いてあるディレクトリをルートとして、"htdocs"と名づけたサブディレクトリを作成してください。最後に、この.cssページを表示する為に、"htdocs/"内に以下の内容を持つ"test-static.css"と言う簡単な.cssファイルを作成してください。

body {
margin-left: 10%;
margin-right: 10%;
}
p { font-family: sans-serif }
h1 { color: green }
h2 { font-size: small }
span.hot { color: red }

この時点で、アプリを走らせてブラウザの出力を見てください。質素なWebページでしょうが、ほんのりと色が付いています。




Exercise.あなたの趣味に見合った外部スタイルシートを記述し、ブログアプリの表示を改善してみましょう。スタイルシートへのリンクを含むように全てのHTMLレスポンスハンドラを調整してみましょう。

Adding a Back Button

ここに、私たちのWebアプリの改良版ページフローのダイアグラムがあります。render-post-detail-pageからブログのトップレベルに戻るバックリンクを足すだけです。



Exercise.render-post-detail-pagerender-blog-pageへ戻る別リンクを含むように調整してください。

もっと面白くするには、フローをもうちょっとだけ豪華にしてみましょう。ユーザにコメントを投稿する直前に選択肢を与えるのです。ひょっとしたらひょっとするでしょう。投稿前に投稿を止めたい、って思うかもしれませんので。



複雑に見えますが、ハンドラの全体像は以前とさほど変わりません。これらハンドラを全て追加すると、webアプリはかなり機能的になります。


#lang web-server/insta

; ブログは (make-blog posts)
; 投稿リストは (listof post)
(define-struct blog (posts) #:mutable)

; 投稿は (make-post title body comments)
; タイトルは文字列、本体も文字列
; コメントは (listof string)
(define-struct post (title body comments) #:mutable)

; BLOG: blog
; ブログの初期状態
(define BLOG
(make-blog
(list (make-post "First Post"
"This is my first post"
(list "First comment!"))
(make-post "Second Post"
"This is another post"
(list)))))

; blog-insert-post!: blog post -> void
; ブログと投稿を受け取り、ブログの頭に投稿を加える
(define (blog-insert-post! a-blog a-post)
(set-blog-posts! a-blog
(cons a-post (blog-posts a-blog))))


; post-insert-comment!: post string -> void
; 投稿とコメント文字列を受け取る。副作用として投稿の
; コメントリストの最後にコメントを追加する。
(define (post-insert-comment! a-post a-comment)
(set-post-comments!
a-post
(append (post-comments a-post) (list a-comment))))

; start: request -> html-response
; リクエストを受け取り、webコンテンツ全てを表示する
; ページを生成する
(define (start request)
(render-blog-page request))

; render-blog-page: request -> html-response
; ブログの中身のhtml-responseページを
; 生成する
(define (render-blog-page request)
(local [(define (response-generator make-url)
`(html (head (title "My Blog"))
(body
(h1 "My Blog")
,(render-posts make-url)
(form ((action
,(make-url insert-post-handler)))
(input ((name "title")))
(input ((name "body")))
(input ((type "submit")))))))

; parse-post: bindings -> post
; 束縛から投稿を抽出する
(define (parse-post bindings)
(make-post (extract-binding/single 'title bindings)
(extract-binding/single 'body bindings)
(list)))

(define (insert-post-handler request)
(blog-insert-post!
BLOG (parse-post (request-bindings request)))
(render-blog-page request))]

(send/suspend/dispatch response-generator)))

; render-post-detail-page: post request -> html-response
; 投稿を受け取り、投稿の詳細ページを生成する
; ユーザは新規コメントを挿入できる
; また、render-blog-pageに戻る事が出来る
(define (render-post-detail-page a-post request)
(local [(define (response-generator make-url)
`(html (head (title "Post Details"))
(body
(h1 "Post Details")
(h2 ,(post-title a-post))
(p ,(post-body a-post))
,(render-as-itemized-list
(post-comments a-post))
(form ((action
,(make-url insert-comment-handler)))
(input ((name "comment")))
(input ((type "submit"))))
(a ((href ,(make-url back-handler)))
"Back to the blog"))))

(define (parse-comment bindings)
(extract-binding/single 'comment bindings))

(define (insert-comment-handler request)
(render-confirm-add-comment-page
(parse-comment (request-bindings request))
a-post
request))

(define (back-handler request)
(render-blog-page request))]

(send/suspend/dispatch response-generator)))

; render-confirm-add-comment-page :
; comment post request -> html-response
; リクエストと共に投稿予定のコメントを受け取る
; ユーザはコメントを投稿し、表示ページに戻れるようにする
; もしくは投稿の詳細ページへと戻る
;
(define (render-confirm-add-comment-page a-comment a-post request)
(local [(define (response-generator make-url)
`(html (head (title "Add a Comment"))
(body
(h1 "Add a Comment")
"The comment: " (div (p ,a-comment))
"will be added to "
(div ,(post-title a-post))

(p (a ((href ,(make-url yes-handler)))
"Yes, add the comment."))
(p (a ((href ,(make-url cancel-handler)))
"No, I changed my mind!")))))

(define (yes-handler request)
(post-insert-comment! a-post a-comment)
(render-post-detail-page a-post request))

(define (cancel-handler request)
(render-post-detail-page a-post request))]

(send/suspend/dispatch response-generator)))

; render-post: post (handler -> string) -> html-response
; 投稿を受け取り、投稿のhtml-response要素を生成する
; 要素は投稿詳細ページへのリンクを含む
(define (render-post a-post make-url)
(local [(define (view-post-handler request)
(render-post-detail-page a-post request))]
`(div ((class "post"))
(a ((href ,(make-url view-post-handler)))
,(post-title a-post))
(p ,(post-body a-post))
(div ,(number->string (length (post-comments a-post)))
" comment(s)"))))

; render-posts: (handler -> string) -> html-response
; make-urlをつけトリ、全投稿のhtml-response
; 要素を生成する
(define (render-posts make-url)
(local [(define (render-post/make-url a-post)
(render-post a-post make-url))]
`(div ((class "posts"))
,@(map render-post/make-url (blog-posts BLOG)))))

; render-as-itemized-list: (listof html-response) -> html-response
; アイテムのリストを受け取り、未整列のリストとして
; レンダリングを生成する
(define (render-as-itemized-list fragments)
`(ul ,@(map render-as-item fragments)))

; render-as-item: html-response -> html-response
; html-responseを受け取り、リストアイテムとして
; レンダリングを生成する
(define (render-as-item a-fragment)
`(li ,a-fragment))

2010年1月10日日曜日

Breaking Up the Display

コメントをユーザのweb体験ともっと統合するにはどうすれば良いのでしょう?投稿全部とコメント全部がページに全部表示されている、ってのはちょっと大げさでしょう。多分、ブログのメインページとコメント表示は別々にした方が良いでしょう。投稿の見え方の補助的な「ディティール」と、そこでのコメントのあり方を提案してみましょう。

さて、ブログのトップレベルの見え方はブログのタイトルと本体とします。ついでに、投稿に関係したコメントが何個付いたのか、カウンターも表示しましょう。

そうすると、投稿の詳細ページに到達する何らかの方法が必要となります。一つの手は、各投稿のタイトルにハイパーリンクを貼る事です:ユーザが投稿の詳細ページを見たければ、そこへ行くためタイトルをクリックすれば良い。投稿の詳細ページに、ユーザが新規のコメントを追加出来るフォームを追加すれば良いでしょう。

以下はwebアプリにコメントを追加する簡単なページフローのダイアグラムです。



ダイアグラムの各場所はリクエストを受け取るハンドラに対応しています。予想どおり、もうちょっとsend/suspend/dispatchを使いましょう。ダイアグラム中の矢印はembed/urlで生成されたURLを表しています。

これはちょっとだけ複雑な結果をもたらします:以前は、ハイパーリンクなしの投稿リストをレンダリングしてました。しかし、全ての特殊な移動用URLを生成する機能がembed/urlを使う以上、ハイパーリンクタイトルを作るときembed/urlを使って受け取るrender-postsrender-postを調整する必要があります。

Webアプリは、現時点、次のようになります:


#lang web-server/insta

; ブログは (make-blog posts)
; 投稿リストは (listof post)
(define-struct blog (posts) #:mutable)

; 投稿は (make-post title body comments)
; タイトルは文字列で本体も文字列
; コメントは (listof string)
(define-struct post (title body comments) #:mutable)

; BLOG: blog
; ブログの初期値
(define BLOG
(make-blog
(list (make-post "First Post"
"This is my first post"
(list "First comment!"))
(make-post "Second Post"
"This is another post"
(list)))))

; blog-insert-post!: blog post -> void
; ブログと投稿を受け取り、ブログの頭に投稿を付け加える
(define (blog-insert-post! a-blog a-post)
(set-blog-posts! a-blog
(cons a-post (blog-posts a-blog))))


; post-insert-comment!: post string -> void
; 投稿とコメント文字列を受け取る。副作用として投稿の
; コメントリストの一番下にコメントを付け加える
(define (post-insert-comment! a-post a-comment)
(set-post-comments!
a-post
(append (post-comments a-post) (list a-comment))))

; start: request -> html-response
; リクエストを受け取り、webコンテンツの全てを表示する
; ページを生成する
(define (start request)
(render-blog-page request))

; render-blog-page: request -> html-response
; ブログの内容のhtml-responseページを
; 生成する
(define (render-blog-page request)
(local [(define (response-generator make-url)
`(html (head (title "My Blog"))
(body
(h1 "My Blog")
,(render-posts make-url)
(form ((action
,(make-url insert-post-handler)))
(input ((name "title")))
(input ((name "body")))
(input ((type "submit")))))))

; parse-post: bindings -> post
; 束縛から投稿を抽出する
(define (parse-post bindings)
(make-post (extract-binding/single 'title bindings)
(extract-binding/single 'body bindings)
(list)))

(define (insert-post-handler request)
(blog-insert-post!
BLOG (parse-post (request-bindings request)))
(render-blog-page request))]

(send/suspend/dispatch response-generator)))

; render-post-detail-page: post request -> html-response
; 投稿とリクエストを受け取り投稿の詳細ページを生成する
; ユーザは新規コメントを挿入可能
(define (render-post-detail-page a-post request)
(local [(define (response-generator make-url)
`(html (head (title "Post Details"))
(body
(h1 "Post Details")
(h2 ,(post-title a-post))
(p ,(post-body a-post))
,(render-as-itemized-list
(post-comments a-post))
(form ((action
,(make-url insert-comment-handler)))
(input ((name "comment")))
(input ((type "submit")))))))

(define (parse-comment bindings)
(extract-binding/single 'comment bindings))

(define (insert-comment-handler a-request)
(post-insert-comment!
a-post (parse-comment (request-bindings a-request)))
(render-post-detail-page a-post a-request))]


(send/suspend/dispatch response-generator)))


; render-post: post (handler -> string) -> html-response
; 投稿を受け取り、投稿のhtml-response要素を生成する
; 要素は投稿の詳細ページを示すリンクを含む
(define (render-post a-post make-url)
(local [(define (view-post-handler request)
(render-post-detail-page a-post request))]
`(div ((class "post"))
(a ((href ,(make-url view-post-handler)))
,(post-title a-post))
(p ,(post-body a-post))
(div ,(number->string (length (post-comments a-post)))
" comment(s)"))))

; render-posts: (handler -> string) -> html-response
; make-urlを受け取り、全投稿のhtml-response
; 要素を生成する
(define (render-posts make-url)
(local [(define (render-post/make-url a-post)
(render-post a-post make-url))]
`(div ((class "posts"))
,@(map render-post/make-url (blog-posts BLOG)))))

; render-as-itemized-list: (listof html-response) -> html-response
; アイテムのリストを受け取り未整列のリストとして
; レンダリングを生成する
(define (render-as-itemized-list fragments)
`(ul ,@(map render-as-item fragments)))

; render-as-item: html-response -> html-response
; html-responseを受け取り、リストアイテムとして
; レンダリングを生成する
(define (render-as-item a-fragment)
`(li ,a-fragment))

私たちは極めて洗練されたアプリを手に入れました:今は投稿も出来るしコメントも書けます。しかしながら、まだこの問題があります:ユーザがpost-detail-pageにいるとブラウザの戻るボタンを押すことなくブログに戻る事が出来ないのです!これじゃあ混乱します。ユーザがwebアプリの片隅で立ち往生せず、ブログのメインページへと戻れるようなページフローを提供しなければなりません。



Extending the Model

次に、投稿毎にコメントのリストを保持出来るようにアプリを拡張しましょう。ブログのデータ定義を次のように改良します:

(struct post (title body comments)
#:mutable)
title : string?
body : string?
comments : (listof string?)


Exercise.改良した投稿のデータストラクチャを書いてみてください。投稿にコメントを加える事を意図して、ストラクチャは変更可能にしてください。

Exercise.いくつか投稿の例を作ってみてください。

Exercise.post-add-comment!機能を定義してください。


post-add-comment! : (post? string? . -> . void)


意図的な副作用は、投稿のコメントリストの最後に新しいコメントを追加するものとします。

Exercise.render-postを生成された要素が項目別リストでコメントを含むように調整してみてください。

Exercise.投稿がコメントを含むように拡張したので、アプリの他の投稿操作部分、例えばmake-postの用途など、も調整が必要となります。投稿の新しいストラクチャへの適応が必要なアプリの他の部分を見分けて修正してください。



投稿のデータストラクチャを変更して、改良したストラクチャを扱う機能を調整すると、webアプリは実行可能となります。ユーザは労力の成果が分かるでしょう:BLOGの初期値がコメント付きの投稿であれば、ユーザは現時点でコメントを見る事が出来るでしょう。しかし明らかに足りないものがあります:ユーザ向けの、投稿にコメントを追加するユーザーインターフェースが無いのです!

Share and Share Alike

私たちのアプリは新たな問題に出くわします:別々のブラウザウィンドウそれぞれに独自のブログが保持されたままです。これだと、殆どの人の為のブログの利点、つまり他人との共有が出来ません!新規投稿を挿入すると、新しいブログ値を生成するより、既存のブログを構造的に変更したほうが良いに決まっています。(HTDP41章)。そこで、構成に変更機能を追加しましょう。

ちょっとした詳細に目を向けましょう:web-server言語では、デフォルトではストラクチャは変更不可なのです。このデフォルトを無効にして、ストラクチャ変更子へのアクセスが欲しいのです。そうするにはストラクチャ定義に#:mutableを付け加えます。

初めに、blogpostのリストだ、としましたが、ブログの変更を許可し、ブログを変更可能なストラクチャとする為、定義に戻りたいと思います。


(define-struct blog (posts) #:mutable)



(struct blog (posts))
posts : (listof post?)


変更可能なストラクチャはストラクチャのフィールドを変更する為の機能を提供します;この場合、set-blog-posts!と呼ばれるストラクチャ変更子を手にします。


set-blog-posts! : (blog? (listof post?) . -> . void)


これでブログの投稿が変更可能になります。

Exercise.blog-insert-post!機能を書いてみましょう。


blog-insert-post! : (blog? post? . -> . void)


この機能の意図的な副作用はブログの投稿を拡張します。


ブログのデータ表現を変更したので、それを用いてwebアプリを改良します。また、注意しなければならない事は、webアプリ内部では同じブログ値を共有しているので、もはやハンドラを用いてそれをたらい回しにする必要がなくなりました:現時点のブログはBLOG変数を通して与えられます。

insert-blog-post!を加えた調整と変数の整理を経過して、私たちのwebアプリは今はこのようになります:


#lang web-server/insta

; ブログは a (make-blog posts)
; 投稿リストは (listof post)
(define-struct blog (posts) #:mutable)

; 投稿は (make-post title body)
; タイトルは文字列、本体も文字列
(define-struct post (title body))

; BLOG: blog
; 初期状態のブログ
(define BLOG
(make-blog
(list (make-post "First Post" "This is my first post")
(make-post "Second Post" "This is another post"))))

; blog-insert-post!: blog post -> void
; ブログと投稿を受け取り、ブログの頭に投稿を加える
(define (blog-insert-post! a-blog a-post)
(set-blog-posts! a-blog
(cons a-post (blog-posts a-blog))))

; start: request -> html-response
; リクエストを受け取り、webコンテンツ全てを
; 表示するページを生成する
(define (start request)
(render-blog-page request))

; parse-post: bindings -> post
; 束縛から投稿を抽出する
(define (parse-post bindings)
(make-post (extract-binding/single 'title bindings)
(extract-binding/single 'body bindings)))

; render-blog-page: request -> html-response
; BLOGの中身のhtml-responseページを生成する
(define (render-blog-page request)
(local [(define (response-generator make-url)
`(html (head (title "My Blog"))
(body
(h1 "My Blog")
,(render-posts)
(form ((action
,(make-url insert-post-handler)))
(input ((name "title")))
(input ((name "body")))
(input ((type "submit")))))))

(define (insert-post-handler request)
(blog-insert-post!
BLOG (parse-post (request-bindings request)))
(render-blog-page request))]

(send/suspend/dispatch response-generator)))

; render-post: post -> html-response
; 投稿を受け取り、投稿のhtml-response要素を生成する
(define (render-post a-post)
`(div ((class "post"))
,(post-title a-post)
(p ,(post-body a-post))))

; render-posts: -> html-response
; ブログを受け取り、全投稿のhtml-response
; 要素を生成する
(define (render-posts)
`(div ((class "posts"))
,@(map render-post (blog-posts BLOG))))

webアプリを訪ねた二つのウィンドウを開いて、両方のウィンドウから投稿してみてください。両方のブラウザが同じブログを共有していることが分かるでしょう。

Advanced Control Flow

暫くの間、ブログがたった一つしか新規投稿を受け付けない、と言う明らかに大きな問題は無視しましょう。心配しないで!これは後で直します。

しかしながら、私たちのプログラムにはもっと高度な問題があります:と言うのも、startはアプリのURLにリクエストを送る機能なんですが、あまりにも多くの役割がありすぎて負荷がかかってきています。概念的には、startは現時点、二つの種類のリクエストを処理しています:ブログを表示するリクエストとブログへの新規投稿を加えるリクエストと、です。

一体、startが、私たちのWebアプリの全ての振る舞いを、お巡りさんのように --- dispatcherと呼びますが --- 交通整理をするようになったらどうなるのでしょうか?想像してみると、アプリに機能を追加するたび、startは制御方法を知らなくてはなりません。果たして、違う種類のリクエストがある度に、自動的に違う機能へと振り分ける方法なんてあるんでしょうか?

web serber ライブラリにはURLを生成して、アプリの別々のパーツへと振り分けるsend/suspend/dispatchと言う機能が用意されています。素敵なデモを紹介しましょう。新規ファイルを立ち上げて、以下のコードを定義ウィンドウに入力してください。


#lang web-server/insta
; start: request -> html-response
(define (start request)
(phase-1 request))

; phase-1: request -> html-response
(define (phase-1 request)
(local [(define (response-generator embed/url)
`(html
(body (h1 "Phase 1")
(a ((href ,(embed/url phase-2)))
"click me!"))))]
(send/suspend/dispatch response-generator)))

; phase-2: request -> html-response
(define (phase-2 request)
(local [(define (response-generator embed/url)
`(html
(body (h1 "Phase 2")
(a ((href ,(embed/url phase-1)))
"click me!"))))]
(send/suspend/dispatch response-generator)))

これはグルグル回るwebアプリです。ユーザがアプリを最初に訪れると、phase-1が始まります。そのページはハイパーリンクを生成し、クリックするとphase-2へ飛びます。ユーザがクリックするとまたphase-1へと戻り、これが延々と繰り返されます。




もうちょっと丁寧にsend/suspend/dispatchのメカニズムを見てみましょう。send/suspend/dispatchはレスポンス生成機能を受け取り、そして特殊なURLを作るembed/urlと呼ばれるレスポンス生成機能を返します。このURLが特殊なのは次のような意味です:ウェブブラウザがこれらのURLを訪ねると、webアプリが再起動しますが、それはstartからではなく、このURLに関連したハンドラから立ち上がる、と言う意味です。Phase-1ではembed/urlの用途はPhase-2に関連してて、逆もまた同じ、と言う事です。

embed/url絡みのハンドラをもっと綺麗にしてみましょう。ハンドラは単にリクエストを受け取る機能なので、localで定義可能です。結局、ローカル定義のハンドラは定義のスコープ内に存在する全ての変数を捕捉します。もう一つ、ループ的な例を見てみます。


#lang web-server/insta
; start: request -> html-response
(define (start request)
(show-counter 0 request))

; show-counter: number request -> html-response
; ハイパーリンクで数値を表示し、リンクがクリックされると
; 数値が増えた新しいページを生成する
(define (show-counter n request)
(local [(define (response-generator embed/url)
`(html (head (title "Counting example"))
(body
(a ((href ,(embed/url next-number-handler)))
,(number->string n)))))

(define (next-number-handler request)
(show-counter (+ n 1) request))]

(send/suspend/dispatch response-generator)))

この例は、対話的結果が累積可能である、と言う事を示しています。ユーザがページを訪問し、ページを生成してゼロを目にしたとしても、ハンドラは対話を介して次の数値へ続くハンドラを生成し、値はどんどん累積されていくのです。





ちょっと寄り道し過ぎたので、ブログアプリへと戻りましょう。フォームの動作を別のハンドラに関連したURLと結びつけるように調整します。


#lang web-server/insta

; ブログは (listof post) で
; 投稿は (make-post title body)
(define-struct post (title body))

; BLOG: blog
; 静的ブログ
(define BLOG
(list (make-post "First Post" "This is my first post")
(make-post "Second Post" "This is another post")))

; start: request -> html-response
; リクエストを受け取り、全てのwebコンテンツを
; 表示するページを生成する
(define (start request)
(render-blog-page BLOG request))

; parse-post: bindings -> post
; 束縛から投稿を抽出する
(define (parse-post bindings)
(make-post (extract-binding/single 'title bindings)
(extract-binding/single 'body bindings)))

; render-blog-page: blog request -> html-response
; ブログとリクエストを受け取り、ブログのコンテンツの
; html-responseを生成する
(define (render-blog-page a-blog request)
(local [(define (response-generator make-url)
`(html (head (title "My Blog"))
(body
(h1 "My Blog")
,(render-posts a-blog)
(form ((action
,(make-url insert-post-handler)))
(input ((name "title")))
(input ((name "body")))
(input ((type "submit")))))))

(define (insert-post-handler request)
(render-blog-page
(cons (parse-post (request-bindings request))
a-blog)
request))]

(send/suspend/dispatch response-generator)))

; render-post: post -> html-response
; 投稿を受け取り、投稿の要素のhtml-responseを生成する
(define (render-post a-post)
`(div ((class "post"))
,(post-title a-post)
(p ,(post-body a-post))))

; render-posts: blog -> html-response
; ブログを受け取り、全投稿の要素の
; html-responseを生成する
(define (render-posts a-blog)
`(div ((class "posts"))
,@(map render-post a-blog)))

render-blog-page機能の構造は、前のshow-counterの例と極めて似ているところに注目してください。最終的に、ユーザはブログに複数の投稿が可能になり、それを読む事が出来るようになりました。

残念ながら、まだ問題があります。問題を見るには次のようにしてみてください:システムにいくつか投稿して、新しいウィンドウでブラウザを開きます。新しいブラウザでwebアプリのURLを開きます。一体何が起きるでしょう?

2010年1月8日金曜日

Inspecting Requests

我々のアプリは、いまだちょっと静的に見えます。と言うのも、ページを動的に生成してはいるのですが、外部ユーザに新しい投稿を許すような設計にまだなっていないから、です。ここをやっつけてしまいましょう。ユーザに新しいブログエントリ用のフォームを提供しましょう。ユーザが実行ボタンを押すと、ページの頭に新しい投稿が現れるようにするのです。

今まではrequestオブジェクトで何かをする、と言う事を避けてきました。しかし、requestオブジェクトは避けるべきものではありません!ユーザがwebフォームに何か入力して投稿すると、ユーザのブラウザはフォームの値を保持した新しいrequestを生成します。ここでは、ユーザが入力した値を受け取るrequest-bindings機能を使います。request-bindingsの型は:

request-bindings : (request? . -> . bindings?)

です。
request-bindingsに加えて、名前を受け取り、その名前に関連した値を返すextract-binding/single機能と言うものもあります。

extract-binding/single : (symbol? bindings? . -> . string?)

最後に、exists-binging?で束縛に名前があるかどうかチェックします。

exists-binding? : (symbol? bindings? . -> . boolean?)

これらの機能を使って、requestを受け取り、何か役立つ事を行う機能を実装しましょう。

Exercise.bingings?を受け取るcan-parse-post?と言う機能を書いてみましょう。'title'bodyと二つのシンボルに束縛されたものがあれば#tを返し、そうじゃなければ#fを返します。


can-parse-post? : (bindings? . -> . boolean?)


Exercise.束縛を受け取るcan-parse-post?と言う機能を書いてみましょう。ここではでは'title'bodyと言うシンボルに値が束縛されている構造とします。parse-postはこれらの値を含む投稿を生成します。

parse-post : (bindings? . -> . post?)


さて、これらの補助機能を用いて、入力フォームを操れるようにwebアプリを拡張します。ページの最後に小さな入力フォームを付け加えて、新規投稿を加えられるようにプログラムを変更してみましょう。そして、startメソッドは、最初にリクエストがパース可能であるかどうか調べ、そうであるならば、投稿セットを拡大し、最終的にこれらのブログ投稿を表示するようにします。


#lang web-server/insta

; ブログは (listof post) で
; 投稿は (make-post title body)
(define-struct post (title body))

; BLOG: blog
; 静的ブログ
(define BLOG
(list (make-post "First Post" "This is my first post")
(make-post "Second Post" "This is another post")))

; start: request -> html-response
; リクエストを受け取り全てのwebコンテンツを
; 表示するページを生成する
(define (start request)
(local [(define a-blog
(cond [(can-parse-post? (request-bindings request))
(cons (parse-post (request-bindings request))
BLOG)]
[else
BLOG]))]
(render-blog-page a-blog request)))


; can-parse-post?: bindings -> boolean
; 束縛が 'title と 'body を含む値がある場合、真を返す
(define (can-parse-post? bindings)
(and (exists-binding? 'title bindings)
(exists-binding? 'body bindings)))


; parse-post: bindings -> post
; 束縛を受け取り、取り出した投稿を返す
(define (parse-post bindings)
(make-post (extract-binding/single 'title bindings)
(extract-binding/single 'body bindings)))

; render-blog-page: blog request -> html-response
; ブログをリクエストを受け取り、ブログコンテンツである
; html-response ページを生成する
(define (render-blog-page a-blog request)
`(html (head (title "My Blog"))
(body
(h1 "My Blog")
,(render-posts a-blog)
(form
(input ((name "title")))
(input ((name "body")))
(input ((type "submit")))))))



; render-post: post -> html-response
; 投稿を受け取り、html-response 要素を生成する
(define (render-post a-post)
`(div ((class "post"))
,(post-title a-post)
(p ,(post-body a-post))))


; render-posts: blog -> html-response
; ブログを受け取り、全投稿の html-response
; 要素を生成する
(define (render-posts a-blog)
`(div ((class "posts"))
,@(map render-post a-blog)))


これは動くように見えますが・・・一つ問題があります!二つの新規投稿をしてみてください。一体、何が起きるでしょうか?

2010年1月7日木曜日

Rendering HTML

webブラウザがアプリのURLを訪ねると、ブラウザはリクエスト構造を造って、webアプリにそれを送ります。手始めに、リクエストを受け取り、レスポンスを生成する機能を作りましょう。レスポンスの基本はHTMLページを表示する事です。

(define html-response/c
(or/c string?
(or/c (cons/c symbol? (listof html-response/c))
(cons/c symbol?
(cons/c (listof (list/c symbol? string?))
(listof html-response/c))))))

例:

"hello"はHTMLではhelloと表示されます。

<p>This is an example</p>は

'(p "This is an example")

で生成されます。

<a href="link.html">Past</a>は

'(a ((href "link.html")) "Past")

で生成されます。

<p>This is <div class="emph">another</div> example.</p>は

'(p "This is " (div ((class "emph")) "another") " example.")

で生成されます。

これらhtml-responseは直接conslistで作成できます。しかしながら、それでは厳しい表記となるでしょう。比較してみてください:

(list 'html (list 'head (list 'title "Some title"))
(list 'body (list 'p "This is a simple static page.")))


対:


'(html (head (title "Some title"))
(body (p "This is a simple static page.")))

両者とも同じhtml-responseを生成しますが、後者の方が記述も解読もはるかに簡単です。ここでは、How to Design Program:13章で解説された拡張リスト省略表記を使っています。先頭の引用符はリスト構造を表していて、これを使えば自信を持って静的なhtmlレスポンスを作る事が出来るのです。

しかしながら、動的コンテンツでは、単純なリスト省略表記を使うと問題が生じます。html-response構造に式を挿し入れたい場合、単純なリスト省略表記のアプローチは使えません。と言うのも、それらはリテラルなリスト構造の一部として扱われてしまうからです!

欲しいのは、構造の一部だけは普通の式として扱えるオプション付きの、クオートされたリスト省略記法の簡易性を備えた表記法、です。つまり、簡易表記可能な、動的に埋め込めるプレースホルダー付きのテンプレートを定義したいわけです。

Schemeはこのテンプレート機能を逆引用符として提供しています。逆引用とは、全体構造の直前にバッククオートを使う事です。普通のクオートされたリスト省略記法のように、殆どのリスト構造はネストされていてもリテラルに保護されます。一部の式の評価値を挿入したい場所で、その式の直前にクオート解除の為のコンマを挿し入れます。例えば:

; render-greeting: string -> html-response
; name を受け取り、動的 html-response を生成する。
(define (render-greeting a-name)
`(html (head (title "Welcome"))
(body (p ,(string-append "Hello " a-name)))))

Exercise.(listof post?)を受け取り、そのコンテンツのhtml-responseを生成する機能、render-postsを書いてください。

render-posts : ((listof post?) . -> . html-response/c)

例えば:

(render-posts empty)

は次を生成します:

'(div ((class "posts")))

一方、

(render-posts (list (make-post "Post 1" "Body 1")
(make-post "Post 2" "Body 2")))

は次を生成します:

'(div ((class "posts"))
(div ((class "post")) "Post 1" "Body 1")
(div ((class "post")) "Post 2" "Body 2"))



render-posts機能を入手したので、webアプリに戻ってhtml-responseを返すstart機能を実装しましょう。

#lang web-server/insta

; ブログは (listof post)
; 投稿は (make-post title body)
(define-struct post (title body))

; BLOG: blog
; 静的ブログ
(define BLOG
(list (make-post "First Post" "This is my first post")
(make-post "Second Post" "This is another post")))

; start: request -> html-response
; リクエストを受け取り、webコンテンツの全てを
; 表示するページを生成する
(define (start request)
(render-blog-page BLOG request))

; render-blog-page: blog request -> html-response
; ブログとリクエストを受け取り、ブログのコンテンツの
; html-response を生成する
(define (render-blog-page a-blog request)
`(html (head (title "My Blog"))
(body (h1 "My Blog")
,(render-posts a-blog))))

; render-post: post -> html-response
; 投稿を受け取り、投稿のhtml-response要素を生成する
(define (render-post a-post)
`(div ((class "post"))
,(post-title a-post)
(p ,(post-body a-post))))

; render-posts: blog -> html-response
; ブログを受け取り、全投稿のhtml-response要素を
; 生成する
(define (render-posts a-blog)
`(div ((class "posts"))
,@(map render-post a-blog)))

Runを押せば、webブラウザがブログの投稿を表示します。

Basic Blog

データ定義を考えるところからはじめましょう。投稿のリストを提供したい。投稿を次のように定義します。:

(define-struct post (title body))



(struct post (title body))
title : string?
body : string?


Exersise. 投稿の例をつくろう。

そうすると、ブログとは投稿のリストになります:

blog : (listof post?)

結果、もっとも単純なブログの例は以下のようになります:

(define BLOG (list (make-post "First Post!"
"Hey, this is my first post!")))

さて、サンプルブログの構造ができたので、webアプリとしてこれを表示しましょう。

The Application

この入門では、ブログを作り上げる事によって進んでいきたいと思います。
ユーザは投稿ができ、投稿にはコメントする事ができます。
ここでは対話的アプローチを採用し、道程にはいくつかの落とし穴を設けています。ゲームの方針は、大体において以下の通りです:

  • 静的な投稿を見せる。

  • ユーザにシステムへの新しい投稿を許す。

  • 投稿にコメントを加えられるようにモデルを拡張する。

  • 全てのユーザに同じ投稿セットを共有させる。

  • データ構造をディスク上にシリアライズする。



この入門の最後には、簡単なブログアプリを手にしている事でしょう。

Getting Started

この入門で必要な事全てはPLT Schemeで提供されています。DrScheme モジュール言語を使いましょう(注)。定義ウィンドウに次の入力をします。

#lang web-server/insta
(define (start request)
'(html
(head (title "My Blog"))
(body (h1 "Under construction"))))

Runボタンを押してください。Webブラウザが"Under Construction"ページで開いたら「最初のwebアプリを作ったんだ!」と手を叩いて喜びましょう。複雑な事はやってませんが、とにかく作ったのです。取りあえず今はStopボタンを押してサーバをシャットダウンしましょう。




注:Emacs + Quack + mzscheme の環境の場合は、トップに次の一文を入れます。

#!/usr/bin/env mzscheme

つまり、全体的なコードは次のようになります。

#!/usr/bin/env mzscheme

#lang web-server/insta
(define (start request)
'(html
(head (title "俺のブログ"))
(body (h1 "工事中"))))

ファイルを一旦保存し、Emacsのeshell上ででもchmod +xとして実行権限を与え、続いてeshell上で実行します。



inferior-schemeプロセスではブラウザは起動しないので気をつけて下さい。

Web Applications in PLT Scheme

どうやって動的Webアプリを作ろう?この入門では、PLT Schemeを使ってwebアプリを作る方法を紹介します。例として、簡単なWebジャーナル(ブログ)を作ってみましょう。Webサーバのスタートアップの仕方、動的Webコンテンツの作成法、ユーザとの対話の仕方もカバーします。

この入門の対象者はHow to Design Programsでストラクチャの使い方、デザインの仕方、高階関数、localの使い方、ミューテーションの考え方を学んだ人たちです。