处理请求:基本原理
在Aqueduct中,HTTP请求和响应是Request
和Response
的实例。 对于应用程序收到的每个HTTP请求,都会创建一个 Request
实例。 必须为每个请求创建一个 Response
。响应是由控制器对象创建的。本指南讨论了控制器的行为和初始化。你可以阅读更多关于请求和响应对象的信息这里。
概述
控制器是Aqueduct应用程序的基本构件。控制器以某种方式处理一个HTTP请求。例如,控制器可以返回200 OK响应,并带有JSON编码的城市名称列表。 控制器还可以检查请求,以确保其授权标头中具有正确的凭据。
控制器被链接到一起,将它们的行为组成一个通道。一个通道通过按顺序执行每个控制器的行为来处理一个请求。例如,一个通道可能会验证请求的凭据,然后通过组成两个分别执行这些操作的控制器来返回城市名称列表。
你对控制器进行子类化,提供其请求处理逻辑,并且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记录下来,并将请求从通道中删除(不会传递给链接的控制器)。
这是所有抛出的值(Response
和HandlerException
除外)的默认行为。
抛出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
Controller
s 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.