跳转至

处理请求:基本原理

在Aqueduct中,HTTP请求和响应是RequestResponse的实例。 对于应用程序收到的每个HTTP请求,都会创建一个 Request 实例。 必须为每个请求创建一个 Response 。响应是由控制器对象创建的。本指南讨论了控制器的行为和初始化。你可以阅读更多关于请求和响应对象的信息这里

概述

控制器是Aqueduct应用程序的基本构件。控制器以某种方式处理一个HTTP请求。例如,控制器可以返回200 OK响应,并带有JSON编码的城市名称列表。 控制器还可以检查请求,以确保其授权标头中具有正确的凭据。

控制器被链接到一起,将它们的行为组成一个通道。一个通道通过按顺序执行每个控制器的行为来处理一个请求。例如,一个通道可能会验证请求的凭据,然后通过组成两个分别执行这些操作的控制器来返回城市名称列表。

Aqueduct Channel

你对控制器进行子类化,提供其请求处理逻辑,并且Aqueduct中有常见的控制器子类供你使用。

链接控制器

控制器通过其link方法链接在一起。 此方法采用闭包形式,该闭包返回通道中的下一个控制器。 下图显示了一个由两个控制器组成的通道:

final controller = VerifyController();
controller.link(() => ResponseController());

在上面,VerifyController链接ResponseController。 验证控制器处理的请求可以响应该请求,也可以让响应控制器进行处理。 如果验证控制器发送了响应,则响应控制器将永远不会收到请求。 可以链接任意数量的控制器,但是链接的最后一个控制器必须响应请求。 始终响应请求的控制器称为“端点控制器”。 中间件控制器验证或修改请求,并且通常仅在遇到错误时才响应。

链接发生在应用通道中,并且是在应用程序启动时最终确定的(也就是说,一旦你设置了控制器,一旦应用程序开始接收请求,控制器就不能更改)。

子类化控制器以处理请求

每个Controller都实现了它的handle方法来处理请求。你在你的控制器中重写这个方法,为你的控制器提供逻辑。下面是一个端点控制器的例子,因为它总是发送响应:

class NoteController extends Controller {
  @override
  Future<RequestOrResponse> handle(Request request) async {
    final notes = await fetchNotesFromDatabase();

    return Response.ok(notes);
  }
}

这个handle方法返回一个Response对象。 每当控制器返回响应时,Aqueduct都会将响应发送到客户端并终止请求,这样就没有其他控制器可以处理它。

中间件控制器在可以提供响应或出现错误情况时返回响应。 例如,如果请求的凭据无效,则Authorizer控制器将返回401 Unauthorized响应。 要使请求传递到下一个控制器,必须返回请求对象。

例如,Authorizer的伪代码如下所示:

class Authorizer extends Controller {
  @override
  Future<RequestOrResponse> handle(Request request) async {
    if (isValid(request)) {
      return request;
    }

    return Response.unauthorized();
  }
}

端点控制器

在大多数情况下,可以通过将ResourceController子类化来创建端点控制器。 该控制器允许您在一个控制器中声明多个处理程序方法,以更好地组织逻辑。 例如,一种方法可能处理POST请求,而另一种方法处理GET请求。

使用中间件修改响应

中间件控制器可以向请求添加响应修改器。当端点控制器最终创建一个响应时,这些修饰符会在响应发送之前应用到响应中。通过在请求上调用addResponseModifier来添加修改器:

class Versioner extends Controller {
  Future<RequestOrResponse> handle(Request request) async {
    request.addResponseModifier((response) {
      response.headers["x-api-version"] = "2.1";
    });

    return request;
  }
}

任意数量的控制器都可以向一个请求添加响应修改器;它们将按照添加的顺序进行处理。响应修改器在响应体被编码之前应用,允许对响应体对象进行操作。无论端点控制器产生的响应是什么,响应修改器都会被调用。如果在添加响应修改器时抛出了一个未捕获的错误,则不会调用任何剩余的响应修改器,并发送一个500 Server Error响应。

链接函数

对于简单的行为,可以将具有与 handle相同签名的函数链接到控制器。

  router
    .route("/path")
    .linkFunction((req) async => req);
    .linkFunction((req) async => Response.ok(null));

