You don't have javascript enabled. Good luck! :(

Scaffolding とサイトテンプレート

最終更新日: 2019/03/01

あなたは、これまで動かしてきたような小さなサンプルプログラムではなく、本物のサイトを書いてみたいと思っているのではないでしょうか?この章はそんな読者のための章です。Yesod ライブラリの隅々まで完全に理解していたとしても、プロダクションレベルのサイトをセットアップするためには、まだまだ多くの通過すべきステップがあります。

  • 設定ファイルのパージング
  • シグナル制御 (*nix)
  • 静的ファイルの効率的な配信
  • 良いファイルレイアウト

scaffolded サイトは多くの Yesod ユーザのベストプラクティスの組み合わせを、すぐに使えるサイトの骨組みに統合したものです。どんなサイトでも scaffolded サイトの利用を強く推奨します。この章では scaffolding の全体構造、使い方、少しだけわかりづらい機能等について説明します。

多くの部分で、この章はサンプルコードを含んでいないでしょう。そのため、実際の scaffolded サイトに沿って進めることが推奨されます。

scaffolded サイトという性質上、これは Yesod の最も流動的なコンポーネントです。そのため、バージョン毎に変化することもあり、そういった理由でこの章の情報は古くなっている可能性があります。

どのように scaffold するか

yesod-bin パッケージは (利便性のため yesod という名前の) 実行ファイルをインストールします。この実行ファイルはいくつかのコマンドを提供しています (コマンドの一覧は stack exec -- yesod --help で確認できます)。 scaffolding を生成するためのコマンドは stack new my-project yesod-postgres です。このコマンドによって my-project という名前のディレクトリに postgres データベースバックエンドの scaffolding サイトを生成します。また、利用可能なテンプレートの一覧については stack templates コマンドで確認できます。

(stack templates コマンドで取得可能な) 様々な利用可能なテンプレートとの一番の違いは、データベースバックエンドです。例えば、SQL や MongoDB バックエンドを持つものや、データベースを含まない “yesod-simple” テンプレートなどの選択肢があります。yesod-simple テンプレートは余分な依存関係を持たないため、最小限の構成でサイト全体を作ることができます。この章の残りでは、データベースバックエンドを持つ scaffolding サイトに焦点を当てますが、yesod-simple バックエンドとはわずかな違いしかありません。

サイトを作った後は stack install yesod-bin --install-ghc コマンドでプロジェクト内にコマンドラインツールをインストールしましょう。そして、ディレクトリ内で stack build を実行します。このコマンドによって、足りない依存関係がビルド/インストールされます。

最後に stack exec -- yesod devel コマンドで開発用サイトを起動しましょう。開発用サイトでは、コードの変更を検知して自動的にファイルのリロード/サイトのリビルドを行います。

ファイル構造

scaffolded サイトは完全に cabal 化された Haskell パッケージとしてビルドされます。ソースファイルの他に、設定ファイル、テンプレート、静的ファイルが生成されます。

Cabal ファイル

直接 stack を使う場合でも、間接的に stack exec -- yesod devel とする場合でも、コードをビルドする際には常に cabal ファイルを経由します。cabal ファイルを開けば、ライブラリと実行ファイルの両方のブロックが存在することが分かるはずです。もし、 library-only フラグが有効であれば、実行ファイルのブロックはビルドされません。これは yesod devel がアプリケーションを呼び出す方法です。それ以外では、実行ファイルブロックもビルドされます。

library-only フラグは yesod devel コマンドでだけ使うべきです。絶対に cabal ファイルに明示的に書き込むべきではありません。他にもフラグがあります。dev は cabal ファイルが実行ファイルのビルドを許可しますが、同じ機能を library-only フラグで行っています。例えば、最適化無しや、Shakespearen テンプレート関数のリロード版などです。

一般的に、次のようにビルドを行います。

  • 開発の際は、基本的に yesod devel を使いましょう
  • プロダクションビルドを行う場合は stack clean && stack build を実行します。これにより最適化された実行ファイルが dist フォルダに生成されます (yesod keter コマンドもまた利用可能です)

NoImplicitPrelude 拡張に驚くかもしれません。サイトは自分のモジュールに Import モジュールを含みます。これは Prelude に多少の変更を行って Yesod を少しだけ便利に使えるようにしたものです。そういった理由で NoImplicitPrelude 拡張を有効にしています。

気をつける最後の点としては exposed-module リストです。もし、アプリケーションに何かモジュールを追加する場合はこのリストを必ず更新し yesod devel が正しく機能するようにしなけれなりません。残念なことに、この更新作業を忘れたとしても Cabal や GHC は警告を出さずに、代わりに yesod devel から非常にわかりにくいエラーメッセージが表示されます。

