Twirl模板引擎介绍
Twirl 是 Play 内置的模板引擎,负责数据层展示与用户行为收集。Twirl 被设计成一个独立的模块,可以脱离 Play 环境单独使用。Twirl 采用Scala作为底层模板语言,所以你无需学习额外的语法便可以轻松上手。
Hello, Twirl
创建文件views/hello.scala.html
,内容如下:
@(name: String)
<html>
<body>
<h1>Hello, @name!</h1>
</body>
</html>
每个模板文件最终将会被编译成一个同名函数,所以我们也可以称模板文件为模板函数。模板函数的内容包括两部分,第一行为函数参数声明,其余部分为函数体。对于上面定义的模板文件,编译后生成的函数类型为:
代码语言:javascript复制(name: String) => Html
由于编译后的模板函数就是普通的 Scala 函数,所以你可以在任何地方使用模板函数:
代码语言:javascript复制val content = views.html.hello("play")
跟常见的模板层引擎一样,模板函数的函数体包含两部分内容,一部分是静态的HTML内容,另一部分是动态的Scala表达式。静态的HTML内容将会保持不变原样输出,而动态的 Scala 表达式部分将会插入动态生成的内容。 Twirl使用@
符号区分Scala表达式和HTML文本,即以@
符号开头的部分是Scala表达式,其余部分即为HTML内容。
我们可以通过@符号在函数体内引用参数:
代码语言:javascript复制<h1>Hello, @name!</h1>
配合()
和{}
可以写出更复杂的语句:
<h1>Hello, @(user.firstName user.lastName)!</h1>
<h1>Hello, @{
customer.firstName
customer.lastName
}!
</h1>
()
用于插入单行代码,插入结果为当前表达式的值;而{}
用于插入多行代码,插入结果为最后一行表达式的值。
由于模板文件参与编译过程,并且是类型安全的,所以编译器会帮你拦住大部分错误。
Twirl是无状态的
JSP或是其它的第三方模板引擎都会有一个上下文(Context)的概念,上下文中保存着当前请求的状态。而在Twirl中则没有上下文的概念,模板函数仅仅是一个普通的函数,没有复杂的上下文状态存在,这种无状态的设计更加简洁并易于理解,不仅方便测试,而且大大提升了模板层的可用性,我们不仅可以在 Controller 层使用模板页面,在 Service 层一样可以使用。例如可以利用Twirl编写一个邮件模板,或者是利用Twirl生成静态Html文件等等。
大家可能觉得奇怪,没有了上下文,在模板中如何获取当前的请求呢?答案很简单:通过参数传递喽!利用Scala的隐式参数的特性,在调用模板函数时不需要显示传入,编译器会自动传入。
Twirl基本语法
下面介绍几个常用的Scala表达式,方便你快速熟悉Twirl语法。
@if
表达式用于控制某部分HTML内容是否显示:
@if(user.isMale) {
<h1>你好, @{user.name}先生</h1>
} else {
<h1>你好, @{user.name}小姐</h1>
}
@for
表达式用于重复显示HTML内容:
<ul>
@for(u <- users) {
<li>@{user.name}</li>
}
</ul>
对于通用逻辑可以定义为可复用函数:
代码语言:javascript复制@display(product: Product) = {
@product.name ($@product.price)
}
<ul>
@for(product <- products) {
@display(product)
}
</ul>
@defining
用于定义可重用的值:
@defining(user.firstName " " user.lastName) { fullName =>
<div>你好 @{fullName}</div>
}
使用函数也可以实现可重用值,并且更加简洁:
代码语言:javascript复制@fullName = @{user.firstName " " user.lastName}
<div>你好 @{fullName}</div>
@import
用于引入外部依赖:
@(user: User)
@import utils._
...
通过@**@可以插入一段注释:
代码语言:javascript复制@*********************
* This is a comment *
*********************@
@Html
用于展示原始字符串内容,避免转义,通常用于输出HTML文本或Json格式内容:
@Html(htmlContent)
页面布局
通常我们会创建一个views/main.scala.html
文件用于控制页面的整体布局:
@(title: String)(content: Html)
<!DOCTYPE html>
<html>
<head>
<title>@title</title>
</head>
<body>
<section class="content">@content</section>
</body>
</html>
main
模板接受两个参数,一个是页面标题title
,另一个是页面正文content
。然后我们就可以在views/index.scala.html
模板中复用这个布局:
@(title: String)
@main(title) {
<h1>欢迎光临!</h1>
}
处理表单
用户在浏览器端通过Html表单填充业务数据并提交至服务器端进行处理,与之对应的,Play 在服务器端提供了 Form 类用于处理与Html表单相关的操作:
- 数据绑定
- 数据校验
- 数据抽取
- 错误处理
- 页面渲染
在使用 Play 的 Form 相关功能之前,需要先导入如下路径:
代码语言:javascript复制import play.api.data._
import play.api.data.Forms._
import play.api.data.validation.Constraints._
数据绑定
数据绑定是指将用户输入的表单数据绑定到 Form 对象的过程,例如下面定义一个用于接收用户登录邮箱和密码的 Form 实例:
代码语言:javascript复制val loginForm = Form(tuple("email" -> text, "password" -> text))
利用 Form.bindFromRequest() 方法可以从当前的请求体中绑定表单参数:
代码语言:javascript复制val bindForm = userForm.bindFromRequest() match {
case Some(v) => println("绑定成功")
case _ => println("绑定失败")
}
数据校验
下面我们为表单参数添加如下约束:
- email参数必填,且格式必须为邮箱
- password参数必填,且内容必须为非空
val loginForm = Form(tuple("email" -> email, "password" -> nonEmptyText))
此时在使用 Form.bindFromRequest() 方法从当前的请求体中绑定表单参数时,只有当所有的表单参数均满足约束条件才能绑定成功,否则绑定失败:
代码语言:javascript复制val bindForm = userForm.bindFromRequest() match {
case Some(v) => println("绑定成功")
case _ => println("绑定失败")
}
常用的约束如下:
-
text
: 映射为 scala.String 类型, 可以使用 minLength 和 maxLength 参数限定长度。 -
nonEmptyText
: 映射为非空的 scala.String 类型, 可以使用 minLength 和 maxLength 参数限定长度。 -
number
: 映射为 scala.Int 类型,可选参数: min, max, 和 strict。 -
longNumber
: 映射为 scala.Long 类型, 可选参数: min, max, 和 strict。 -
bigDecimal
: 映射为 scala.math.BigDecimal 类型,可选参数:precision 和 scale. -
date
,sqlDate
: 映射为 java.util.Date, java.sql.Date 类型,可选参数:pattern 和 timeZone. -
email
: 映射为邮箱格式的 scala.String 类型。 -
boolean
: 映射为 scala.Boolean。 -
checked
: 映射为 scala.Boolean。 -
optional
: 映射为 scala.Option。
除了上面的内置约束,我们可以针对每个表单项编写更精确的自定义约束,例如:
代码语言:javascript复制val userForm = Form(
tuple(
"email" -> text.verifying(_ == "user@playscala.cn"),
"name" -> text.verifying(_ == "user")
)
)
我们也可以针对整个 Form 编写自定义约束:
代码语言:javascript复制 val userForm = Form(
tuple(
"email" -> email,
"name" -> nonEmptyText
) verifying("邮箱名和用户名不匹配!", t => t._1.contains(t._2))
)
数据抽取
当执行了数据绑定,并且成功地通过了数据校验,我们就可以从 Form 中抽取业务数据了:
代码语言:javascript复制loginForm.bindFromRequest().fold(
formWithErrors => {
//绑定失败,formWithErrors 包含了详细的错误信息
BadRequest(views.html.login(formWithErrors))
}, tuple => {
//利用模式匹配取出业务数据
val (email, password) = tuple
Redirect(routes.Application.home(email))
}
)
在上面的示例中,我们从 Form 中抽取的结果类型为Tuple,但是当表单项比较多时使用Tuple类型就不太合适了。针对上面的示例,我们稍作改动便可以将抽取的结果类型变为 Case Class:
代码语言:javascript复制case class UserData(email: String, name: String)
val userForm = Form(
mapping(
"email" -> email,
"name" -> nonEmptyText
)(UserData.apply)(UserData.unapply)
)
错误处理
当数据校验未通过时,我们将会得到一个包含错误信息的 formWithErrors 对象,通过调用 Form.errors 方法可以获取所有错误列表:
代码语言:javascript复制val allErrors: Seq[FormError] = formWithErrors.errors
每个 FormError
包含如下信息:
-
key
如果key为空则为全局错误,否则为表单字段错误且和表单字段同名。 -
message
错误消息提示或错误消息对应的key。 -
args
用于填充错误消息的参数。
Form.globalErrors
包含在Form.errors
中,其key
值为空,无对应的表单项。通常为 Form 级的自定义校验错误。
如果表单校验发生错误,我们可以直接把错误信息以Json格式写回客户端:
代码语言:javascript复制loginForm.bindFromRequest().fold(
formWithErrors => {
//绑定失败,写回错误信息
Ok(Json.obj("status" -> 1, "errors" -> formWithErrors.errorsAsJson))
}, tuple => {
//绑定成功
Ok(Json.obj("status" -> 0))
}
)
页面渲染
我们可以直接将 Form 对象作为模板参数传递到模板层,Play 专门为模板层提供了一个工具包(views.html.helper._)用于处理表单操作。除了上文的 formWithErrors 对象, 我们也可以将业务数据填充到 Form 实例中,然后传递给模板页面进行渲染:
代码语言:javascript复制val userForm = Form(tuple("email" -> email, "name" -> nonEmptyText))
Ok(views.html.editUser(userForm.fill(("user@playscala.cn", "user"))))
在editUser.scala.html 模板文件中,我们可以很方便地将 userForm 中的数据渲染成 HTML 表单:
代码语言:javascript复制@(userForm: Form[(String, String)])
@helper.form(action = routes.Application.doEditUser()) {
@helper.inputText(userForm("email"))
@helper.inputText(userForm("name"))
}
利用 helper 工具包在模板层渲染表单时,对前端页面设计有较强的侵入性,严重影响了前后端分离开发,所以在实际开发中不建议使用 helper 工具包,而是直接编写 Html 代码:
代码语言:javascript复制@(userForm: Form[(String, String)])
<form action="@routes.Application.doEditUser()" method="Post">
<input name="email" value="@userForm("email").value">
<input name="name" value="@userForm("name").value">
</form>
更进一步,模板层参数中也不应该出现 Form 类型参数,前端通过异步方式获取表单校验或提交的结果。当用户再次提交模板层渲染出的表单时,表单参数传至服务器端,重新执行校验、绑定和抽取等步骤,整个处理过程形成了一个闭环。
关于模板层 helper 的详细内容请参考官方文档。
小结
Twirl 模板引擎使用 Scala 编程语言作为其底层的模板语法,利用无状态的函数式设计,为开发者带来了非常不错的开发体验。由于 Twirl 优秀的设计,即使在前后端分离的主流开发形势下,仍然发挥着不可替代的作用。
转载请注明 joymufeng