美文网首页Nest.js前端
8、Nest.js 中的拦截器

8、Nest.js 中的拦截器

作者: RoyLin1996 | 来源:发表于2018-08-14 14:39 被阅读483次

    什么是拦截器(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 中的看守器

    相关文章

      网友评论

        本文标题:8、Nest.js 中的拦截器

        本文链接:https://www.haomeiwen.com/subject/efmqbftx.html