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)
}
相關文章
相關標籤/搜索