Play 2.0 用户指南 - HTTP编程 --针对Scala开发者


    Play 2.0 的 Scala API 位于play.api包下。
   

    该API直接位于 play 顶级包中(而play.mvc是为Java开发者提供的)。对于Scala开发者,查阅play.api.mvc。html

     Actions, Controllers and Results

    什么是Action?
    
    大多数Play应用程序接受的请求由一个Action处理。
    一个play.api.mvc.Action基本上是 一个 (play.api.mvc.Request => play.api.mvc.Result)函数,它处理请求并生成响应发给客户端。
java

val echo = Action { request =>
  Ok("Got request [" + request + "]")
}


    action返回一个 play.api.mvc.Result对象,使用 HTTP response 对象返回给客户端。例如: Ok 返回一个200响应,包含text/plain 响应体。
    
    建立Action

    最简单的Action仅须要定义一个参数,一个表达式块,返回一个Result值web

Action {
  Ok("Hello world")
}

    这是建立Action最简单的方式,但咱们没法获取request对象。一般Action中都须要访问request对象。
    
    看看第二个Action,包含了参数Request => Result :ajax

Action { request =>
  Ok("Got request [" + request + "]")
}

    标记 request 参数为 隐式 一般都颇有用,可供其它API隐式的使用:json

Action { implicit request =>
  Ok("Got request [" + request + "]")
}


    最后一种建立方式,包含了一个特别的可选 BodyParser 参数:
      api

Action(parse.json) { implicit request =>
  Ok("Got request [" + request + "]")
}

    Body Parser稍后会作讲解。如今,你只须要了解Any content body parser的使用方式。

   控制器是actions的生成器

    控制器不过是产生Action的某个单例对象。
    
    定义Action生成器的最简单方法是提供一个无参,返回值为Action的方法。浏览器

package controllers

import play.api.mvc._

object Application extends Controller {

  def index = Action {
    Ok("It works!")
  }
    
}


    固然,该方法能够包含参数,这些参数能够被Action闭包访问:缓存

def hello(name: String) = Action {
  Ok("Hello " + name)
}

   简单Results安全

    目前,你可能只对一种results感兴趣:HTTP result,包含状态字,一系列HTTP Head消息和返回给web客户端的消息体。

    play.api.mvc.SimpleResult 用于定义该类result:服务器

def hello(name: String) = Action {
  Ok("Hello " + name)
}


    固然,也有一些助手方法用于方便的建立经常使用的result,如 Ok result:

def index = Action {
  Ok("Hello world!")
}

    该代码产生和上例相似的响应。

    下面展现了建立不一样 Results 的示例。

val ok = Ok("Hello world!")
val notFound = NotFound
val pageNotFound = NotFound(<h1>Page not found</h1>)
val badRequest = BadRequest(views.html.form(formWithErrors))
val oops = InternalServerError("Oops")
val anyStatus = Status(488)("Strange response type")


    可在 play.api.mvc.Results 的traint和companion对象查看所有助手方法。

    重定向也是普通Result

    浏览器重定向仅仅是另外一种普通响应。可是,此类返回值不携带响应体。

    有几种建立重定向的方法:

def index = Action {
  Redirect("/user/home")
}


    默认使用 303 SEE_OTHER 响应类型,但你也能够按需设置其余状态字:

def index = Action {
  Redirect("/user/home", status = MOVED_PERMANENTLY)
}

   “TODO” 虚拟页面
    你可使用一个Action的空实现定义为TODO:它的result是个标准的 'Not implemented yet'页面:

def index(name:String) = TODO

HTTP 路由

    内建的HTTP路由

    Router是將每一个接受到的HTTP请求转换成Action调用的组件。
    
    一个HTTP请求,被框架视为一个事件。该事件包含了两类重要信息:
        请求路径(例如:/clients/1542,/photos/list),和查询参数。
        HTTP方法(GET,PUT,POST...)

    路由规则在conf/routes中定义,并被编译。意味着,你能够在浏览器中直接查看路由错误:


   

routes声明语法
    
    conf/routes配置文件被router解析使用。该文件定义了应用程序的全部路由规则。每一个路由定义包含HTTP方法,URI模式,和一个Action调用。

    先看看示例:

