Play ドキュメントを Skinny で書くと - The template engine
これは Play framework 2.x Scala Advent Calendar 2013 の 18 日目です。
http://www.adventar.org/calendars/114
なお、この記事で触れる Play は 2.2.1、Skinny は 0.9.20 です。両者とも最新バージョンでは API や仕様が変更になっている場合があります。
The template engine
http://www.playframework.com/documentation/2.2.x/ScalaTemplates
Play2 は独自の Scala Template を持っており、最近では、この実装は Twirl という名前で単独のライブラリとして切り出されています。
https://github.com/spray/twirl
Twirl は Spray のプロジェクトの一部なのですが、最近 Spray も Typesafe のプロダクトになったので、この辺も将来的には統合されていくとかどうとか(あまり動向追ってない)。
Twirl は Play に依存していないので Scalatra など別のフレームワークとも組み合わせて利用することができます。みんな大好き GitBucket は Scalatra + Twirl ですね。
https://github.com/takezoe/gitbucket
一方、Skinny は Scalate を標準のテンプレートエンジンとしてサポートしています。
http://skinny-framework.org/documentation/view-templates.html
http://scalate.fusesource.org/
Scalate も基本的には事前に Scala コードに変換してコンパイルするアプローチですが、Skinny ではなるべくコンパイル待ち・リスタート待ちを減らしたフィードバックの早い開発を実現するために、デフォルトだと開発時はインタプリタ的に実行する設定になっています。
これのデメリットとして、全てのテンプレートに対して検証がされないだけでなく、初回アクセスのパフォーマンスも悪くなるので skinny package で本番向けにビルドする際は事前コンパイルするようになっています。
なお FreeMarker、Thymeleaf のサポートも拡張として存在しますが、最も Scala の表現力を活かせるのはやはり Scalate になります。Java 向けのテンプレートでは Scala メソッド呼び出しでかなり制限があるので、アプリケーションの性質によっては厳しくなりそうです。この辺は用途に合わせて判断されるのがよいかと思います。
それでは早速 Play のドキュメントのサンプルコードを見ていきます。
Overview
@(customer: Customer, orders: List[Order])
<h1>Welcome @customer.name!</h1>
<ul>
@for(order <- orders) {
<li>@order.title</li>
}
</ul>
上記のような例は Scalate の SSP(Scala Server Pages) だとこのようになります。少し書き方が違うだけですね。
http://scalate.fusesource.org/documentation/ssp-reference.html
<%@ val customer: Customer %>
<%@ val orders: List[Order] %>
<h1>Welcome ${customer.name}!</h1>
<ul>
#for (order <- orders)
<li>${order.title}</li>
#end
</ul>
Scalate は SSP 以外にも Scaml、Jade、Mustache をサポートしています。私は Jade がお気に入りなので、この記事では Jade のサンプルを多く紹介します。
Jade は Scaml をより簡潔に書けるようにしたものなので Scaml と Jade のリファレンスを併せて見るとほぼやりたいことが網羅されています。ここでは Jade を中心に説明しますが Scaml はタグを %li のように書く以外は Jade とほぼ同じになります。
http://scalate.fusesource.org/documentation/scaml-reference.html
http://scalate.fusesource.org/documentation/jade.html
-@val customer: Customer
-@val orders: List[Order]
h1
Welcome #{customer.name}!
ul
-for (order <- orders)
li
=order.title
Play Scala Template(Twirl)だと、コンパイル済のコードを controller から参照しますが*1
val content = views.html.Application.index(c, o)
Skinny では render メソッドに文字列で指定します。
render("/Application/index")
Play では app/views/Application/index.scala.html というファイルが存在する前提ですが Skinny では src/main/webapp/WEB-INF/views/Application/index.html.ssp が存在することが前提となります。
Syntax: the magic ‘@’ character
Play Scala Template(Twirl)では変数や for/if 式の始まりが @ からになりますが Scalate の場合は SSP と Scaml/Jade、Mustache でそれぞれ異なります。この辺は Scalate のドキュメントをご覧ください。
http://scalate.fusesource.org/
Template parameters
Play では、テンプレートが受け取るパラメータをこのように指定しますが
@(customer: Customer, orders: List[Order]) @(title: String = "Home") @(title: String)(body: Html)
それぞれ Jade だと
-@val customer: Customer -@val orders: List[Order] -@val title: String = "Home" -@val title: String -@val body: String
のようになります。
Iterating
<ul>
@for(p <- products) {
<li>@p.name ($@p.price)</li>
}
</ul>
Jade だと以下のようになります。インデントでスッキリと書くことが出来ますね。
ul
-for (p <- products)
li #{p.name} (#{p.price})
If-blocks
@if(items.isEmpty) {
<h1>Nothing to display</h1>
} else {
<h1>@items.size items!</h1>
}
Jade だと以下のようになります。こちらもインデントでスッキリと書くことが出来ますね。
- if (items.isEmpty)
h1 Nothing to display
- else
h1 #{items.size} items!
Declaring reusable blocks
@display(product: Product) = {
@product.name ($@product.price)
}
<ul>
@for(product <- products) {
@display(product)
}
</ul>
@title(text: String) = @{
text.split(' ').map(_.capitalize).mkString(" ")
}
<h1>@title("hello world")</h1>
JSP に何でも書いちゃうノリで SSP でゴリゴリ実装すれば同じことは出来ますが、わざわざ紹介するようなやり方でもないですね...
<% def display(product: Product) = s"${product.name} (${product.price})" %>
%for (product <- products)
<%= display(product) %>
%end
ユーティリティの class/object を用意して利用すれば特に問題はなさそうです。
Declaring reusable values
@defining(user.firstName + " " + user.lastName) { fullName =>
<div>Hello @fullName</div>
}
私の認識が正しければ { .. } 内に限定して値を渡すことはできないですね。そこにこだわらなければ Jade だとこんな感じでやればよいと思います。
-@val fullName: String = user.firstName + " " + user.lastName
div Hello #{fullName}
Import statements
@import utils._
SSP だと
<% import utils._ %>
Jade だと
- import utils._
のようになります。
Play だと build.sbt にあらかじめ
templatesImport += "com.abc.backend._"
とデフォルトの import を設定できますが Skinny では以下のクラスに
src/main/scala/templates/ScalatePackage.scala
/** Returns the Scala code to add to the top of the generated template method */ def header(source: TemplateSource, bindings: List[Binding]) = """ import com.abc.backend._ import com.abc.util._ """
のように指定します。
Comments
@********************* * This is a comment * *********************@
Scaml/Jade では
-# This is a comment Next line is also comment
のように -# のあと、インデントします。
Escaping
<p> @Html(article.content) </p>
デフォルトでエスケープされる点は同じです。エスケープしたくない場合は
p != article.content
または
p = unescape(article.content)
と指定してください。
Scala templates common use cases
http://www.playframework.com/documentation/2.2.x/ScalaTemplateUseCases
Layout
Play では views/main.scala.html で以下のようにしますが
@(title: String)(content: Html)
<!DOCTYPE html>
<html>
<head>
<title>@title</title>
</head>
<body>
<section class="content">@content</section>
</body>
</html>
Skinny は src/main/webapp/WEB-INF/layouts/default.jade で同じことは以下のように記述します。拡張子を変えて ssp/scaml/mustache で記述しても問題ありません。また、このテンプレートを読み込む先と利用するエンジンが異なっていても構いません(views/root/index.html.ssp でこの jade を使用するのも OK)。
-@val title: String
-@val content: String
!!! 5
html
head
title #{title}
body
section.content #{content}
詳細はこちらをご覧ください。
http://scalate.fusesource.org/documentation/user-guide.html
@(title: String)(sidebar: Html)(content: Html)
<!DOCTYPE html>
<html>
<head>
<title>@title</title>
</head>
<body>
<section class="sidebar">@sidebar</section>
<section class="content">@content</section>
</body>
</html>
このテンプレートをこんな感じで使うと。
@sidebar = { <h1>Sidebar</h1> }
@main("Home")(sidebar) { <h1>Home page</h1> }
Skinny ではまずテンプレートがこうなり(Jade の場合)
-@val title: String
-@val sidebar: String
-@val content: String
!!! 5
html
head
title #{title}
body
section.sidebar #{sidebar}
section.content #{content}
使う側はこのようになります。
-attributes("title") = "Home"
-attributes("sidebar") = capture {
h1 Sidebar
-}
h1 Home page
Jade だと取っ付きづらく感じられる方もいるかもしれませんので SSP の例もあげるとこのようになります。
<% attributes("title") = "Home" %>
<% attributes("sidebar") = capture { %>
<h1>Slidebar</h1>
<% } %>
<h1>Home Page</h1>
capture などについては User Guide をご覧ください。
http://scalate.fusesource.org/documentation/user-guide.html
Tags (they are just functions, right?)
views/tags/notice.scala.html のようなファイルに関数を定義しておけば taglib 的に使えるよ、という感じのものですが Scalate では普通に object つくってそれを import して使えばいいという感じのようです。
Includes
テンプレートの中で普通に render を呼べば他のテンプレートを include できます。例えば Jade のファイルと同じ階層に hello.ssp がある場合、以下のように指定するだけです。
= render("hello.ssp")
moreScripts and moreStyles equivalents
Play では @routes.Assets.at のように呼び出すと assets のパスを解決してくれますが Scalate では ContextPath を考慮した形でパスを設定してくれる uri/url というメソッドが使えます。以下は Jade の例です。
script(type="text/javascript" src={uri("/assets/js/jquery-2.0.3.min.js")})
link(rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.1/css/bootstrap.min.css")
以下と同じことは capture でできますね。
@scripts = {
<script type="text/javascript">alert("hello !");</script>
}
明日・・・
ぶっちゃけ明日の記事は書いてないですし、ちょっとそろそろアレな感じです。明日、誰もいなかったら途切れてしまうかも・・
http://www.adventar.org/calendars/114
誰かの参加をお待ちしていますね・・って、私は一体何なのだろう。
*1:逆に言えば view がまだなかったら controller のコンパイルが通らない