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>
()
用於插入單行代碼,插入結果爲當前表達式的值;而{}
用於插入多行代碼,插入結果爲最後一行表達式的值。
由於模板文件參與編譯過程,並且是類型安全的,所以編譯器會幫你攔住大部分錯誤。
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. -
date
,sqlDate
: 映射爲 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