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

原创
2012/03/19 14:54
阅读数 6.5K

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

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

     Actions, Controllers and Results

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

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


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

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

Action {
  Ok("Hello world")
}

    这是创建Action最简单的方式,但我们无法获取request对象。通常Action中都需要访问request对象。
    
    看看第二个Action,包含了参数Request => Result :

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

    标记 request 参数为 隐式 通常都很有用,可供其它API隐式的使用:

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


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

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)
}

展开阅读全文
加载中
点击加入讨论🔥(5) 发布并加入讨论🔥
打赏
5 评论
10 收藏
5
分享
返回顶部
顶部