HaskellのYesodでWebアプリ開発入門 (2)
前回の記事
HaskellでYesodを使ってWebアプリケーションを作ってみている際のメモ記事です。
前回はインストールと初回起動までやりました。
Yesodのディレクトリ構造
前回は何も考えずに起動しただけでしたので、次はお決まりの流れでScaffoldのディレクトリ構造の確認をしたいと思います。
現在プロジェクトディレクトリに含まれるファイルは以下の様な感じですね。Homebrewでインストールしたtree
コマンドの結果からdist
ディレクトリを省略したものです。
sample ├── Application.hs ├── Foundation.hs ├── Handler │ └── Home.hs ├── Import.hs ├── Model.hs ├── Settings │ ├── Development.hs │ └── StaticFiles.hs ├── Settings.hs ├── app │ ├── DevelMain.hs │ └── main.hs ├── config │ ├── client_session_key.aes │ ├── favicon.ico │ ├── keter.yaml │ ├── models │ ├── mysql.yml │ ├── postgresql.yml │ ├── robots.txt │ ├── routes │ └── settings.yml ├── deploy │ └── Procfile ├── devel.hs ├── dist │ ├─ ... │ ... ├── messages │ └── en.msg ├── static │ ├── combined │ │ └── rMJ_CwK2.css │ ├── css │ │ ├── bootstrap.css │ │ └── normalize.css │ └── img │ ├── glyphicons-halflings-white.png │ └── glyphicons-halflings.png ├── templates │ ├── default-layout-wrapper.hamlet │ ├── default-layout.hamlet │ ├── homepage.hamlet │ ├── homepage.julius │ └── homepage.lucius ├── tests │ ├── HomeTest.hs │ ├── TestImport.hs │ └── main.hs └── yesod-devel │ ├── arargs.txt │ └── ghcargs.txt └── yesod-sample.cabal 23 directories, 148 files
RoRをある程度触ったことのある人ならなんとなく想像がつくかと思いますが、ざっくりと分かる範囲でファイルの役割を書き出してみます。 ファイルの中身を見ながら読んでいただけるとなんとなくわかっていただけるかと。
(以下の表には想像も含まれていますので、正しいことが分かり次第更新します)
ディレクトリやファイル | 役割 |
---|---|
Application.hs | app/main.hsから呼び出されるアプリケーションの本体を作る関数makeApplication が定義されている |
Foundation.hs | Application.hsでインスタンスが作成されるアプリケーションの本体が定義されている |
Handler/ | いわゆるコントローラの働きをするHandlerの定義ファイルが置かれるディレクトリ。中のファイルの詳細は後で書くので省略 |
Import.hs | プロジェクト内のすべてのモジュールで読み込まれるYesodなどのモジュールをimportしたモジュール |
Model.hs, config/models | O/R mapperであるPersistentで使用されるモデル関連ファイル |
Settings/Development.hs | Development環境用を区別するための関数が定義されている |
Settings/StaticFiles.hs | 静的ファイルの読み出しのための関数を定義したファイル |
Settings.hs | いろいろな設定が書かれているファイル。静的ファイルのURL設定やHTMLファイルなどの構文解析に関する情報が書かれている |
app/DevelMain.hs | ghciでアプリケーションの環境を使うためのメイン・エントリ・ポイント |
app/main.hs | 通常のアプリケーションのメイン・エントリ・ポイント |
config/client_session_key.aes | コンパイル時に自動生成されるユーザのクッキー情報を暗号化するためのファイル |
config/favicon.ico, config/robots.txt | 普通のファビコンやらrobots.txtやら |
config/keter.yaml, deploy/Procfile | デプロイに関するファイル。後者にはHerokuへデプロイするための情報が書かれている |
config/mysql.yml, config/postgresql.yml | 前回も紹介したデータベース設定に関するファイル |
config/routes | URLとコントローラの対応関係を記述するファイル |
config/settings.yml | ルートURLやポートなど、アプリケーション全体に関する情報が記述されたファイル |
dist/build/sample/sample | アプリケーションの実行ファイル。本番環境ではこの実行ファイルのプロセスからデーモンプロセスを作る |
devel.hs | デバッグ環境のアプリケーションのメイン・エントリ・ポイント |
massages/ | i18n(国際化対応)のための辞書ファイルを置くディレクトリ |
sample.cabal | 前回紹介したcabalのための設定ファイル |
static/ | CSS・JavaScriptライブラリや画像などの静的ファイルを置くディレクトリ。デフォルトではBootstrapのバージョン2.3.2(古い!)のCSSと画像ファイル、Normallize.cssのバージョン2.1.2(古い!)とそれらCSSをつなげたCSSファイルが含まれている |
templates | 動的な部分を含む(テンプレートを利用する)HTMLやJavaScript、CSSファイルを置くディレクトリ。Yesodでは標準でHTMLにHamlet、JavaScriptにJulius、CSSにLuciusかCassiusという言語を使うようになっている |
tests/ | テストコードを置くディレクトリ。Ruby並に自然な形でテストコードが書かれていてワクワクする |
yesod-devel/ | デバッグ環境で実行する際の設定を記したファイル、かな |
思っていたよりも見通しはスッキリ。 Haskellの構文の柔軟さのお陰で全体的にファイルの可読性が高く、性能を追求したりし始めるまでは案外簡単に開発出来てしまうのでは? という期待が膨らみます。
ghci
表のDevelMain.hs
の説明にちょろっと書きましたが、Yesodではアプリケーション実行時と同じ環境でghciを利用するための設定が追加されています。
DevelMain.hs
の冒頭に説明がありますが、ghciを
$ cabal repl --ghc-options="-O0 -fobject-code"
として実行すると、アプリケーション使用時にロードされるパッケージを全てロードした状態でHaskellの対話環境を使えます。
とてつもなく便利ですね。
TemplateHaskell
ファイルを見ていくと、少ししかHaskellを学んだことがない人は「おややっ??」となったのではないでしょうか。
きっとこれを編集すればいいんじゃないかな、というHandler/Home.hs
にも以下の様な見慣れない(というかjQueryみたいな)式があります。
$(widgetFile "homepage")
これはTemplateHaskellというHaskellにおけるマクロのようなもので、こいつを使うとLispのquoteとeval, funcallでやるようなことが書けてしまうというすぐれものです。
widgetFile
はYesodの関数ではなく、Scaffoldで生成されたSettings.hs
の中に含まれており、型を確認することができます。
widgetFile :: String -> Q Exp widgetFile = (if development then widgetFileReload else widgetFileNoReload) widgetFileSettings
つまり、先の式はwidgetFile
関数が"homepage"
というString
を元にQuoteモナド(Lispのquoteされた式みたいなもの)をつくり、それを$()
で評価した結果を表していたわけですね。
Yesod公式のScaffoldに関するエントリによると、widgetFile
はtemplates
ディレクトリ以下にある適用した文字列を名前として持つHaml, Julius, Lucius, Cassiusファイルをパースして1つのウィジェット(アプリケーション内でのレスポンスの表現?)にまとめてくれる関数のようです。
この関数はFoundation.hs
内のApp
インスタンスの定義にあるdefaultLayout
関数でも使われています。
defaultLayout widget = do master <- getYesod mmsg <- getMessage -- We break up the default layout into two components: -- default-layout is the contents of the body tag, and -- default-layout-wrapper is the entire page. Since the final -- value passed to hamletToRepHtml cannot be a widget, this allows -- you to use normal widget features in default-layout. pc <- widgetToPageContent $ do $(combineStylesheets 'StaticR [ css_normalize_css , css_bootstrap_css ]) $(widgetFile "default-layout") giveUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
コメントにも書かれていますが、templates/default-layout-wrapper.hamlet
にページ全体のレイアウト、templates/default-layout.hamlet
にBODYのレイアウトが記述されているようです。
ここまで見てから再度Handler/Home.hs
のgetHomeR
関数の全体を見ると
getHomeR :: Handler Html getHomeR = do (formWidget, formEnctype) <- generateFormPost sampleForm let submission = Nothing :: Maybe (FileInfo, Text) handlerName = "getHomeR" :: Text defaultLayout $ do aDomId <- newIdent setTitle "Welcome To Yesod!" $(widgetFile "homepage")
ざっくり、default-layout-wrapper.hamlet
, default-layout.hamlet
, homepage.hamlet
, homepage.julius
, homepage.lucius
をくっつけて、widgetFile
関数を呼ぶまでに用意したformWidget
やらformEnctype
やらsubmission
やらやらが展開されて、レスポンスとして返されてるんだろうなあ、というくらいは分かるようになりました。
それではいよいよ、ここらへんをいじって静的ファイルを自分なりに作り替えてみたいと思います。
ちなみに、Template Haskellの簡単な説明ならruiccさんの記事がわかりやすかったです。
プロジェクトのGit管理
ところで、プロジェクトの生成の際に.gitignore
ファイルも一緒に作成されるので、編集を始める前にプロジェクトのディレクトリをGitで管理しておくと、この後にScaffoldのコマンドで行なった変更点をgit diff
で見れたり、簡単に変更をやめたりできるので便利です。
ただ、もしプロジェクトをオープンソースで管理したりするのであれば、パスワードが含まれるconfig/mysql.yml
やconfig/postgresql.yml
はデフォルトでは.gitignore
に含まれていないので追加して、本番環境へデプロイする際に別個で送信するべきかと思います。
(Capistranoを使うのはなんだかちょっとなあ、っていう感じなんですけどね...)
Vimのシンタックスハイライト
編集を始める前に準備をもう一点。 テンプレート用の言語達をハイライトしてくれるVimプラグインをインストールしておきます。
幸い、pbrisbinさんのhtml-template-syntaxというレポジトリのプラグインを追加すれば、簡単にテンプレートファイル達をシンタックスハイライトすることができました。
Warningの修正
yesod init
でダウンロードされるscaffoldではYesodの最新版に対応していないらしく、最初からいくつかのWarningがでてしまっているので、それを修正します。
まずはModel.hs
内でimport Prelude
と書かれていますが、Preludeはデフォルトでimportされるので消去します。
次に、Foundation.hs
内で使用されているgiveUrlRenderer
がdeprecatedとなっているので、代わりにwithUrlRenderer
を使用します。
これで鬱陶しいWarningが消えたはずです。 なんで修正されないんだろう...
テンプレートファイルの書き換え
まずは単に、Scaffoldで用意されているHomeハンドラを編集して、Webのトップページらしくしてみます。
せっかく作ってくれた(といってもGithubからコピーしてきているだけだが)Homeハンドラやらやらなので少し躊躇しましたが、サンプルプログラムもそんなに面白く無いのでスパッと全てなくしちゃいます。
Homeハンドラに関する記述があるのはconfig/routes
, Handler/Home.hs
, templates/homepage.*
, sample.cabal
, Application.hs
の5箇所のファイルですが、後者2つはimportなどの宣言を行っているだけなので、前の3箇所だけを変更します。
ひとまず、config/routes
とHandler/Home.hs
は以下のように編集しました。
/static StaticR Static getStatic /auth AuthR Auth getAuth /favicon.ico FaviconR GET /robots.txt RobotsR GET / HomeR GET
{-# LANGUAGE OverloadedStrings #-} module Handler.Home where import Import getHomeR :: Handler Html getHomeR = defaultLayout $(widgetFile "home/index")
とりあえず表示するためにtemplates/home/index.hamlet
という名前でHello, World!
だけのファイルをつくります。
<h1>Hello, World!
これらの変更を行ってからyesod devel
を叩けばlocalhost:3000でHello, World!の文字が見られると思います。
ルーティングはRoRみたいにStaticPagesとかいう名前のハンドラを作ってその中のメソッドでURLによってGETの結果を変える、という風にしようかとも考えましたが、それは別にRESTでもなんでもないし、widgetFile
関数でうまくテンプレートファイル達をくっつけてくれることを考えたらトップページと概要ページ、コンタクトページとかで1つづつディレクトリを作るほうがいいように思えたのでこうしてみました。
よくよく考えて見れば、よくやるHTMLはviewとかのディレクトリに置いてCSSとJavaScriptはassetsとかに置くという区別は、DSLを使うなら同じような階層構造を持つディレクトリが増殖するだけであまり意味ない気がするのですが、どうなんでしょうかね。VimでHTMLとCSSを並べて開きたい時とかも面倒なだけだし、Assets precompileするとしても拡張子で判断がつく上にむしろwidgetFile
の形式のほうが依存関係はわかりやすいし...
ちなみに、サンプルプログラムのHandlerの一番上で宣言されていたTupleSections
というエクステンションは(, True)
を\x -> (x, True)
として解釈してくれるようなものらしいです。これはsampleForm
関数で使用していただけなので消しても問題ありませんが、OverloadedStrings
はダブルクオーテーションの文字列をByteString
として扱ってくれ、性能改善になるので一応残しておきました。
Handlerの追加
早くテンプレートファイル達をいじってHamletやらやらと戯れたいところですが、さすがに見れるファイルが1つだけだとヘッダも作りようがないので、コンタクトページだけ追加しておきます。
Handlerを追加するには、Handler
以下にモジュールを作るだけでなく、sample.cabal
の依存関係に追加したモジュールを加えて、Application.hs
で追加したモジュールをインポートした上で、config/route
でルーティングの設定をする必要があります。地味に面倒臭いですね。
というわけで、Scaffoldを使いました。
$ yesod add-handler Name of route (without trailing R): Contact Enter route pattern (ex: /entry/#EntryId): /contact Enter space-separated list of methods (ex: GET POST): GET $
作成したHandlerを見ると
module Handler.Contact where import Import getContactR :: Handler Html getContactR = error "Not yet implemented: getContactR"
と、ただエラーを投げる関数になっていますので、これをHomeハンドラと同じように編集し、とりあえずのHamletファイルも追加しておきます。
{-# LANGUAGE OverloadedStrings #-} module Handler.Contact where import Import getContactR :: Handler Html getContactR = defaultLayout $(widgetFile "contact/index")
templates/contact/index.hamlet
<h1>Contact
Homeの変更からデバッグ環境のサーバを起動したままでいれば、変更が勝手に反映されてlocalhost:3000/contactでコンタクトページを表示できます。
Play! frameworkのActivatorなんかよりよっぽど高速でいいですね。
おわりに
だんだんそのユーザとしてはYesodの全体像が見えてきて楽しくなってきました。
本当はテンプレートファイルを綺麗に編集するところまで書こうと思っていたのですが、無意味に長くなってきてしまったのでそれらは次回に回します。