Facebook Twitter LinkedIn E-mail
magnify
Home 2014 九月

Play Framework Web开发教程(32): 模板基本知识和一些通用结构

本篇介绍Play模板的一些基本用法,之后你可以编写View模板来实现MVC中的View部分。
@特殊符号
在Scala模板中,@符号表现Scala表达式的开始。和其它Web框架一些模板不同的是,Play的View模板没有使用特殊的符号作为表达式的结束符。Play的模板编译器自动根据上下文判断Scala表达式是否结束。这样可以使得编写View模板变得非常简洁:

Hello @name!
Your age is @user.age.

本例的第一行,name为一Scala表达式,第二行中,user.age也是表达式。而age后的”.”不是表达式的一部分。那么些吗的例子中:

Next year, your age will be @user.age + 1

Scala表达式也只到user.age为止,如果你需要表示比age大一年,那么需要使用括号:

Next year, your age will be @(user.age + 1)

有时,你需要使用多个Scala语句,那么需要使用{},比如:

Next year, your age will be
@{val ageNextYear = user.age + 1; ageNextYear}

在{}之间你可以使用任意的scala语句。
因为@是特殊字符,如果你需要输出@符号本身,那么你需要使用另外一个@进行转义,比如:

username@@example.com

模板使用@* *@作为注释,多条Scala表达式可以使用Scala语言的注释语法结构。 模板编译器不会在编译后的HTML文档中输出注释。
表达式
模板中Scala表达式可以使用任意的Scala语句,Play自动添加如下的API包到View模板中:

■ models._
■ controllers._
■ play.api.i18n._
■ play.api.mvc._
■ play.api.data._
■ views.%format%._

 
引入models._ 和controllers._ 可以保证在View模板中可以使用定义的Model和Controller类。 i18n定义的语言本地化和全球化支持。mvc._引入MVC相关的部件。data._定义了表单(form)和数据校验相关的类定义,而views.%format% 根据你定义的View模板格式而定,比如你使用html格式的模板,那么你引入views.html._,它定义了一些HTML辅助函数方便生成HTML。

显示集合类型数据
在Play中使用集合类型的地方非常多,比如显示用户列表,文章列表,产品目录,分类或者标签,比如我们可以使用下面的代码显示文章的名称:

<ul>
@articles.map { article =>
<li>@article.name</li>
}
</ul>

或者使用for-表达式(View编译器自动添加yeild关键字,因为没有yield,for表达式不会生成任何结果,因此你使用for表达式时可以省略掉yield部分)

<ul>
@for(article <- articles) {
	<li>@article.name</li>
}
</ul>

安全和转义
应用的开发者应该总是把安全放在心上,当处理View模板时,cross-site 脚本的安全漏洞要非常注意。 在允许用户输入的情况下,要注意HTML注入的风险,下图给出了一个使用cross-site 脚本攻击网站的示意图:

20140915001

因此我们需要使用HTML转义来避免这种情况。对于Play的View模板引擎来说,不是每个值都是同等对待的,比如对于字符串banana来说,如果我们需要在一个HTML文档中显示这个字符串,我们需要判断这是一个HTML代码片段,还是一个普通的字符串,如果是HTML代码片段,那么需要直接输出banana,如果是作为普通字符串来显示,那么需要对< / > 进行转义,因为它们是特殊字符串,那么我们需要输出:<b>banana</b>
有时你可能会觉得糊涂,究竟什么时候需要转义,对于Play来说,你在View模板中使用的字面量都被Play认为是HTML语句片段,不经转义直接输出。这是因为总是由开发人员来编写View模板,因此通常是认为安全的。但其中的Scala语句的输出都经过转义。比如我们由如下的View模板:

@(review: Review)
<h1>Review</h1>
<p>By: @review.author</p>
<p>@review.content</p>

我们使用如下代码来显示一个页面:

val review = Review("John Doe", "This article is <b>awesome!</b>")
Ok(views.html.basicconstructs.escaping(review))

这将显示如下页面:
20140915002

这正是我们所需要的,我们不希望用户输入的内容是HTML代码片段,也就是不允许用户输入使用HTML标记。

