HaskellのYesodでWebアプリ開発入門 (3)
前回の記事
HaskellでYesodを使ってWebアプリケーションを作ってみている際のメモ記事です。
ずいぶんと間が開いてしまいましたが、前回はディレクトリ構造のチェックとトップページ・コンタクトページへのルートの追加を行いました。
ウィジェット
今回は前回作ったトップページとコンタクトページをそれなりの見た目に整形していこうと思います。 ただ、DSLの構文を見る前に、Yesodでのビューの扱い方について少し知っておく必要がありました。 Yesodでは他のフレームワークとは少し違う、ウィジェットと呼ばれる概念を使ってページの構成を行なっています。
ウィジェットはページテンプレートで扱うコンテンツの柔軟性を向上し、テンプレートファイルを複数に分割したり、例外的なビューを作るたびにテンプレートファイル内に分岐を追加するようなことを防いでくれる、シンプルですが非常に有用な概念です。
ウィジェットではページのコンテンツを以下の7種類に分類して保持します。
- ページタイトル
- 外部スタイルシート
- 外部JavaScript
- スタイルシートファイル
- JavaScriptファイル
- HEADタグ内のコンテンツ
- BODYタグ内のコンテンツ
Hamletなどで作成したページコンテンツはtoWidget
関数を用いるだけでウィジェットに変換することができ、その際にちゃんとCassiusやLuciusのデータはSTYLEタグで囲まれ、JuliusのデータはSCRIPTタグで囲まれます。
toWidget
関数ではデフォルトで、CassiusやLuciusのデータはHEADタグ内のコンテンツ、HamletやJuliusのデータはBODYタグ内のコンテンツとしてウィジェットに変換しますが、代わりに以下の関数を用いることで、変換する種類を特定できます。
関数 | 変換する種類 |
---|---|
setTitle | ページタイトル |
addStylesheetRemote | 外部スタイルシート |
addScriptRemote | 外部JavaScript |
addStylesheet | スタイルシートファイル |
addScript | JavaScriptファイル |
toWidgetHead | HEADタグ内のコンテンツ |
toWidgetBody | Bodyタグ内のコンテンツ |
ちなみに、「外部〜」と「〜ファイル」は、指定するURLが外部の絶対URLであるかプロジェクト内のルートであるか、というだけの違いです。
それで、これをどう使うか、ですが、ここで前回さらっと流してしまったdefaultLayout
関数もう一度見てみます。
(コメントを省略しています)
defaultLayout widget = do master <- getYesod mmsg <- getMessage pc <- widgetToPageContent $ do $(combineStylesheets 'StaticR [ css_normalize_css , css_bootstrap_css ]) $(widgetFile "default-layout") withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
なお、この中のwidgetToPageContent
は大体以下の様な定義の関数です。
widgetToPageContent :: Widget -> Handler (PageContent url) data PageContent url = PageContent { pageTitle :: Html , pageHead :: HtmlUrl url , pageBody :: HtmlUrl url }
(Yesod Web Frameword Book - Using Widgetsより引用)
そして、長過ぎるので貼り付けることはしませんが、templates/default-layout-wrapper.hamlet
では<title>#{pageTitle pc}
, ^{pageHead pc}
, ^{pageBody pc}
という行があります。
つまり、defaultLayout
関数はwidgetToPageContent
関数に全てのウィジェットを生成するdo構文の式を適用して、ページのコンテンツとして埋め込める形にウィジェット達を整形してpc
という変数へ束縛し、その整形結果を埋め込むよう目論んだtemplates/default-layout-wrapper.hamlet
を変数pc
のスコープ内でwithUrlRenderer
関数に適用することで、その式が評価された時にはバラバラに追加されたウィジェット達がレイアウトの中に整然と並ぶよう仕向けているのです。
まだ上の7つの関数の使い所はわかりづらいので、defaultLayout
関数の呼び出し元も確認してみます。
前回書いたHandler/Home.hs
と、補足としてtemplates/default-layout.hamlet
も以下に貼り付けました。
{-# LANGUAGE OverloadedStrings #-} module Handler.Home where import Import getHomeR :: Handler Html getHomeR = defaultLayout $(widgetFile "home/index")
$maybe msg <- mmsg <div #message>#{msg} ^{widget}
defaultLayout
関数に適用されている$(widgetFile "home/index")
という式はdefaultLayout
関数内でwidget
という変数に束縛され、それがdefaultLayout
関数内のdo構文の中で展開されているtemplates/default-layout.hamlet
の中に埋め込まれています。
つまり、defaultLayout
関数の引数として先の関数達をつなげたdo構文を渡せば、それが最終的なHTMLにもうまく反映される、ということです。実際にHandler/Home.hs
とHandler/Contact.hs
を編集してみます。
{-# LANGUAGE OverloadedStrings #-} module Handler.Home where import Import getHomeR :: Handler Html getHomeR = defaultLayout $ do setTitle "Home | Yesod Sample" $(widgetFile "home/index")
{-# LANGUAGE OverloadedStrings #-} module Handler.Contact where import Import getContactR :: Handler Html getContactR = defaultLayout $ do setTitle "Contact | Yesod Sample" $(widgetFile "contact/index")
ちゃんとタイトルが付いたかと思います。
もちろんこれらのことはRoRなどでもできないことはありませんが、遅延評価とモナドの力がなければこれほど自然には書けず、多くの暗黙的ルールをドキュメントで学ばないと使いこなせない代物になってしまっていたのではないでしょうか。
Hamletの構文
YesodではHTMLのテンプレート言語としてHamletを用いているので、まずはその構文をについて知る必要があります。 細かいことはYesod Web Framework BookのShakespearean Templatesの項に書いありますが、Hamletの特徴としては大体以下のような点があるように思いました。
- インデントでタグのスコープを見るために閉じタグが要らない
- 型安全にHaskellの値を埋め込むことができる
- 呼び出されたスコープを引き継ぐ(引数で渡したりすることなく、束縛されている値や関数を参照できる)
- 手続き的なディレクティブで分岐やループ、パターンマッチを扱える
1つめはそのままですが、2つめの埋め込める値は文字列や数字だけでなく、ToHtml
クラスのインスタンスとなっている全ての型の値となります。
ToHtml
クラスの型はtoHtml
関数とfromHtml
関数でHtml
型と行き来できる必要があります。
また、3つめはこれまで見てきたコードからも分かる通り、テンプレートファイル内のスコープはそのテンプレートが展開される関数のスコープに属することを表します。 RoRでは呼び出したコントローラを表現するクラスがスコープとなっていたので、クラス変数の汚染があり、特定のアクションのための変数を他のアクションから分離できませんでした。 それを、そもそもHaskellではそういったスコープが存在しないので、普通にしていると引数を使う必要が出てきますが、TemplateHaskellでHamletファイルを評価する関数を呼び出したスコープのクロージャにすることでいいとこ取りをした、ということですかね。
4つめが非常に煩雑なのですが、Hamletでは分岐やループ、パターンマッチといった構文を必要とする動的なコンテンツの挿入に$
記号から始まる特殊なディレクティブを用います。詳細についてはYesod Web Framework BookのHamlet Syntaxの項に網羅されているので、ここでは今後出てきた都度に紹介する形にしようかと思います。
また、HamletではHTMLをそのまま書くよりもIDやクラス、その他の属性値を簡単に設定できるようになっていますが、これらも今後出てきた際に都度紹介します。
レイアウトファイルの移動
さて、説明が長くなってしまいましたが、Hamletを用いたコーディングの方法がだいたいわかったので、主にレイアウトファイルをいじって、前回作成したトップページとコンタクトページをまあまあまともなページにしてみたいと思います。
まず、ディレクトリ階層が浅いと後々不便になるかと思うので、レイアウト用のHamletファイルをlayout
ディレクトリ以下に移動し、名前も分かりやすいものに変更します。
$ cd templates $ mkdir layout $ mv default-layout.hamlet layout/application.hamlet $ mv default-layout-wrapper.hamlet layout/wrapper.hamlet
この変更にともなって、Foundation.hs
の中のdefaultLayout
関数で指定しているファイル名を変更する必要があります。
pc <- widgetToPageContent $ do $(combineStylesheets 'StaticR [ css_normalize_css , css_bootstrap_css ]) $(widgetFile "layout/application") giveUrlRenderer $(hamletFile "templates/layout/wrapper.hamlet")
ところで、ファイルを編集するとyesod devel
のプロセスがカタカタと動いてリビルドした上でエラーがあったら提示してくれるというのは、RoRでGuardを使ってテスト駆動開発をしている時と同じくらいの快適さと安心感ですね。
まだテストコードは1行も書いていないというのに...。こんなんだったら、ユニットテストなどほとんど必要なくなるように思えます。
Normalize.cssのバージョンアップとBootstrap.jsの不要な箇所の削除
次に、ずっと放置していましたがこの際なので、Normalize.cssを最新版にバージョンアップします。ついでにBootstrap.jsもバージョンアップしようかと思いましたが、個人的にこれが入るとCSSが格段に書きづらくなるため、ひとまずBootstrap.jsは便利なGlyphicon部分だけ残して、後は削除してしまうこととしました。本記事を描いた時点でNormalize.cssはバージョン3.0.1、Bootstrap.jsはバージョン3.2.0でした。
単に各サイトからファイルをダウンロードして、Bootstrap.jsはコピーライト表示とGlyphiconに関する部分だけを抜き出したファイルを作成し、必要なファイルをstatic
ディレクトリ以下に配置します。
筆者のstatic
ディレクトリ内は以下の様になりました。
static ├── combined │ └── rMJ_CwK2.css ├── css │ ├── glyphicons.css │ └── normalize.css ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ └── glyphicons-halflings-regular.woff └── tmp ├── autogen-SWBGXCgb.js └── autogen-vozS04Wr.css 3 directories, 5 files
これに伴ってFoundation.hs
のdefaultLayout
関数の中身を再び多少変更します。
pc <- widgetToPageContent $ do $(combineStylesheets 'StaticR [ css_normalize_css , css_glyphicons_css ]) $(widgetFile "layout/application") giveUrlRenderer $(hamletFile "templates/layout/wrapper.hamlet")
このcombineStylesheets
およびcombineScripts
関数はSettings/StaticFiles.hs
で定義されています。
-- | This generates easy references to files in the static directory at compile time, -- giving you compile-time verification that referenced files exist. -- Warning: any files added to your static directory during run-time can't be -- accessed this way. You'll have to use their FilePath or URL to access them. $(staticFiles Settings.staticDir) combineSettings :: CombineSettings combineSettings = def -- The following two functions can be used to combine multiple CSS or JS files -- at compile time to decrease the number of http requests. -- Sample usage (inside a Widget): -- -- > $(combineStylesheets 'StaticR [style1_css, style2_css]) combineStylesheets :: Name -> [Route Static] -> Q Exp combineStylesheets = combineStylesheets' development combineSettings combineScripts :: Name -> [Route Static] -> Q Exp combineScripts = combineScripts' development combineSettings
また、その上のコメントに書かれている通り、css_normalize_css
やcss_glyiphicon_css
はSettings/StaticFiles.hs
をコンパイルした際に定義される、静的ファイルのパスを表現する変数です。
yesod devel
のプロセスを起動しっぱなしにしている場合、static
ディレクトリ以下の変更だけではSettings/StaticFiles.hs
はリビルドされないため、Vimでファイルを開いて:w
で保存するなどしないと、上の変更はエラーになります。
なお、確実にリビルドさせるにはcabal build
コマンドをプロジェクトルートで走らせます。
おわりに
HamletファイルとCassiusファイルを編集してそこそこ使い物になりそうなレイアウトを作るところまで行くつもりでしたが、思いの外説明が長くなってしまったのでここで一度記事を切ろうかと思います。 次回はレイアウトをして、静的なWebページとして公開できるくらいまでアプリケーションの完成度を高めます。