読者です 読者をやめる 読者になる 読者になる

HaskellのYesodでWebアプリ開発入門 (3)

Yesod Haskell Web

前回の記事

HaskellでYesodを使ってWebアプリケーションを作ってみている際のメモ記事です。

ずいぶんと間が開いてしまいましたが、前回はディレクトリ構造のチェックとトップページ・コンタクトページへのルートの追加を行いました。

ウィジェット

今回は前回作ったトップページとコンタクトページをそれなりの見た目に整形していこうと思います。 ただ、DSLの構文を見る前に、Yesodでのビューの扱い方について少し知っておく必要がありました。 Yesodでは他のフレームワークとは少し違う、ウィジェットと呼ばれる概念を使ってページの構成を行なっています。

ウィジェットはページテンプレートで扱うコンテンツの柔軟性を向上し、テンプレートファイルを複数に分割したり、例外的なビューを作るたびにテンプレートファイル内に分岐を追加するようなことを防いでくれる、シンプルですが非常に有用な概念です。

ウィジェットではページのコンテンツを以下の7種類に分類して保持します。

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.hsHandler/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の特徴としては大体以下のような点があるように思いました。

  1. インデントでタグのスコープを見るために閉じタグが要らない
  2. 型安全にHaskellの値を埋め込むことができる
  3. 呼び出されたスコープを引き継ぐ(引数で渡したりすることなく、束縛されている値や関数を参照できる)
  4. 手続き的なディレクティブで分岐やループ、パターンマッチを扱える

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.hsdefaultLayout関数の中身を再び多少変更します。

        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_csscss_glyiphicon_cssSettings/StaticFiles.hsコンパイルした際に定義される、静的ファイルのパスを表現する変数です。 yesod develのプロセスを起動しっぱなしにしている場合、staticディレクトリ以下の変更だけではSettings/StaticFiles.hsはリビルドされないため、Vimでファイルを開いて:wで保存するなどしないと、上の変更はエラーになります。

なお、確実にリビルドさせるにはcabal buildコマンドをプロジェクトルートで走らせます。

おわりに

HamletファイルとCassiusファイルを編集してそこそこ使い物になりそうなレイアウトを作るところまで行くつもりでしたが、思いの外説明が長くなってしまったのでここで一度記事を切ろうかと思います。 次回はレイアウトをして、静的なWebページとして公開できるくらいまでアプリケーションの完成度を高めます。