ルートとエンティティ

本書で何度か “利便性のため routes/entities を準クォートで宣言します。プロダクションサイトでは外部ファイルを利用してください。” というようなコメントを目にしてきたでしょう。scaffolding では、ちゃんと外部ファイルを使います。

ルートは config/routes エンティティは config/models で定義されます。ファイルの内容は本書で既に説明した準クォートと全く同じ構文になっています。また、yesod devel はこれらのファイルが変更された際に、自動的に適切なモジュールの再コンパイルを行います。

models ファイルは Model.hs から参照されます。このファイルには何でも好きなように宣言することができますが、いくつかのガイドラインがあります。

  • エンティティで使われるデータ型は persistFileWith の呼び出しの前に Model.hs でインポート、または宣言されている必要があります。
  • 補助的なユーティリティは Import.hs で宣言するか、もしモデルにとても関連が深いものであれば Model フォルダ内のファイルに宣言し、Import.hs からインポートしてください。

ファウンデーションとアプリケーションモジュール

本書で利用してきた mkYesod 関数はいくつかの宣言を行います。

  • ルート型
  • ルートレンダリング関数
  • ディスパッチ関数

ディスパッチ関数は全てのハンドラ関数を参照します。そのため、それら全てのハンドラ関数はディスパッチ関数と同じファイルに定義するか、ディスパッチ関数を含むモジュールからインポートされる必要があります。

一方で、ハンドラ関数はほぼ確実にルート型を参照します。なので、それらはルート型が定義されるファイルと同じファイルに定義するか、そのファイルをインポートする必要があります。もし、このロジックに従うのであれば、アプリケーション全体が本質的に1つのファイルに存在することになってしまいます!

明らかにこの方法はやりたくありません。よって mkYesod の代わりに scaffolding サイトは mkYesod の機能を分離したバージョンを利用します。Foundation.hs は, mkYesodData を呼び出し、それはルート型とレンダリング関数を定義します。この関数はディスパッチ関数を宣言しないため、ハンドラ関数をスコープに入れておく必要はありません。Import.hsFoundaton.hs をインポートし、全てのハンドラモジュールは Import.hs をインポートします。

Applications.hs では mkYesodDispatch を呼び出してディスパッチ関数を生成します。これが機能するためには、全てのハンドラ関数がスコープになければならないので、新たに生成する全てのハンドラモジュールのインポート文を忘れずに追加してください。

それ以外では Application.hs は非常に単純です。2つの主要な関数があるだけです。getApplicationDev はアプリケーションを起動するために yesod devel で使われ、makeApplication は実行ファイルを起動するために利用されます。

Foundation.hs はもっと面白いことを行います。

  • ファウンデーションデータ型を宣言します
  • YesodYesodAuthYesodPersist のようないくつものインスタンスを宣言します
  • メッセージファイルをインポートします。mkMessage で始まる行を探すと、メッセージを含むフォルダ (message/) と、デフォルト言語 (英語は en) を指定しているということがわかるでしょう

また Foundation.hs はアプリケーションのファウンデーションに YesodAuthEmailYesodBreadcrumbs のような追加インスタンスを宣言ための正しい場所です。

Yesod 型クラスのメソッドの特別な実装について議論する際に、またこのファイルを見直すでしょう。

Import

Import モジュールはいくつかの共通の繰り返されるパターンから生まれました。

  • あらゆるハンドラで用いられる補助関数 (<> = mappend 演算子のような) を定義したい
  • 常に同じ5つのインポート宣言 (Data.TextControl.Applicative、etc) を各ハンドラモジュールに追加したい
  • Prelude から出ている悪魔のような関数 (headreadFile、…) を絶対に使わないようにしたい

そうですね、悪魔という言葉は言い過ぎかもしれません。これらの関数がなぜ良くないのか説明すると、head は部分関数なので空リストに対し例外を投げます。readFile は 遅延I/O なので、ファイルハンドルがすぐにクローズされません。さらに、readFileText ではなく String を使います。

この問題を解決するためには NoInplicitPrelude 言語拡張を有効にして、Prelude の必要な部分を再エクスポートし、他に必要なものを全て追加します。そして、同様に独自の関数を定義し、このファイルをすべてのハンドラでインポートします。

この章が出版されたあとで scaffolded サイトが classy-prelude-yesod のような、別の prelude に移行する可能性があります。Import がここで定義されたようなものとかなり違ったものに見えても、驚かないでください。

ハンドラモジュール

