8、Nest.js 中的拦截器

什么是拦截器(Interceptor)?

拦截器就是使用 @Injectable 修饰并且实现了 NestInterceptor 接口的类。
在Nest中拦截器是实现 AOP 编程的利器。

传统 MVC 应用

Nest 默认将控制器处理程序的返回值解析成 JSON(纯字符串不解析),我们如何在 Nest 中实现传统 MVC 程序呢? 即返回一个使用模板引擎渲染的视图。

首先选择一个模板引擎,这里使用的 art-template

chart.png

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 中的看守器

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容