链接函数的行为与Controller.handle相同:它可以返回请求或响应,自动处理异常,并可以链接控制器(和函数)。

控制器实例化和回收

重要的是要理解为什么 link需要一个闭包,而不是控制器对象。Aqueduct是一个面向对象的框架。对象既有状态,也有行为。一个应用程序会收到多个请求,这些请求将由同一类型的控制器处理。如果一个可突变的控制器对象被重复使用来处理多个请求,那么它可能会在请求之间保留一些状态。这将产生难以调试的问题。

大多数控制器都是不可变的,换句话说,它们的所有属性都是最终属性,并且没有setters。 这(主要是)确保控制器不会在请求之间更改行为。 当一个控制器是不可变的,link闭包被调用一次来创建和链接控制器对象,然后闭包被丢弃。 相同的控制器对象将被重用于每个请求。

控制器可以是可变的,但要注意的是,它们不能重复用于多个请求。 例如,ResourceController可以具有绑定到请求值的属性,因此这些属性将更改,并且必须为每个请求创建一个新实例。

可变的Controller子类必须实现Recyclable <T>。 将为每个请求调用link闭包,从而创建一个可回收控制器的新实例来处理该请求。 如果控制器的初始化过程很昂贵,则可以通过实现Recyclable <T>中的方法,一次计算出初始化结果,并将其重新用于每个控制器实例。

class MyControllerState {
  dynamic stuff;
}

class MyController extends Controller implements Recyclable<MyControllerState> {
  @override
  MyControllerState get recycledState => expensiveCalculation();

  @override
  void restore(MyControllerState state) {
    _restoredState = state;
  }

  MyControllerState _restoredState;

  @override
  FutureOr<RequestOrResponse> handle(Request request) async {
    /* use _restoredState */
    return new Response.ok(...);
  }
}

第一次链接控制器时,将调用一次recycledState的 getter。 可回收控制器的每个新实例在处理请求之前都会调用其restore方法,并且将recycledState返回的数据作为参数传递。 例如,ResourceController对其操作方法进行 "编译"。 编译后的产物以回收状态存储,以便将来的实例可以更有效地绑定请求数据。

异常处理

如果在处理请求过程中抛出异常或错误,当前处理请求的控制器将捕捉到它。对于大多数被捕获的值,控制器将发送一个500服务器响应。异常或错误的详细信息将被logged记录下来,并将请求从通道中删除(不会传递给链接的控制器)。

这是所有抛出的值(ResponseHandlerException除外)的默认行为。

抛出Responses

A Response can be thrown at any time; the controller handling the request will catch it and send it to the client. This completes the request. This might not seem useful, for example, the following shows a silly use of this behavior:

class Thrower extends Controller {
  @override
  Future<RequestOrResponse> handle(Request request) async {
    if (!isForbidden(request)) {
      throw new Response.forbidden();
    }

    return Response.ok(null);
  }
}

抛出HandlerExceptions

Exceptions can implement HandlerException to provide a response other than the default when thrown. For example, an application that handles bank transactions might declare an exception for invalid withdrawals:

enum WithdrawalProblem {
  insufficientFunds,
  bankClosed
}
class WithdrawalException implements Exception {
  WithdrawalException(this.problem);

  final WithdrawalProblem problem;
}

Controller code can catch this exception to return a different status code depending on the exact problem with a withdrawal. If this code has to be written in multiple places, it is useful for WithdrawalException to implement HandlerException. An implementor must provide an implementation for response:

class WithdrawalException implements HandlerException {
  WithdrawalException(this.problem);

  final WithdrawalProblem problem;

  @override
  Response get response {
    switch (problem) {
      case WithdrawalProblem.insufficientFunds:
        return new Response.badRequest(body: {"error": "insufficient_funds"});
      case WithdrawalProblem.bankClosed:
        return new Response.badRequest(body: {"error": "bank_closed"});
    }
  }
}

CORS Headers and Preflight Requests

Controllers have built-in behavior for handling CORS requests. They will automatically respond to OPTIONS preflight requests and attach CORS headers to any other response. See the chapter on CORS for more details.