ハンドラモジュールは Handler フォルダ内に入れるべきです。サイトテンプレートは Handler/Home.hs というモジュールを1つ含みます。ハンドラ関数をどのように個々に分割するかは自由ですが、経験的には以下に従うと良いでしょう。

  • 同じルートの異なるメソッドは同じファイルに定義すべきです。例えば getBlogRpostBlogR など。
  • 関連するルートは通常同じファイルに定義します。例えば getPeopleRgetPersonR など。

もちろん、この規則に従うかどうかはあなた次第です。新しいハンドラファイルを追加するときは必ず以下の作業を行います。

  • バージョンコントロールに追加します (バージョンコントロールは使っていますよね)。
  • cabal ファイルに追加します。
  • Application.hs ファイルに追加します。
  • ファイルの先頭でモジュール宣言を行い、import Import という行をその下に追加します。

最後の3つのステップを自動化するために stack exec -- yesod add-handlerコマンドを利用することもできます。

widgetFile

ページ特有の CSS や Javascript を含めたいと思うことは非常に一般的です。ただし、Hamlet ファイルを参照する度に対応する Lucius や Julius ファイルの管理を自分で行いたくは無いでしょう。そのため、サイトテンプレートは widgetFile 関数を提供しています。

次のようなハンドラ関数があるとします。

getHomeR = defaultLayout $(widgetFile "homepage")

Yesod は以下のファイルを探します。

  • templates/homepage.hamlet
  • templates/homepage.lucius
  • templates/homepage.cassius
  • templates/homepage.julius

もしこれらのファイルが見つかれば、ファイルは自動的に出力に含まれます。

このような動作になっているため、もしアプリケーションを yesod devel で起動し、その後に、新しいファイル (例えば templates/homepage.julius) を作成した場合、widgetFile を呼んでいるファイルが再コンパイルされるまで、このファイルは含まれません。このような場合 yesod devel を再コンパイルさせるために、そのファイルを強制的に保存する必要があります。

defautLayout

まず最初にカスタマイズしたもいものの1つはサイトの見栄えです。レイアウトは2つのファイルに分割されます。

  • templates/default-layout-wrapper.hamlet はページの基本的なラッパーです。ファイルはウィジェットではなくプレーンな Hamlet として解釈されます。そのため、他のウィジェットの参照、i18n 文字列の埋め込み、外部 CSS/JS の追加等はできません。
  • templates/default-layout.hamlet は、ページの大部分を記述する場所です。widget の値を忘れずにページに含めてください。なぜなら、widget の値はページ毎のコンテンツを含むためです。このファイルはウィジェットとして解釈されます。

また、default-layout は widgetFile 関数で処理されるので、default-layout.* という名前の Lucius、Cassius、Julius ファイルも自動的に含まれます。

静的ファイル

scaffolded サイトは自動的に静的ファイルサブサイトを含みます。現在のビルド中に更新されないファイルを配信するために最適化されています。これは、次のことを意味します。

  • 静的ファイルの識別子が生成されると (例えば static/mylogo.pngmylogo_png になります)、クエリ文字列パラメータとしてファイルコンテンツのハッシュ値が追加されます。これらは全てコンパイル時に起こります。
  • yesod-static が静的ファイルを配信する時、遠い将来の expiration ヘッダをセットし、コンテンツのハッシュに応じた etag を含めます。
  • mylogo_png へのリンクを埋め込む際は常に、レンダリングはクエリ文字列パラメータを含みます。そのため、ロゴを変更し、再コンパイルしてから新しいアプリケーションを起動すれば、クエリ文字列の変化により、ユーザはキャッシュされたコピーを無視して新しいバージョンをダウンロードします。

さらに、Settings.hs ファイルに特定の静的ルートを設定することで、異なるドメイン名から配信できるようになります。これは静的ファイルのリクエストに対してクッキーの送信が必要無いという利点を持ちます。また、CDN や Amazon S3 のようなサービスにホストすることで、静的ファイルのオフロードを可能にします。詳細についてはファイルのコメントを参照してください。

また、ウィジェットに含まれる CSS や Javascript が HTML の中に含まないという別の最適化もあります。それらのファイルのコンテンツは外部ファイルに記述され、HTML からはリンクとして参照されます。このファイルも同様にコンテンツのハッシュに応じた名前になります。この最適化の効果としては

  1. キャッシュが正しく機能します。
  2. Yesod は同じハッシュのファイルが存在すれば CSS/Javascript コンテンツをディスクに書き込むといった高コストな操作を避けることができます。

最後に、すべての Javascript は hjsmin を使って自動的に最小化されます。

まとめ

この章の目的は scaffolded サイトに存在するすべての行を説明することではなく、それがどのように機能するかについて概略を説明することでした。これらの内容をより深く理解するための最良の方法は、scaffolded サイトを使って実際に Yesod サイトを書き始めてみることです。