一般情况下,Scala语句部分输出是经过转义的,如果你需要阻止这种缺省行为 ,可以使用Html封装一下,此外你还可以使用Scala的XML函数库,直接输出HTML代码,例如:

@{
	<b>hello</b>
}

这里的hello也是没有转义的。

 

Play Framework Web开发教程(31): 使用View模板概述

前面介绍了MVC中的Controller 和Model部分,如果你不要Web页面(前台),那么我们的Play 教程就可以到此为止了,或者你也不想使用Play来生成HTML页面,而是使用其它Web前台技术,那么View部分你也可以不看了:-)。
不过大部分的Play应用都是需要使用Web页面的,对于HTML页面的显示,你可以使用Scala语句直接输出HTML,但是Play框架提供了更好的方法,-View模板引擎。使用View模板,你无需使用Scala语句直接生成HTML,你可以编写HTML文档,嵌入模板语言,可以大大提高工作效率:
通常在Controller中使用View模板,下图显示了Play中模板在HTTP请求-响应循环中的位置:
20140909001
使用View模板允许你重复使用部分HTML代码,比如页头,页脚,布局等。此外,使用View模板的另一个好处是可以把业务逻辑和现实逻辑分开。
Play模板引擎中使用Scala语言作为模板语言,因此支持数据类型安全,每个View模板最终都编译成Scala代码,在Controller使用View模板有如调用函数。

 

Play Framework Web开发教程(30): 使用静态资源

从Web应用中页面上显示的不全都是需要动态生成的,通常的Web应用也包含一些静态文件:比如图像,JavaScript文件,CSS文件等。Play支持这些静态资源,方法和前面生成动态HTTP响应类型,也是通过路由配置,把HTTP请求映射到一个Controller动作。
使用缺省配置
大部分情况,你想在Web应用中使用一些静态文件,此时缺省的配置就足够了。此时,你需要把这些文件和目录放在Web应用的public目录下,而使用URL路径/assets来访问它们,后面的路径为相对于public的路径。
比如,你的Play应用使用了一个图标,存放在public/images/favicon.png, 你可以使用http://localhost:9000/assets/images/favicon.png 来访问这个图片:

<link href="/assets/images/favicon.png" rel="shortcut icon" type="image/png">

同样的,缺省你可以把javascripts文件和CSS 文件分别放在public/javascripts和public/sytlesheets目录下。

你可以查看一下conf/routes文件,缺省的HTTP路由配置,定义了如何访问静态资源:

