Play For Scala 開發指南 - 第8章 用戶界面 頂 原

Twirl模板引擎介紹

Twirl 是 Play 內置的模板引擎,負責數據層展示與用戶行爲收集。Twirl 被設計成一個獨立的模塊,可以脫離 Play 環境單獨使用。Twirl 採用Scala作爲底層模板語言,所以你無需學習額外的語法便可以輕鬆上手。

Hello, Twirl

創建文件views/hello.scala.html,內容如下:

@(name: String)

<html>
 <body>
  <h1>Hello, @name!</h1>
 </body>
</html>

每個模板文件最終將會被編譯成一個同名函數,所以我們也可以稱模板文件爲模板函數。模板函數的內容包括兩部分,第一行爲函數參數聲明,其餘部分爲函數體。對於上面定義的模板文件,編譯後生成的函數類型爲:

(name: String) => Html

由於編譯後的模板函數就是普通的 Scala 函數,所以你可以在任何地方使用模板函數:

val content = views.html.hello("play")

跟常見的模板層引擎一樣,模板函數的函數體包含兩部分內容,一部分是靜態的HTML內容,另一部分是動態的Scala表達式。靜態的HTML內容將會保持不變原樣輸出,而動態的 Scala 表達式部分將會插入動態生成的內容。 Twirl使用@符號區分Scala表達式和HTML文本,即以@符號開頭的部分是Scala表達式,其餘部分即爲HTML內容。

我們可以通過@符號在函數體內引用參數:

<h1>Hello, @name!</h1>

配合(){}可以寫出更復雜的語句:

<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內容是否顯示:

@if(user.isMale) {
  <h1>你好, @{user.name}先生</h1>
} else {
  <h1>你好, @{user.name}小姐</h1>
}

@for表達式用於重複顯示HTML內容:

<ul>
@for(u <- users) {
  <li>@{user.name}</li>
}
</ul>

對於通用邏輯可以定義爲可複用函數:

@display(product: Product) = {
  @product.name ([email protected])
}

<ul>
@for(product <- products) {
  @display(product)
}
</ul>

@defining用於定義可重用的值:

@defining(user.firstName + " " + user.lastName) { fullName =>
  <div>你好 @{fullName}</div>
}

使用函數也可以實現可重用值,並且更加簡潔:

@fullName = @{user.firstName + " " + user.lastName}
<div>你好 @{fullName}</div>

@import用於引入外部依賴:

@(user: User)

@import utils._
...

通過@**@可以插入一段註釋:

@*********************
* 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 相關功能之前,需要先導入如下路徑:

import play.api.data._
import play.api.data.Forms._
import play.api.data.validation.Constraints._

數據綁定

數據綁定是指將用戶輸入的表單數據綁定到 Form 對象的過程,例如下面定義一個用於接收用戶登錄郵箱和密碼的 Form 實例:

val loginForm = Form(tuple("email" -> text, "password" -> text))

利用 Form.bindFromRequest() 方法可以從當前的請求體中綁定表單參數:

val bindForm = userForm.bindFromRequest() match {
  case Some(v) => println("綁定成功")
  case _       => println("綁定失敗")
}

數據校驗

下面我們爲表單參數添加如下約束:

  • email參數必填,且格式必須爲郵箱

  • password參數必填,且內容必須爲非空

val loginForm = Form(tuple("email" -> email, "password" -> nonEmptyText))

此時在使用 Form.bindFromRequest() 方法從當前的請求體中綁定表單參數時,只有當所有的表單參數均滿足約束條件才能綁定成功,否則綁定失敗:

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。

除了上面的內置約束,我們可以針對每個表單項編寫更精確的自定義約束,例如:

val userForm = Form(
  tuple(
    "email" -> text.verifying(_ == "[email protected]"), 
    "name" -> text.verifying(_ == "user")
  )
)

我們也可以針對整個 Form 編寫自定義約束:

  val userForm = Form(
    tuple(
      "email" -> email,
      "name" -> nonEmptyText
    ) verifying("郵箱名和用戶名不匹配!", t => t._1.contains(t._2))
  )

數據抽取

當執行了數據綁定,並且成功地通過了數據校驗,我們就可以從 Form 中抽取業務數據了:

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:

case class UserData(email: String, name: String)
  
val userForm = Form(
  mapping(
    "email" -> email,
    "name" -> nonEmptyText
  )(UserData.apply)(UserData.unapply)
)

錯誤處理

當數據校驗未通過時,我們將會得到一個包含錯誤信息的 formWithErrors 對象,通過調用 Form.errors 方法可以獲取所有錯誤列表:

val allErrors: Seq[FormError] = formWithErrors.errors

每個 FormError 包含如下信息:

  • key 如果key爲空則爲全局錯誤,否則爲表單字段錯誤且和表單字段同名。

  • message 錯誤消息提示或錯誤消息對應的key。

  • args 用於填充錯誤消息的參數。

Form.globalErrors包含在Form.errors中,其key值爲空,無對應的表單項。通常爲 Form 級的自定義校驗錯誤。

如果表單校驗發生錯誤,我們可以直接把錯誤信息以Json格式寫回客戶端:

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 實例中,然後傳遞給模板頁面進行渲染:

val userForm = Form(tuple("email" -> email, "name" -> nonEmptyText))
Ok(views.html.editUser(userForm.fill(("[email protected]", "user"))))

在editUser.scala.html 模板文件中,我們可以很方便地將 userForm 中的數據渲染成 HTML 表單:

@(userForm: Form[(String, String)])

@helper.form(action = routes.Application.doEditUser()) {
  @helper.inputText(userForm("email"))
  @helper.inputText(userForm("name"))
}

利用 helper 工具包在模板層渲染表單時,對前端頁面設計有較強的侵入性,嚴重影響了前後端分離開發,所以在實際開發中不建議使用 helper 工具包,而是直接編寫 Html 代碼:

@(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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章