文件

§動作組成

本章介紹定義一般動作功能的數種方式。

§自訂動作建構器

我們先前看過,有許多方式宣告動作,包括使用要求參數、不使用要求參數、使用主體剖析器等。事實上,還有更多方式,我們會在非同步程式設計章節中看到。

這些建構動作的方法,實際上都是由名為ActionBuilder的特質定義,而我們用來宣告動作的Action物件,只是這個特質的一個執行個體。透過實作自己的ActionBuilder,您可以宣告可重複使用的動作堆疊,然後用來建構動作。

我們從一個簡單的記錄裝飾器範例開始,我們希望記錄每次呼叫這個動作。

第一個方式是在 invokeBlock 方法中實作此功能,此方法會針對由 ActionBuilder 建置的每個動作呼叫

import play.api.mvc._

class LoggingAction @Inject() (parser: BodyParsers.Default)(implicit ec: ExecutionContext)
    extends ActionBuilderImpl(parser)
    with Logging {
  override def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
    logger.info("Calling action")
    block(request)
  }
}

現在我們可以在控制器中使用 依賴注入 來取得 LoggingAction 的執行個體,並以我們使用 Action 的相同方式使用它

class MyController @Inject() (loggingAction: LoggingAction, cc: ControllerComponents)
    extends AbstractController(cc) {
  def index = loggingAction {
    Ok("Hello World")
  }
}

由於 ActionBuilder 提供建置動作的所有不同方法,這也適用於例如宣告自訂主體剖析器

def submit: Action[String] = loggingAction(parse.text) { request =>
  Ok("Got a body " + request.body.length + " bytes long")
}

§組合動作

在大部分應用程式中,我們會希望有多個動作建置器,有些執行不同類型的驗證,有些提供不同類型的通用功能等。在這種情況下,我們不會希望針對每種類型的動作建置器重新撰寫我們的記錄動作程式碼,我們會希望以可重複使用的形式定義它。

可重複使用的動作程式碼可透過包裝動作來實作

import play.api.mvc._

case class Logging[A](action: Action[A]) extends Action[A] with play.api.Logging {
  def apply(request: Request[A]): Future[Result] = {
    logger.info("Calling action")
    action(request)
  }

  override def parser           = action.parser
  override def executionContext = action.executionContext
}

我們也可以使用 Action 動作建置器來建置動作,而無須定義我們自己的動作類別

import play.api.mvc._

def logging[A](action: Action[A]) = Action.async(action.parser) { request =>
  logger.info("Calling action")
  action(request)
}

動作可以使用 composeAction 方法混合到動作建置器中

class LoggingAction @Inject() (parser: BodyParsers.Default)(implicit ec: ExecutionContext)
    extends ActionBuilderImpl(parser) {
  override def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
    block(request)
  }
  override def composeAction[A](action: Action[A]): Logging[A] = new Logging(action)
}

現在建置器可以使用與之前相同的方式

def index = loggingAction {
  Ok("Hello World")
}

我們也可以在沒有動作建置器的情況下混合包裝動作

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

§更複雜的動作

到目前為止,我們僅顯示完全不影響要求的動作。當然,我們也可以讀取並修改輸入的要求物件

import play.api.mvc._
import play.api.mvc.request.RemoteConnection

def xForwardedFor[A](action: Action[A]) = Action.async(action.parser) { request =>
  val newRequest = request.headers.get("X-Forwarded-For") match {
    case None => request
    case Some(xff) =>
      val xffConnection = RemoteConnection(xff, request.connection.secure, None)
      request.withConnection(xffConnection)
  }
  action(newRequest)
}

注意:Play 已內建支援 X-Forwarded-For 標頭。

我們可以封鎖要求

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

def onlyHttps[A](action: Action[A]) = Action.async(action.parser) { request =>
  request.headers
    .get("X-Forwarded-Proto")
    .collect {
      case "https" => action(request)
    }
    .getOrElse {
      Future.successful(Forbidden("Only HTTPS requests allowed"))
    }
}

最後,我們也可以修改傳回的結果

import play.api.mvc._

def addUaHeader[A](action: Action[A]) = Action.async(action.parser) { request =>
  action(request).map(_.withHeaders("X-UA-Compatible" -> "Chrome=1"))
}

§不同的要求類型

動作組成允許您在 HTTP 要求和回應層級執行額外的處理,但您通常會想要建構資料轉換的管線,以新增背景或對要求本身執行驗證。ActionFunction 可視為要求上的函式,參數化輸入要求類型和傳遞到下一層的輸出類型。每個動作函式可以代表模組化處理,例如驗證、物件的資料庫查詢、權限檢查或您希望在動作間組成和重複使用的其他作業。

有幾個預先定義的特質實作 ActionFunction,對於不同類型的處理很有用

您也可以透過實作 invokeBlock 方法來定義您自己的任意 ActionFunction。通常,將輸入和輸出類型設為 Request 的實例(使用 WrappedRequest)很方便,但這並非絕對必要。

§驗證

動作函式最常見的用例之一是驗證。我們可以輕鬆實作我們自己的驗證動作轉換器,以從原始要求中判斷使用者,並將其新增到新的 UserRequest。請注意,這也是一個 ActionBuilder,因為它將簡單的 Request 作為輸入

import play.api.mvc._

class UserRequest[A](val username: Option[String], request: Request[A]) extends WrappedRequest[A](request)