GET   /clients/:id          controllers.Clients.show(id: Long)

    每一个路由定义都以一个HTTP方法开头,仅接URI模式,最后是Action调用定义。

# Display a client.
GET   /clients/:id          controllers.Clients.show(id: Long)

    可使用 # 编写注释

# Display a client.
GET   /clients/:id          controllers.Clients.show(id: Long)


    HTTP方法

    HTTP方法能够是任何HTTP支持的方法(GET,POST,PUT,DELETE,HEAD)。

    URI模式

    URI模式定义了路由的请求路径。部分路径能够是动态的。

    静态路径

    例如,想精确的匹配接受的GET /clients/all 请求,能够这样定义:

GET   /clients/all              controllers.Clients.list()

   动态部分
    若是你想定义一个经过ID检索用户的路由,你就须要添加一个动态部分:

GET   /clients/:id          controllers.Clients.show(id: Long)

    须要注意的是一个URI模式能够定义多个动态部分。

    动态部分的默认匹配策略被正则式 [^/]+ 定义,意味着任何定义了 :id 的动态部份都将被彻底匹配。

    跨越多个 /

    若是你想捕获多个动态部分,被斜线分隔,你可使用 *id 语法定义动态部分,它將使用 .* 正则规则:

GET   /files/*name          controllers.Application.download(name)

    这里,相似/files/images/logo.png这样的GET请求,name动态部分將捕获images/logo.png值。

    使用正则式定义动态部分

    你也可使用正则式定义动态部分,利用 $id<regex>语法:

GET   /clients/$id<[0-9]+>  controllers.Clients.show(id: Long)

    调用Action方法

    路由的最后一部分定义Action调用。这部分必须定义一个经验证返回值为 play.api.mvc.Action 值的控制器方法的调用声明。

    若是该方法未定义任何参数,请给出方法全限定名:

GET   /                     controllers.Application.homePage()


    若是action方法定义了一些参数,全部这些参数將在请求的URI中搜索,不管是URI路径自己仍是查询参数串。

# Extract the page parameter from the path.
GET   /:page                controllers.Application.show(page)

    或者

# Extract the page parameter from the query string.
GET   /                     controllers.Application.show(page)

 如下是相应的controller show 方法的定义:

def show(page: String) = Action {
    loadContentFromDatabase(page).map { htmlContent =>
        Ok(htmlContent).as("text/html")
    }.getOrElse(NotFound)
}


    参数类型

    对于String类型的参数,输入参数是可选的。若是你要玩改造,传入一个特定Scala类型的参数,明确指定:

GET   /client/:id           controllers.Clients.show(id: Long)

    并相应在控制器show方法中定义。controllers.Clients:

def show(id: Long) = Action {
    Client.findById(id).map { client =>
        Ok(views.html.Clients.display(client))
    }.getOrElse(NotFound)
}


    定值参数
    
    有时你会想使用某个定值参数:

# Extract the page parameter from the path, or fix the value for /
GET   /                     controllers.Application.show(page = "home")
GET   /:page                controllers.Application.show(page)

    默认值参数

   您还能够为请求参数提供默认值:

# Pagination links, like /clients?page=3
GET   /clients              controllers.Clients.list(page: Int ?= 1)


    路由优先级

    许多URL路径均可知足匹配要求。若是有冲突,采用先声明先使用的原则。

    反转路由

    router 能够將一个Scala方法调用反转生成URL。这使得你能將全部的URI模式在单一文件中集中配置,这样你就能更自信的將来重构应用。

    配置文件使用的每一个控制器,router都将在 routes 包中生成一个 “反转的” 控制器,它具备相同的方法相同的签名,但使用play.api.mvc.Call代替play.api.mvc.Action作为返回值。

    在play.api.mvc.Call定义HTTP调用,并提供HTTP方法和URI。

    例如,若是你像这样建立控制器:

package controllers

import play.api._
import play.api.mvc._

object Application extends Controller {
    
  def hello(name: String) = Action {
      Ok("Hello " + name + "!")
  }
    
}

    并在 conf / routes 文件中这样映射:

# Hello action
GET   /hello/:name          controllers.Application.hello(name)

你就可使用 controllers.routes.Application 反转出 hello 方法的URL:

// Redirect to /hello/Bob
def helloBob = Action {
    Redirect(routes.Application.hello("Bob"))    
}

    处理返回结果

    改变默认Content-Type


    Result 类型將根据设定的Scala值自动推断。

    例如:

val textResult = Ok("Hello World!")

    将Content-Type自动设置为text/plain,而:

val xmlResult = Ok(<message>Hello World!</message>)

    会將 Content-Type 设为 text/xml.

    提示:这是经过 play.api.http.ContentTypeOf 类来完成的。

    该机制至关有用,但有时候你须要定制。可使用as(contentType)实现:

val htmlResult = Ok(<h1>Hello World!</h1>).as("text/html")


    更好的方式:

 val htmlResult = Ok(<h1>Hello World!</h1>).as(HTML)

    注意:使用 HTML 替代 "text/html"的好处是字符编码转被自动处理,而且Content-Type头也会被设为 text/html;charset=utf-8。咱们稍后会看到。

    处理HTTP请求头

    你能够为响应结果添加(更新)HTTP头信息。

 Ok("Hello World!").withHeaders( CACHE_CONTROL -> "max-age=3600", ETAG -> "xx" )


    注意设置HTTP请求头将自动覆盖现有值。

    设置和删除Cookies

    Cookies不过是HTTP HEAD的特定部分,不过咱们提供了一系列的便利处理方法。

    你能够轻松的给HTTP Response 添加Cookie:

Ok("Hello world").withCookies(
  Cookie("theme", "blue")
)

    删除浏览器Cookie:

Ok("Hello world").discardingCookies("theme")

    更改HTTP Response 编码

    对于HTTP响应,确保正确的字符编码很是重要。Play默认使用utf-8处理编码。

    字符集编码既用来將响应文本转换成相应的网络socket字节码,也用于肯定HTTP头 ;charset=xxx 的信息。

    字符集编码由 play.api.mvc.Codec 自动处理。在当前请求上下文中导入 一个隐式 play.api.mvc.Codec 对象,能够改变字符集,以供全部操做使用:

object Application extends Controller {
    
  implicit val myCustomCharset = Codec.javaSupported("iso-8859-1")
    
  def index = Action {
    Ok(<h1>Hello World!</h1>).as(HTML)
  }
    
}


    这里,由于在当前上下文中有一个隐式的字符集,OK(...)方法即將生成的XML消息转成 ISO-8859-1 编码,也自动生成 text/html;charset=iso-8859-1 Content-Type头信息。

    如今,想知道 HTML 方法是怎么工做的吗?如下就是该方法的定义:

def HTML(implicit codec: Codec) = {
  "text/html; charset=" + codec.charset
}

    你也能够在你的API用相似的方式处理字符编码。

Session 和 Flash 上下文

    它们在Play中有何不一样?

    若是你试图在多个HTTP请求中保存数据,你能够將它们保存在Session或Flash中。保存在Session中的数据,对整个用户会话都有效,而保存在Flash中的数据只对下一次请求有效。
    
    理解Session和Flash的数据不在服务器端保存,而由客户cookie维护是至关重要的。这意味着数据容量很是有限(最大4KB),而且你只能保存string值。
    固然cookie数据被安全码加密,所以客户端不能修改该数据(或使其失效)。

    Play Session 不是为缓存数据准备的。若是你想缓存某个Session相关的数据,你可使用Play内建的缓存机制,保存惟一的SessionID值,维护用户数据。

    Session没有超时技术。当用户关闭浏览器时,它就会失效。若是你须要为特定的应用提供超时功能,能够在用户Session保存时间戳(timestamp),根据应用的须要来使用它。(如session最大生存时间,过时时间等等)

    读取Session值

    你能够经过request获取Session

def index = Action { request =>
  request.session.get("connected").map { user =>
    Ok("Hello " + user)
  }.getOrElse {
    Unauthorized("Oops, you are not connected")
  }
}

    另外,也能够经过一个隐式的request取得Session:

def index = Action { implicit request =>
  session.get("connected").map { user =>
    Ok("Hello " + user)
  }.getOrElse {
    Unauthaurized("Oops, you are not connected")
  }
}

    向Session存储数据

    由于Session仅仅是个Cookie,也仅仅是一个HTTP请求头。你能够像操纵其它Result属性同样的操纵Session数据:

Ok("Welcome!").withSession(
  "connected" -> "user@gmail.com"
)


    须要注意该方式將替换整个session。下面是对现有session添加元素的方式:

Ok("Hello World!").withSession(
  session + ("saidHello" -> "yes")
)


    可用相似的方式删除数据:

Ok("Theme reset!").withSession(
  session - "theme"
)


    丢弃整个session

    下面是一个特别的操做,將丢弃整个session

Ok("Bye").withNewSession


    Flash 上下文

    Flash上下文的工做机制与Session很像,但有两点不一样:
        只为一个请求保存数据
        Flash Cookie未特别标识,它可能会被用户修改

    重要:Flash 上下文只应用在非ajax请求的普通应用中,用来传输相似success/error的消息。由于数据仅保存到下一次请求,又因在复杂的应用中没法担保请求顺序,Flash会受竞争条件影响。

    下面是使用 Flash scope 的例子:

def index = Action { implicit request =>
  Ok {
    flash.get("success").getOrElse("Welcome!")
  }
}

def save = Action {
  Redirect("/home").flashing(
    "success" -> "The item has been created"
  )
}

    Body Parser

    Body Parser是什么

    HTTP PUT 或 POST 请求包含着body。body能够用Content-Type指定格式。在Play中, body parser 將请求体转换成Scala值。

    然而body可能很大,body parser 不能等待数据所有加载到内存后再解析。 A BodyParser [A] 基本上算是一个Iteratee [Array[Byte],A],意味着它以块为单位接收字节数据(只要浏览器上传一些数据),而且以 A 类型计算结果值.
    
    先考虑几个例子:
        一个 text body parser 收集字节块,转成String,將该String值作为返回值(Iteratee [Array[Byte],String])
        一个 file body parser 可將每份数据块保存到一个本地文件中,并给予一个java.io.File引用做为返回值(Iteratee          [Array[Byte],File])
        A s3 body parser 能够將每一块字节推送到Amazon S3,將S3 object id作为返回值(Iteratee [Array[Byte],S3ObjectId ])

    另外,一个 body parser能够在解析开始前,对HTTP头作些预先检查。例如:body parser能够检查一些HTTP头是否被正确设置,或者用户是否试图上传过大文件等。


    注意:这就是为何 body parser 不是一个真正的 Iteratee [Array[Byte],A] 的缘由,但又偏偏由于是一个[Array[Byte],Either[Result,A]],意味着,它有权直接发回HTTP响应结果(一般是400 BAD_REQUEST , 412 PRECONDITION _FAILED or 413 REQUEST _ENTITY_TOO_LARGE),若是它以为不能为 request body 计算正确的值的话。

    一旦 body parser完成工做并返回一个A类型的值,相应的action函数將被调用,经处理的body值就被传递给request。
    

    更多关于Actions


    以前,咱们提到一个Action是一个 Request => Result 函数。这不彻底正确。
    让咱们更细致的查看 Action trait:

trait Action[A] extends (Request[A] => Result) {
  def parser: BodyParser[A]
}


    首先咱们看看有个范型的类型 A ,action必须定义一个 BodyParser [A] 。
    Request [A] 被定义为:

trait Request[+A] extends RequestHeader {
  def body: A
}


    A 是request body 的类型。咱们可使用任意Scala类型指定,例如 String,NodeSeq,Array[Byte],JsonValue,或者java.io.File,只要咱们有一个能够处理该类型的body parser。
    总而言之,一个 Action[A] 使用一个 BodyParser[A] 从HTTP请求中,取出一个A类型的值,并构建一个Request[A]对象,转递给action代码。

    默认的 Body Parser

    以前的例子中,咱们从未指定 body parser。那么,它是怎么工做的?若是你不指定 body parser,Play將使用默认的,会將request body 处理为一个 play.api.mvc.AnyContent的 body parser。
    
    该 body parser 检查Content-Type,以决定处理为什么种类型的值:

        text/plain:String
        application/json:JsValue
        text/xml:NodeSeq
        application/form-url-encoded:Map[String,Seq[String]]
        multipart/form-data:MultipartFormData[TemporaryFile]
        任何其它类型:RawBuffer

    例如:

def save = Action { request =>
  val body: AnyContent = request.body
  val textBody: Option[String] = body.asText 
  
  // Expecting text body
  textBody.map { text =>
    Ok("Got: " + text)
  }.getOrElse {
    BadRequest("Expecting text/plain request body")  
  }
}

    指定 body parser

    body parser 的定义位于play.api.mvc.BodyParsers.parse包下。
    例如,建立一个指望text body的action(正如前面的例子):

def save = Action(parse.text) { request => 
   Ok("Got: " + request.body) 
}


    看到代码有多简单了吗?不处理错误,由于parse.text body parser自己就会根据错误发送400 BAD_REQUEST响应。咱们不需在代码中重复检查,咱们能够放心的假定request.body包含了经验证的 String body。

    咱们也可使用:

def save = Action(parse.tolerantText) { request =>
  Ok("Got: " + request.body)
}

该代码并未检查Content-Type,而且经常以String加载body。
提示:
    Tip: There is a tolerant fashion provided for all body parsers included in Play.

    这是另外一个例子,咱们將 request body 保持在一个文件中:

def save = Action(parse.file(to = new File("/tmp/upload"))) { request =>
  Ok("Saved the request content to " + request.body)
}

    结合 body parsers

    以前的例子,全部的 request body 都存储在同一文件中。这会有些问题,不是吗?让咱们编写另外一个自定义 body parser 从Session中提取用户名,为每一个用户分配一个文件:

val storeInUserFile = parse.using { request =>
  request.session.get("username").map { user =>
    file(to = new File("/tmp/" + user + ".upload"))
  }.getOrElse {
    error(Unauthorized("You don't have the right to upload here"))
  }
}

def save = Action(storeInUserFile) { request =>
  Ok("Saved the request content to " + request.body)  
}


    注意:这里咱们并无编写本身的 Body Parser,仅仅是结合现有的。这一般都足够了,它已涵盖了大多数状况。编写一个全新的 Body Parser会在高级主题中提到。
    

    最大内容长度


    基于文本的 body parser(如text,json,xml或者formUrlEncoded)会使用最大内容长度,由于内容必须所有加载到内存中。

    默认最大长度为100KB,但你也能够内嵌指定:

// Accept only 10KB of data.
def save = Action(parse.text(maxLength = 1024 * 10)) { request =>
  Ok("Got: " + text)
}


    提示:最大内容长度能够在application.conf中设置:

    parsers.text.maxLength=128K

    你也能够用 maxLength 包装任何的 body parser:

// Accept only 10KB of data.
def save = Action(maxLength(1024 * 10, parser = storeInUserFile)) { request =>
  Ok("Saved the request content to " + request.body)  
}

    Action组合

    本章介绍一些通用的action功能。

    基本action组合

    让咱们以一个简单的日志装饰功能起步:咱们想记录该action的每次调用。
    
    第一种方法,不定义本身的Action,仅提供一个助手方法构建标准的Action:

def LoggingAction(f: Request[AnyContent] => Result): Action[AnyContent] = {
  Action { request =>
    Logger.info("Calling action")
    f(request)
  }
}

    能够这么使用:

def index = LoggingAction { request =>
  Ok("Hello World")    
}

    示例很简单,但它仅适用于默认的 parse.anyContent body parser,咱们没办法指定自定义的 body parser。咱们固然能够定义另外一个助手方法:

def LoggingAction[A](bp: BodyParser[A])(f: Request[A] => Result): Action[A] = {
  Action(bp) { request =>
    Logger.info("Calling action")
    f(request)
  }
}

    接着:

def index = LoggingAction(parse.text) { request =>
  Ok("Hello World")    
}

    包装现有actions

    另外一种方式是自定义LogginAction,做为其它Action的包装者:

case class Logging[A](action: Action[A]) extends Action[A] {
  
  def apply(request: Request[A]): Result = {
    Logger.info("Calling action")
    action(request)
  }
  
  lazy val parser = action.parser
}

    如今你能够用它包装任何action:

def index = Logging { 
  Action { 
    Ok("Hello World")
  }
}

注意:它將重用包装过的action body parser,你也能够编写:

def index = Logging { 
  Action(parse.text) { 
    Ok("Hello World")
  }
}

另外一种不定义Loggin类而完成一样工做的方式:  

def Logging[A](action: Action[A]): Action[A] = {
  Action(action.parser) { request =>
    Logger.info("Calling action")
    action(request)
  }
}

    一个更复杂的例子

    让咱们看一个更复杂而常见的认证例子。主要问题是咱们须要一个能放行已认证用户,能包装action和body parse,并扮演用户认证的action。

def Authenticated[A](action: User => Action[A]): Action[A] = {
  
  // Let's define an helper function to retrieve a User
  def getUser(request: RequestHeader): Option[User] = {
    request.session.get("user").flatMap(u => User.find(u))
  }
  
  // Wrap the original BodyParser with authentication
  val authenticatedBodyParser = parse.using { request =>
    getUser(request).map(u => action(u).parser).getOrElse {
      parse.error(Unauthorized)
    }          
  }
  
  // Now let's define the new Action
  Action(authenticatedBodyParser) { request =>
    getUser(request).map(u => action(u)(request)).getOrElse {
      Unauthorized
    }
  }
  
}


    你能够这么使用:

def index = Authenticated { user =>
  Action { request =>
    Ok("Hello " + user.name)      
  }
}

    注意:在play.api.mvc.Security.Authenticated包中,已经有一个比该例更好的实现了。

    建立认证Action的另外一种方法

    让咱们看看不包装整个action,不携带认证的body parser,如何重写前一个例子:

def Authenticated(f: (User, Request[AnyContent]) => Result) = {
  Action { request =>
    request.session.get("user").flatMap(u => User.find(u)).map { user =>
      f(user, request)
    }.getOrElse(Unauthorized)      
  }
}

    这样使用:

def index = Authenticated { (user, request) =>
   Ok("Hello " + user.name)    
}


    面对的问题是,你再也不能标记request为implicit。但你可使用柯里化来解决:

def Authenticated(f: User => Request[AnyContent] => Result) = {
  Action { request =>
    request.session.get("user").flatMap(u => User.find(u)).map { user =>
      f(user)(request)
    }.getOrElse(Unauthorized)     
  }
}


    接下你能够:

def index = Authenticated { user => implicit request =>
   Ok("Hello " + user.name)    
}


    另外一种方式(多是最简单的)是建立自定义request子类,如 AuthenticatedRequest (咱们已將两个参数合并为一个参数):

case class AuthenticatedRequest(
  val user: User, request: Request[AnyContent]
) extends WrappedRequest(request)

def Authenticated(f: AuthenticatedRequest => Result) = {
  Action { request =>
    request.session.get("user").flatMap(u => User.find(u)).map { user =>
      f(AuthenticatedRequest(user, request))
    }.getOrElse(Unauthorized)            
  }
}

    接着:

def index = Authenticated { implicit request =>
   Ok("Hello " + request.user.name)    
}

    咱们固然能够按需扩展该例子使其更通用,让其能够指定一个body parser。

case class AuthenticatedRequest[A](
  val user: User, request: Request[A]
) extends WrappedRequest(request)

def Authenticated[A](p: BodyParser[A])(f: AuthenticatedRequest[A] => Result) = {
  Action(p) { request =>
    request.session.get("user").flatMap(u => User.find(u)).map { user =>
      f(AuthenticatedRequest(user, request))
    }.getOrElse(Unauthorized)      
  }
}

// Overloaded method to use the default body parser
import play.api.mvc.BodyParsers._
def Authenticated(f: AuthenticatedRequest[AnyContent] => Result): Action[AnyContent]  = {
  Authenticated(parse.anyContent)(f)
}
相关文章
相关标签/搜索