GET /assets/*file controllers.Assets.at(path="/public", file)

这条规则,表明 HTTP GET请求 /assets/时映射到Controllers.Assets.at方法,该方法使用两个参数,告诉该action方法在什么地方查找所需文件。缺省使用public路径,如果你需要使用其它目录存放不同类型的资源,可以定义自己的路由规则,比如:

GET /images/*file controllers.Assets.at(path="/public/images", file)
GET /styles/*file controllers.Assets.at(path="/public/styles", file)

使用assets逆向路由
之前我们介绍了使用逆向路由来避免hardocded的URL,因为Assets.at也是一个普通的Action方法,因此你可以使用assets逆向路由,例如:

<link href="@routes.Assets.at("images/favicon.png")"
rel="shortcut icon" type="image/png">

缓存和ETag
除了逆向路由的优点,使用asset控制器的另外一个优点是内置的缓存支持以及和Http Entity Tag(Etag)的支持。 从而允许客户端根据需要是否要从服务器请求资源还是可以使用Cached中的文件。
比如,如果我们发送一个请求网站的图标,assets控制器计算Etag的值,然后在HTTP返回的响应消息头添加如下:
Etag: 978b71a4b1fef4051091b31e22b75321c7ff0541

ETag 的值为资源文件名和修改时间的一个Hash值。一但获得了这个ETag值,客户端,可以发出一个条件请求,表示只有在资源在上次请求后有变化时再给我,它可以在请求中添加消息头:

If-None-Match: 978b71a4b1fef4051091b31e22b75321c7ff0541

如果Play assets控制器计算出同样的Hash值,就可以通知客户端,不需要重新下载,可以使用缓存中的拷贝:

HTTP/1.1 304 Not Modified
Content-Length: 0

使用gzip 压缩资源
网页加载的速度至关重要,HTTP压缩是Web服务器和客户端解决网页大小的一个重要功能。使用gzip可以大大缩小大文本文件(比如JavaScript等)。
它的工作流程是:首先浏览器(客户端)告诉服务器,它可以接受压缩过的资源,这可以通过在请求消息头中添加:
Accept-Encoding:gzip
来告知服务器,它支持的压缩方法,服务器由此可以发送一个压缩过的资源,而不是原来未经处理的资源。同时告诉浏览器压缩的方法:
Content-Encoding: gzip
对于Play应用,这部分处理是内置的,不需要程序员做特别处理,如果浏览器支持压缩,Play应用自动压缩资源然后发给浏览器。这种情况发生在下面条件为真时:

  • Play运行在产品模式(Prod 模式),在开发环境中,压缩不是期望的结果。
  • Play接受的请求被映射到Asset控制器。
  • HTTP请求消息头包含有Accept-Encoding: gzip
  • HTTP请求映射到静态文件,存在同名的含有.gz后缀的文件

如果其中任意一个条件为假,Asset控制器就发回原始(非压缩)文件。

后面我们跳过MVC中的Model部分,这部分可以参考Slick开发教程,和Play开发本身关系不是非常密切。你也可以选用其它ORM框架,或者直接使用SQL。

 

 

Play Framework Web开发教程(29): HTTP响应的消息头

除了上篇介绍的HTTP响应的状态码之外,Play也允许你定制响应的消息头,也就是指导客户端(比如浏览器)如何处理响应消息的元数据。比如对于之前的501 消息,我们可以加上一个消息的长度为0,表示没有消息体:
HTTP/1.1 501 Not Implemented
Content-Length: 0

再比如重定向消息 ,302 ,可以添加Location消息头,告诉客户端新的网站来取得所需的资源:

HTTP/1.1 302 Found
Location: http://localhost:9000/products

实际上,Play内部实现Redirect方法,就是为HTTP响应添加Location消息头:

Status(FOUND).withHeaders(LOCATION -> url)

你可以使用类似的方法来定制你的HTTP响应的消息。比如你实现了一个Web Service来添加一个产品,你可以返回一个201 消息,通知客户端创建产品成功,同时添加一个Location消息头,给出新创建产品的URL:

HTTP/1.1 201 Created
Location: /product/5010255079763
Content-Length: 0

你可以使用如下代码,构成上面的响应消息:

val url = routes.Products.details(product.ean).url
Created.withHeaders(LOCATION -> url)

其中 routes.Products.details(product.ean).url 为逆向路由。

设置Content Type
每个包含HTTP消息体的HTTP响应一般都包含一个Content-Type消息头,它的类型为一MIME类型,表示消息体的数据格式。Play自动为一些已知的消息类型添加个Content-Type消息头 ,比如HTML页面text/html,而对于字符串使用 text/plain。
如果你需要返回JSON类型的数据,你可以重新设置缺省的text/plain类型:

val json = """{ "status": "success" }"""
Ok(json).withHeaders(CONTENT_TYPE -> "application/json")

这是一个非常常见的用法,因此Play提供了一些便利的辅助函数来实现同样的功能:

Ok("""{ "status": "success" }""").as("application/json")

这还可以简化使用JSON 常量(定义在play.api.http.ContentTypes,Controller派生于这个trait)

Ok("""{ "status": "success" }""").as(JSON)

Session数据
有些时候,你需要保存一些数据来记住用户正在进行的操作,比如你需要记住用户查询的关键字,以便用户重复查询。此时一种解决方案是使用Session数据,它是由个Key-Value(键-值)对,它对于当前用户的回话有效,也就是在用户关闭浏览器前有效, 下面是你使用Session数据的基本方法:
首先,是在一个Session中保存要保存的数据:

Ok(results).withSession(
	request.session + ("search.previous" -> query)
)

然后在其它需要使用”search.previous”的地方读取它的值:

val search = request.session.get("search.previous")

如果需要删除Session中某个值,可以使用如下代码:

Ok(results).withSession(
	request.session - "search.previous"
)

注意:Session 的实现是使用HTTP Cookie来实现,因此它的总的容量是几个K字节。所以一般用来保存少量数据。 为安全起见,Play的Cookie 数据是使用密匙加密的,密匙定义在conf/application中,随机生成。

flash数据
这里的Flash不是Flash 动画的Flash,可以理解成闪存,临时保存数据的地方,它的用法和Session数据类型,所不同是,它只在下一次请求有效,然后自动删除:
比如我们使用如下代码保存一个Flash数据:

Redirect(routes.Products.flash()).flashing(
	"info" -> "Product deleted!"
)

在下一页,使用这个数据:

val message = request.flash("info")

Session和Flash数据的实现都是使用HTTP Cookie,Play也提供了直接使用Cookie的API,不过通常情况下还是要避免直接使用Cookie。

 

Play Framework Web开发教程(28): HTTP状态码

最简单的HTTP响应只需要一行状态码来描述处理请求的结果,比如:
HTTP/1.1 501 Not Implemented
我们看看在Play中如何生成所需的状态码。
对于这个Not Implemented 状态码, 可以通过如下代码实现:

def list = Action { request =>
	NotImplemented
}

我们前面提到一个Action方法都是一个函数(Request=>Result)由HTTP请求到HTTP响应的映射。
这个NotImplemented 作为一个play.api.mvc.Status,代表501 ,Status为play.api.mvc.Result的子类,也就是说前面的代码可以下面的等价:

def list = Action {
	new Status(501)
}

NotImplemented 是定义在play.api.mvc.Controller 多个状态码的其中之一。

 

Play Framework Web开发教程(27): 生成HTTP响应(二)

二进制数据
通常情况下,Web应用使用的二进制数据都是静态数据文件,比如图像,关于使用这些静态文件我们后面再说,这里我们介绍动态生成的二进制文件(比如PDF文件,图像等),对于Play应用来说,返回二进制文件和前面提到的文本文件,JSON数据等没有太大的区别,所不同的是,你需要设置正确的内容类型(Content Type)。
比如,前面前面例子显示条码,我们使用的是开源条码函数库barcode4j(http://sourceforge.net/projects/barcode4j/),如果在项目中需要引用这个库,需要在build.sbt中添加这个库的引用:

libraryDependencies ++= Seq(
  jdbc,
  anorm,
  cache,
  "net.sf.barcode4j" % "barcode4j" % "2.0"
)     

之后,我们就可以定义Barcode控制器,定义barcode方法

package controllers
import play.api.mvc.{Action, Controller}
object Barcodes extends Controller {
  val ImageResolution = 144
  def barcode(ean: Long) = Action {
    val MimeType = "image/png"
    try {
      val imageData = ean13BarCode(ean, MimeType)
      Ok(imageData).as(MimeType)
    }
    catch {
      case e: IllegalArgumentException =>
        BadRequest("Couldn’t generate bar code. Error: " + e.getMessage)
    }
  }
  def ean13BarCode(ean: Long, mimeType: String): Array[Byte] = {
    import java.io.ByteArrayOutputStream
    import java.awt.image.BufferedImage
    import org.krysalis.barcode4j.output.bitmap.BitmapCanvasProvider
    import org.krysalis.barcode4j.impl.upcean.EAN13Bean
    val output: ByteArrayOutputStream = new ByteArrayOutputStream
    val canvas: BitmapCanvasProvider =
      new BitmapCanvasProvider(output, mimeType, ImageResolution,
        BufferedImage.TYPE_BYTE_BINARY, false, 0)
    val barcode = new EAN13Bean()
    barcode.generateBarcode(canvas, String valueOf ean)
    canvas.finish
    output.toByteArray
  }
}

注意其中mimetype设为 image/png

然后我们可以定义路由配置

GET         /barcode/:ean         controllers.Barcodes.barcode(ean: Long)

运行后,我们就可以显示条码

20140902001