class UserAction @Inject() (val parser: BodyParsers.Default)(implicit val executionContext: ExecutionContext)
    extends ActionBuilder[UserRequest, AnyContent]
    with ActionTransformer[Request, UserRequest] {
  def transform[A](request: Request[A]) = Future.successful {
    new UserRequest(request.session.get("username"), request)
  }
}

Play 也提供內建的驗證動作建構器。有關這方面的資訊以及如何使用它,請參閱 這裡

注意:內建的驗證動作建構器只是一個方便的輔助工具,用於將實作驗證所需的程式碼,在簡單情況下減至最少,它的實作與上述範例非常類似。

由於撰寫自己的驗證輔助工具很簡單,因此我們建議在內建輔助工具不符合您的需求時,這麼做。

§將資訊新增至要求

現在讓我們考慮一個使用 Item 類型物件的 REST API。/item/:itemId 路徑下可能有很多路由,而這些路由都需要查詢項目。在這種情況下,將此邏輯放入動作函式中可能會很有用。

首先,我們將建立一個要求物件,將 Item 新增至我們的 UserRequest

import play.api.mvc._

class ItemRequest[A](val item: Item, request: UserRequest[A]) extends WrappedRequest[A](request) {
  def username = request.username
}

現在我們將建立一個動作精煉器,用於查詢該項目並傳回錯誤 (Left) 或新的 ItemRequest (Right)。請注意,此動作精煉器是在方法內定義的,該方法會取得項目的 ID

def ItemAction(itemId: String)(implicit ec: ExecutionContext) = new ActionRefiner[UserRequest, ItemRequest] {
  def executionContext = ec
  def refine[A](input: UserRequest[A]): Future[Either[Status, ItemRequest[A]]] = Future.successful {
    ItemDao
      .findById(itemId)
      .map(new ItemRequest(_, input))
      .toRight(NotFound)
  }
}

§驗證要求

最後,我們可能需要一個動作函式,用於驗證要求是否應繼續。例如,我們可能想要檢查 UserAction 的使用者是否有權限存取 ItemAction 的項目,如果沒有,則傳回錯誤

def PermissionCheckAction(implicit ec: ExecutionContext) = new ActionFilter[ItemRequest] {
  def executionContext = ec
  def filter[A](input: ItemRequest[A]) = Future.successful {
    if (!input.item.accessibleByUser(input.username))
      Some(Forbidden)
    else
      None
  }
}

§將所有內容組合在一起

現在,我們可以使用 andThen 將這些動作函式串連在一起 (從 ActionBuilder 開始) 以建立動作

def tagItem(itemId: String, tag: String)(implicit ec: ExecutionContext): Action[AnyContent] =
  userAction.andThen(ItemAction(itemId)).andThen(PermissionCheckAction) { request =>
    request.item.addTag(tag)
    Ok("User " + request.username + " tagged " + request.item.id)
  }

Play 也提供 全域篩選器 API ,這對於全域橫切關注很有用。

§動作組合與主體剖析的互動

預設情況下,主體剖析會在動作組合發生之前進行,表示您可以在每個動作中透過 request.body() 存取已剖析的請求主體。不過,有些使用案例會在透過動作組合定義的部分(或全部)動作處理完畢之後,再延後主體剖析。例如

當然,當延後主體剖析時,在主體剖析發生之前執行的動作中,請求主體尚未剖析,因此 request.body() 會傳回 null

您可以在 conf/application.conf 中啟用延後主體剖析

play.server.deferBodyParsing = true

請注意,就像所有 play.server.* 設定金鑰一樣,當在 DEV 模式下執行時,Play 偵測不到此設定,僅在 PROD 模式下才會偵測到。若要在 DEV 模式中設定此設定,您必須在 build.sbt 中設定它

PlayKeys.devSettings += "play.server.deferBodyParsing" -> "true"

您不必啟用延後主體剖析,只要使用路由修改器 deferBodyParsing,就能針對特定路由啟用它

+ deferBodyParsing
POST    /      controllers.HomeController.uploadFileToS3

反之亦然。如果您啟用延後主體剖析,您可以使用路由修改器 dontDeferBodyParsing,針對特定路由停用它

+ dontDeferBodyParsing
POST    /      controllers.HomeController.processUpload

現在,主體可以透過呼叫play.api.mvc.BodyParser.parseBody進行剖析

def home(): Action[AnyContent] = Action.async(parse.default) { implicit request: Request[AnyContent] =>
  {
    // When body parsing was deferred, the body is not parsed here yet, so following will be true:
    //  - request.body == null
    //  - request.attrs.contains(play.api.mvc.request.RequestAttrKey.DeferredBodyParsing)
    // Do NOT rely on request.hasBody because it has nothing to do if a body was parsed or not!
    BodyParser.parseBody(
      parse.default,
      request,
      (req: Request[AnyContent]) => {
        // The body is parsed here now, therefore:
        //  - request.body has a value now
        //  - request.attrs does not contain RequestAttrKey.DeferredBodyParsing anymore
        Future.successful(Ok)
      }
    )
  }
}

下一步:內容協商


在此文件發現錯誤?此頁面的原始程式碼可以在 這裡 找到。在閱讀 文件指南 後,請隨時提交拉取請求。有問題或建議要分享?前往 我們的社群論壇 與社群展開對話。