8、Nest.js 中的拦截器
什么是拦截器(Interceptor)?
拦截器就是使用 @Injectable 修饰并且实现了 NestInterceptor 接口的类。
在Nest中拦截器是实现 AOP 编程的利器。
传统 MVC 应用
Nest 默认将控制器处理程序的返回值解析成 JSON(纯字符串不解析),我们如何在 Nest 中实现传统 MVC 程序呢? 即返回一个使用模板引擎渲染的视图。
首先选择一个模板引擎,这里使用的 art-template:
npm install --save art-template
npm install --save express-art-template
修改我们的入口程序:
src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from 'app.module';
import { HttpExceptionFilter } from 'common/filters/http-exception.filter';
import { ApiParamsValidationPipe } from 'common/pipes/api-params-validation.pipe';
import { static as resource } from 'express';
import * as art from 'express-art-template';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 处理静态文件
app.use('/static', resource('resource'));
// 指定模板引擎
app.engine('art', art);
// 设置模板引擎的配置项
app.set('view options', {
debug: process.env.NODE_ENV !== 'production',
minimize: true,
rules: [
{ test: /<%(#?)((?:==|=#|[=-])?)[ \t]*([\w\W]*?)[ \t]*(-?)%>/ },
{ test: /{%([@#]?)[ \t]*(\/?)([\w\W]*?)[ \t]*%}/ }
]
});
// 设置视图文件的所在目录
app.setBaseViewsDir('resource/views');
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalPipes(new ApiParamsValidationPipe());
await app.listen(3000);
}
bootstrap();
目前程序已经可以处理 resource 目录下的静态文件了。
resource 目录是和 src 目录平级的,看起来像下面这样:
image.png
新建一个 home 模块:
image.png在控制器中拿到 response 对象然后调用它的 render 函数就可以返回渲染后的 html:
src/home/home.controller.ts
import { Controller, Get, Res } from '@nestjs/common';
@Controller()
export class HomeController {
@Get()
index(@Res() res) {
return res.render('home/home.art');
}
}
这样做太丑陋了,我们的思路是,只要控制器返回的是一个 View 类型,则渲染视图,否则使用默认的解析逻辑。
新建一个 View.ts:
src/common/libs/View.ts
export class View {
// 视图的名称
public name: string
// 要渲染的数据源
public data: any
constructor(name: string, data: any = {}) {
this.name = name;
this.data = data;
}
}
HomeController 中就是很简单的返回一个 View 的实例:
src/home/home.controller.ts
import { Controller, Get } from '@nestjs/common';
import { View } from 'common/libs/view';
@Controller()
export class HomeController {
@Get()
index(): View {
// 返回首页的视图
return new View('home/home.art');
}
}
拦截器登场
src/common/Interceptors/view.interceptor.ts
import { ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import * as util from 'util';
import { View } from '../libs/view';
@Injectable()
export class ViewInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
call$: Observable<any>,
): Observable<any> {
// 拿到 response 对象
const response = context.switchToHttp().getResponse();
// 将 render 回调函数转成一个 promisify 然后绑定执行的上下文
const render = util.promisify(response.render.bind(response));
// 请自行了解什么是 Rxjs
return call$.pipe(map(async value => {
if (value instanceof View) {
// 返回渲染后的 html
value = await render(value.name, value.data);
}
return value;
}))
}
}
这里对于一些基础知识如 promisify, bind 等不做介绍了。每个拦截器都有 intercept() 方法,这个方法有2个参数。 第一个是 ExecutionContext 实例它继承自 ArgumentsHost,可以根据上下文的不同, 拿到不同的对象, 如果是 HTTP 请求, 则这个对象中包含 getRequest() 和 getResponse(),如果是 websockets 则包含 getData() 和 getClient()。第二个参数是一个 Observable 流,需要读者有一定的 Rxjs 知识。 Nest 使用 call$ 的 subscribe 结果作为最终的响应。response 的 render 函数第三个参数是一个回调函数,如果不传入 express 会直接响应输出(这样会导致重复设置响应),如果传入了则可以获取到 模板引擎渲染后的 字符串,在这里我们需要将这个回调函数 promisify 化,拿到响应然后使用 map 操作符改变流的结果,最终的响应就是模板引擎渲染后的字符串。
最后
只在 home 模块中使用 View 拦截器:
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { ViewInterceptor } from 'common/interceptors/view.interceptor';
import { HomeController } from './home.controller';
@Module({
controllers: [HomeController],
providers: [
{
provide: APP_INTERCEPTOR,
useClass: ViewInterceptor,
},
]
})
export class HomeModule {}
同理 异常过滤器、管道 等等 也可以只作用在特定模块上,使用不同的常量就可以了。
这里使用的是 APP_INTERCEPTOR 来标识提供者是一个 拦截器,如果是管道则用 APP_PIPE。
使用拦截器还可以做更多的事情,例如:记录日志、 返回缓存 等等。
上一篇:7、Nest.js 中的类验证器
下一篇:9、Nest.js 中的看守器