概念
如果说先前文章介绍Nest模块化是以静态的方式了解Nest,那么接下去部分让我们就从运行态时Nest的动态过程来了解。更早的文章曾经简单介地绍了Nest处理请求和相应的过程中的几个关键概念,在接下来的几篇文章中,让我们更深入了解Nest的处理过程。
Express中间件
中间件本是Express很强大的一个功能,提供了丰富的扩展性。任何一个中间件都可以对request和response对象访问和存取,再用next()函数将控制权交给下一个中间件。Nest的中间件基本上等同于Express(因为是继承了Express),对于Express中间件,其官方文档给予的定义是:
- 可以执行任意代码
- 修改request和response对象
- 终止请求响应过程
- 通过next()函数调用下一个中间件方法
- 如果不是最后一个环节,则必须执行next()来传递控制权
提示:所有的中间件,都是在相关路径被映射到控制器执行代码之前执行。
Express的中间件大致可以分为5种:
- 全局中间件:不论执行什么,都会被执行到;
- 路由中间件:某个路由下属的中间件;
- 错误处理中间件:专门处理异常,中间件入口第一个参数是错误对象;
- 内建中间件:集成在Express内部的功能;
- 三方中间件:第三方开发的中间件,例如Cookie转换等;
Nest中间件
Nest的中间件基本继承了来自于Express的理念。其过程如下图:
因为Nest将Express中的Router抽象成为Controller,注册中间件的过程放在了Module中。因此,Nest相比Express,对程序是中间件还是Controller(在Express中对应Router)有着非常明确的边界。
创建一个中间件
利用命令行快捷创建中间件:
nest g mi Logger
类(形态的)中间件
默认情况下,Nest会以类的方式创建一个中间件对象。
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: () => void) {
console.log(
`The browser's accepted header is : ${req.headers['accept-language']}`,
);
next();
}
}
函数(形态的)中间件
除了以类方式声明的中间件,Nest也支持以函数方式定义中间件,以下代码等同上述的代码。
export function logger(req: Request, res: Response, next: NextFunction) {
console.log(
`The browser's accepted language is : ${req.headers['accept-language']}`,
);
next();
};
中间件注册
最后将中间件注册到任意模块中即可完成中间件的整个编写与注册过程。
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggerMiddleware).forRoutes('/');
}
}
curl localhost:3000 -H "accept-language: Chinese"
The browser's accepted language is : Chinese
Hello world!
深入了解中间件
NestMiddleware接口
在Nest,中间件同样也需要由IoC容器来管理,这也就是如同其他Provider一样,不论是类或者是函数,都需要用一个@Injectable()装饰器处理。如果中间件是一个类,那么就需要实现NestMiddleware接口,在Nest源码中,这个接口只有一个use函数需要定义。
export interface NestMiddleware<TRequest = any, TResponse = any> {
use(req: TRequest, res: TResponse, next: () => void): any;
}
其中,req和res分别对应请求和响应对象。可以看到种类被定义为any。如果希望有强类型下的代码自动补全,则需要手动重新指定类型(如上面的例子,指定Express的请求和响应对象)。
next()
next方法是将当前控制权递交给下一个。需要留意:执行next时,不能有参数,唯一的参数是当且仅当发生异常情况时使用。例如,将代码改为:
export class ReqMiddleware implements NestMiddleware {
use(req: ERequest, res: any, next: (T?: any) => void) {
if (req.query.key) {
console.log(req.query.key);
next();
} else next('Some error occurred');
}
}
[Nest] 13699 - 01/27/2021, 4:10:53 PM [ExceptionsHandler] Some error occurred +3796ms
如何在多个中间件之间传递参数呢?根据Express官方文档的建议,可以利用res对象。例如:
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: () => void) {
req.query.key = req.headers['accept-language'];
next();
}
}
前文在讲述控制器章节时也提到了可以在控制器的函数中使用next()方法。本文上面在介绍Express时提到,在控制器中使用nest()就是相当于将控制器作为一个路由中间件来对待。
处理多个中间件与复杂路由
默认情况下,通过命令行的创建的中间件文件会放在项目的src目录下。但是中间件程序无论是被注册在哪个module中,其效果都是一样的。并且通过forRoutes和exclude方法来设定适用于它的请求路径。
被注册进的模块需要实现NestModule接口;而接口只有一个方法,重写configure函数;函数中仅有一个参数MiddlewareConsumer接口的consumer。
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggerMiddleware).forRoutes('/');
consumer.apply(ReqMiddleware).forRoutes('/user');
}
}
链式注册(流式接口)
根据源码信息可以得知:MiddlewareConsumer只有一个apply方法,返回MiddlewareConfigProxy接口,接口中包含两个方法exclude和forRouters,分别代表排除某些路由和包含某些路由。exclude又返回了MiddlewareConfigProxy,而forRouters返回了MiddlewareConsumer。这样便形成了链式调用。上面的代码就可以改为:
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('/')
.apply(ReqMiddleware)
.forRoutes('/user');
}
}
可以将多个中间件一同注册到模块中,如果他们有相同的作用域,代码还可以改为:
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware,ReqMiddleware)
.forRoutes('/');
}
}
关于参数类型RouteInfo,可以精确定位到Controller中的具体方法。
export interface RouteInfo {
path: string;
method: RequestMethod;
}
上面的注册过程还可以改为:
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes({path:'/',method:RequestMethod.GET})
.apply(ReqMiddleware)
.forRoutes({path:'/user',method:RequestMethod.GET});
}
}
如果是字符串类型的参数,可以支持通配符甚至是正则表达式(如果希望使用正则表达式需要额外安装组件)