Play For Scala 开发指南 - 第8章 用户界面

2019-03-12 16:05:24 浏览数 (1)

Twirl模板引擎介绍

Twirl 是 Play 内置的模板引擎,负责数据层展示与用户行为收集。Twirl 被设计成一个独立的模块,可以脱离 Play 环境单独使用。Twirl 采用Scala作为底层模板语言,所以你无需学习额外的语法便可以轻松上手。

Hello, Twirl

创建文件views/hello.scala.html,内容如下:

代码语言:javascript复制
@(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>

配合(){}可以写出更复杂的语句:

代码语言:javascript复制
<h1>Hello, @(user.firstName   user.lastName)!</h1>
<h1>Hello, @{
             customer.firstName
             customer.lastName
           }!
</h1&gt

()用于插入单行代码,插入结果为当前表达式的值;而{}用于插入多行代码,插入结果为最后一行表达式的值。

由于模板文件参与编译过程,并且是类型安全的,所以编译器会帮你拦住大部分错误。

Twirl是无状态的

JSP或是其它的第三方模板引擎都会有一个上下文(Context)的概念,上下文中保存着当前请求的状态。而在Twirl中则没有上下文的概念,模板函数仅仅是一个普通的函数,没有复杂的上下文状态存在,这种无状态的设计更加简洁并易于理解,不仅方便测试,而且大大提升了模板层的可用性,我们不仅可以在 Controller 层使用模板页面,在 Service 层一样可以使用。例如可以利用Twirl编写一个邮件模板,或者是利用Twirl生成静态Html文件等等。

大家可能觉得奇怪,没有了上下文,在模板中如何获取当前的请求呢?答案很简单:通过参数传递喽!利用Scala的隐式参数的特性,在调用模板函数时不需要显示传入,编译器会自动传入。

Twirl基本语法

下面介绍几个常用的Scala表达式,方便你快速熟悉Twirl语法。

@if表达式用于控制某部分HTML内容是否显示:

代码语言:javascript复制
@if(user.isMale) {
  <h1>你好, @{user.name}先生</h1>
} else {
  <h1>你好, @{user.name}小姐</h1>
}

@for表达式用于重复显示HTML内容:

代码语言:javascript复制
<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用于定义可重用的值:

代码语言:javascript复制
@defining(user.firstName   " "   user.lastName) { fullName =>
  <div>你好 @{fullName}</div>
}

使用函数也可以实现可重用值,并且更加简洁:

代码语言:javascript复制
@fullName = @{user.firstName   " "   user.lastName}
<div>你好 @{fullName}</div>

@import用于引入外部依赖:

代码语言:javascript复制
@(user: User)

@import utils._
...

通过@**@可以插入一段注释:

代码语言:javascript复制
@*********************
* This is a comment  *
*********************@

@Html用于展示原始字符串内容,避免转义,通常用于输出HTML文本或Json格式内容:

代码语言:javascript复制
@Html(htmlContent)

页面布局

通常我们会创建一个views/main.scala.html文件用于控制页面的整体布局:

代码语言:javascript复制
@(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模板中复用这个布局:

代码语言:javascript复制
@(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参数必填,且内容必须为非空
代码语言:javascript复制
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.
  • datesqlDate: 映射为 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

0 人点赞