从0到1实现NestJS的服务端应用——初级篇

1,160 阅读15分钟

介绍

在前面的介绍中,我们已经懂了nest.js的一个基础应用,并且能使用nest.js连接数据库进行操作。那么,接下来就让我们更进一步的使用nest.js吧。

依赖注入

依赖注入是一种将我们需要的实例对象委托给控制反转容器,然后让这个容器提供给我们使用的技术。在nest.js中,这个控制反转的容器就是nest.js本身,也就是我们需要的实例由nest.js来委托提供。

流程

实现注入这个功能,通常需要三个步骤。如下:

  1. 首先通过@Injectable()装饰器来声明一个由nest.js容器管理的类,被管理的这个类在nest.js中就是一个提供者,如下面代码所示:
import { Injectable } from '@nestjs/common';

@Injectable()
export class GoodsService {
  getHello() {
    return 'goods hello.';
  }
}

因为通过了@Injectable()装饰器的修饰,所以GoodsService类已经被nest.js容器进行了管理。

  1. 我们怎么来使用nest.js容器中管理的类呢?方法如下:
import { GoodsService } from './goods.service';
import { Controller, Get, Inject } from '@nestjs/common';

@Controller('goods')
export class GoodsController {
  constructor(
    private readonly goodsService: GoodsService,
  ) {}

  @Get()
  hello() {
    return this.goodsService.getHello(),
  }
}

当我们在构造函数中声明了参数时,例如以下代码:

private readonly goodsService: GoodsService,

此时,我们定义了一个GoodsService类型的变量,在nest.js中该行为视作向控制反转容器请求注入对应提供者的实例对象。

  1. 那么,当我们向控制反转容器请求注入对应的实例对象时nest.js如何知道提供哪一个呢?这里,就需要在module里面进行提供者的注册,代码如下:
import { Injectable, Module } from '@nestjs/common';
import { GoodsController } from './goods.controller';
import { GoodsService } from './goods.service';
@Module({
  controllers: [GoodsController],
  providers: [GoodsService],
})
export class GoodsModule {}

通过providers属性,就表示在该模块中注册了一个由nest.js容器管理的提供者GoodsService类。
从而当我们在该模块的控制器中访问时,nest.js能给我们正确的注入对应实例对象。

使用

现在我们已经知道了依赖注入在nest.js中的一个基本流程,现在我们来看看nest.js中给我们提供的几种写法。

useClass

nest.js中注册提供者时,提供了useClass属性来表示需要注册的类,上面的providers: [GoodsService]其实就是该属性的一个缩写,其完整写法应该如下:

providers: [
    {
      provide: GoodsService,
      useClass: GoodsService,
    }
]

上面表示注册了一个GoodsSerivce类,并且指定了该类的实际注入内容,这里也为GoodsSerivce类。

好的,我们修改之前的写法重启服务,请求接口发现如下图一样一切正常,这就表示我们的注入是成功。

image.png

useValue

除了使用useClass注册容器管理的提供类以外,在nest.js中还给我们提供了useValue的方式来提供管理,方法如下:

import { Injectable, Module } from '@nestjs/common';
import { GoodsController } from './goods.controller';
import { GoodsService } from './goods.service';

@Module({
  controllers: [GoodsController],
  providers: [
    {
      provide: GoodsService,
      useClass: GoodsService,
    },
    {
      provide: 'GoodsType',
      useValue: ['食品', '家居', '玩具'],
    }
  ],
})
export class GoodsModule {}

上面我们通过useValue: ['食品', '家居', '玩具']方式来注册了一个数组对象,里面包含了商品的类型,那么如何使用呢?
对于使用useVlaue来注册的自定义提供者,我们需要通过如下方式注入:

import { GoodsService } from './goods.service';
import { Controller, Get, Inject } from '@nestjs/common';

@Controller('goods')
export class GoodsController {
  constructor(
    private readonly goodsService: GoodsService,
    @Inject('GoodsType') private readonly goodsType: string[],
  ) {}

  @Get()
  hello() {
    return {
      type: this.goodsType,
      greet: this.goodsService.getHello(),
    };
  }
}

通过@Inject('GoodsType')的声明后nest.js容器就知道需要给我们注入对应的自定义的GoodsType提供者了,尝试调接口发现返回如下:

image.png

说明我们注册的提供者已经被成功注入了。

useFactory

除了以上两种以外注册方式以外,在nest.js中还给我们提供了另一种更为强大的注册方式,那就是useFactory方式。
通过useFactory方式注册提供者时,我们还可以在注册过程朱注入并引用其他的提供者来使用,代码如下:

import { Injectable, Module } from '@nestjs/common';
import { GoodsController } from './goods.controller';
import { GoodsService } from './goods.service';

@Injectable()
class MockService {
  create() {
    return new GoodsService();
  }
}

@Module({
  controllers: [GoodsController],
  providers: [
    {
      provide: GoodsService,
      useClass: GoodsService,
    },
    {
      provide: 'GoodsType',
      useValue: ['食品', '家居', '玩具'],
    },
    MockService,
    {
      provide: 'MockGoodsSerivce',
      inject: [MockService],
      useFactory: (mockService: MockService) => mockService.create(),
    },
  ],
})
export class GoodsModule {}

在上面的代码中,我们首先通过定义了一个MockService类,并且使用@Injectable()装饰器标明其为一个提供者并由nest.js来管理。

然后,我们需要在providers中注册MockService类。当注册完成后,我们就可以通过useFactory的方式来注入并使用了,过程简单如下:

  • inject: 需要注入的对象
  • useFactory: (mockService: MockService) => mockService.create() // 查询inject是否有注入对应的对象并返回实例对象

完成注册后,我们就可以在其他服务中使用了,代码如下:

import { GoodsService } from './goods.service';
import { Controller, Get, Inject } from '@nestjs/common';

@Controller('goods')
export class GoodsController {
  constructor(
    private readonly goodsService: GoodsService,
    @Inject('GoodsType') private readonly goodsType: string[],
    @Inject('MockGoodsSerivce') private readonly mockService: GoodsService,
  ) {}

  @Get()
  hello() {
    return {
      type: this.goodsType,
      greet: this.goodsService.getHello(),
      mockGreet: this.mockService.getHello(),
    };
  }
}

尝试调接口,发现返回如下:

image.png

这就表明了我们注册的提供者都是成功的。

Async

现在,我们来看看另一种常见的场景——异步。
JavaScript编程中,我们很容易遇见请求接口这类的场景,假如当我们需要注册的变量是通过异步请求获取到的,那么我们再看看注入的结果会正确吗?代码如下:

import { Injectable, Module } from '@nestjs/common';
import { GoodsController } from './goods.controller';
import { GoodsService } from './goods.service';

@Injectable()
class MockService {
  create() {
    return new GoodsService();
  }
}

@Module({
  controllers: [GoodsController],
  providers: [
    {
      provide: GoodsService,
      useClass: GoodsService,
    },
    {
      provide: 'GoodsType',
      useValue: ['食品', '家居', '玩具'],
    },
    {
      provide: 'MockGoodsSerivce',
      inject: [MockService],
      useFactory: (mockService: MockService) => mockService.create(),
    },
    {
      provide: 'MockType',
      useFactory: () => {
        let types = [];
        setTimeout(() => {
          types = ['食品', '家居', '玩具'];
        }, 1e3);
        return types;
      },
    },
    MockService,
  ],
})
export class GoodsModule {}

上面我们注册了一个MockType的对象,并且该对象返回一个数组,但是该数组需要通过setTimeout延迟后才能赋值。
现在,通过在控制器中注入该对象后来使用,并且尝试调接口发现结果如下:

import { GoodsService } from './goods.service';
import { Controller, Get, Inject } from '@nestjs/common';

@Controller('goods')
export class GoodsController {
  constructor(
    private readonly goodsService: GoodsService,
    @Inject('GoodsType') private readonly goodsType: string[],
    @Inject('MockGoodsSerivce') private readonly mockService: GoodsService,
    @Inject('MockType') private readonly mockType: string[],
  ) {}

  @Get()
  hello() {
    return {
      type: this.goodsType,
      mockType: this.mockType,
      greet: this.goodsService.getHello(),
      mockGreet: this.mockService.getHello(),
    };
  }
}

image.png

可以看到,我们通过异步赋值的结果并没有成功注入到程序中来。那么该怎么解决呢?在nest.js中给我们提供了一个十分简单的方案,只需要简单改造注册时的代码即可,代码如下:

{
  provide: 'MockType',
  useFactory: async () => {
    const types = await new Promise((resolve) =>
      setTimeout(() => {
        resolve(['食品', '家居', '玩具']);
      }, 1e3),
    );
    return types;
  },
},

通过async/await的方式,我们就可以将异步获取的值成功注册,再次调接口,返回如下:

image.png

结果表明我们注册通过异步获取的值是一切正常的。

环境变量

在开发中,我们通常会以环境变量的方式定义一些配置文件来代替一些硬编码的配置方式。

配置

node.js中我们可以通过配置.env文件来定义环境变量,例如我下面在根目录新建了.env文件,并写入如下内容:

TITLE=DEMO

nest.js如果想方便的访问.env文件内容,首先得输入命令安装一个配置库:

npm install @nestjs/config

安装完成后,进入goods.module中,引入并使用该模块:

import { Injectable, Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { GoodsController } from './goods.controller';
import { GoodsService } from './goods.service';

@Injectable()
class MockService {
  create() {
    return new GoodsService();
  }
}

@Module({
  imports: [ConfigModule.forRoot()],
  controllers: [GoodsController],
  providers: [
    {
      provide: GoodsService,
      useClass: GoodsService,
    },
    {
      provide: 'GoodsType',
      useValue: ['食品', '家居', '玩具'],
    },
    {
      provide: 'MockGoodsSerivce',
      inject: [MockService],
      useFactory: (mockService: MockService) => mockService.create(),
    },
    {
      provide: 'MockType',
      useFactory: async () => {
        const types = await new Promise((resolve) =>
          setTimeout(() => {
            resolve(['食品', '家居', '玩具']);
          }, 1e3),
        );
        return types;
      },
    },
    {
      provide: 'ENV',
      useValue: {
        title: process.env.TITLE,
      },
    },
    MockService,
  ],
})
export class GoodsModule {}

这里,我们自注册了一个自定义的ENV对象,并且该对象值为环境变量中的TITLE,现在通过在控制器中注入该对象,并返回:

import { GoodsService } from './goods.service';
import { Controller, Get, Inject } from '@nestjs/common';

@Controller('goods')
export class GoodsController {
  constructor(
    private readonly goodsService: GoodsService,
    @Inject('GoodsType') private readonly goodsType: string[],
    @Inject('MockGoodsSerivce') private readonly mockService: GoodsService,
    @Inject('MockType') private readonly mockType: string[],
    @Inject('ENV') private readonly env: Record<string, string>,
  ) {}

  @Get()
  hello() {
    return {
      type: this.goodsType,
      mockType: this.mockType,
      greet: this.goodsService.getHello(),
      mockGreet: this.mockService.getHello(),
      env: this.env,
    };
  }
}

尝试调接口,发现结果如下:

image.png

发现成功获取到了环境变量,说明我们的配置是成功的。

验证

通过使用@hapi/joi库,我们可以对需要配置的环境变量进行一个验证,保证环境变量配置的符合预期。

打开终端,输入以下命令:

npm install --save @hapi/joi
npm install --save-dev @types/hapi__joi

安装好以后,对ConfirgModule进行如下配置:

imports: [
    ConfigModule.forRoot({
      validationSchema: Joi.object({
        TITLE: Joi.number(),
      }),
    }),
]

我们通过validationSchema搭配Joi.object的方式对需要的环境变量做了一个限制。
这里,我定义了TITLE必须为number类型,但我们现在的TITLEDEMO字符串,所以编译时报错了,如下:

image.png

当把TITLE: Joi.number()改为TITLE: Joi.string()后再次恢复正常。

ConfigService

nest.jsConfigModule中还给我们提供了一个ConfigSerivce服务类,通过该类也可以获取到配置的环境变量值,代码如下:

// app.module.ts
// 将ConfigModule改到app.module里面来引入
import * as Joi from '@hapi/joi';
import { ConfigModule } from '@nestjs/config';

imports: [
    ConfigModule.forRoot({
      validationSchema: Joi.object({
        TITLE: Joi.string(),
      }),
    }),
    GoodsModule,
]

// goods.module.ts
import { ConfigModule } from '@nestjs/config';

imports: [ConfigModule], // 改为引入ConfigModule

通过上面两步修改后,我们就可以在goods模块中使用ConfigService文件了,例如:

import { GoodsService } from './goods.service';
import { Controller, Get, Inject } from '@nestjs/common';
import { ConfigService } from '@nestjs/config/dist';

@Controller('goods')
export class GoodsController {
  constructor(
    private readonly goodsService: GoodsService,
    private readonly configService: ConfigService,
    @Inject('GoodsType') private readonly goodsType: string[],
    @Inject('MockGoodsSerivce') private readonly mockService: GoodsService,
    @Inject('MockType') private readonly mockType: string[],
    // @Inject('ENV') private readonly env: Record<string, string>,
  ) {}

  @Get()
  hello() {
    return {
      type: this.goodsType,
      mockType: this.mockType,
      greet: this.goodsService.getHello(),
      mockGreet: this.mockService.getHello(),
      env: this.configService.get('TITLE'), // 通过ConfigService类来获取而不是注入env变量
    };
  }
}

尝试调接口,再看返回如下:

image.png

这说明了我们引入的ConfigService运行一切正常。

过滤器——Filter

nest.js中为我们内置了异常处理的过滤器,我们通过重新实现过滤器可以对异常进行一个友好的返回。

使用

打开终端,输入以下命令,创建一个过滤器文件:

nest g filter common/filters/http-filter --no-spec

执行完成上面命令后,会发现在src/common目录下生成了一个http-filter.filter.ts文件,打开文件发现内容如下:

import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
} from '@nestjs/common';

@Catch()
export class HttpFilterFilter<T> implements ExceptionFilter
{
  catch(exception: T, host: ArgumentsHost) {}
}

上面代码主要有两点:

  • 通过@Catch()装饰器声明;
  • 实现ExceptionFilter接口的catch方法。

现在我们需要通过捕获HTPP的异常,以此来重新返回一个比较友好的数据,所以对该文件代码进行一个重新编写,最终代码如下:

import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpException,
} from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpFilterFilter<T extends HttpException>
  implements ExceptionFilter
{
  catch(exception: T, host: ArgumentsHost) {
    const http = host.switchToHttp();
    const response = http.getResponse<Response>();
    const request = http.getRequest<Request>();
    const status = exception.getStatus();
    const exceptionResponse = exception.getResponse();
    const err =
      typeof exceptionResponse === 'string'
        ? { err: exceptionResponse }
        : exceptionResponse;

    response.status(status).json({
      ...err,
      path: request.url,
      time: new Date().toISOString(),
    });
  }
}

上面代码中,我们主要有以下步骤:

  1. 传入HttpException类,表明我们要处理HTTP的异常;
  2. 通过host参数分别获取到ExpressRequestResponse对象;
  3. 通过exception获取到错误信息和状态码;
  4. 把整理的信息更友好的返回回去!

好的,完成以上编写后,我们进入main.ts文件中进行一个全局过滤器的注册,代码如下:

app.useGlobalFilters(new HttpFilterFilter());

完成以上步骤后,我们这次请求一个不存在的接口,发现返回如下:

image.png

可以发现,现在的返回信息变的更为详细了。

@useFilters

通过使用@useFilters()装饰器,我们可以对单独的控制器甚至某一个路由进行过滤,例如如下代码:

@Get(':name')
@UseFilters()
helloName(@Param('name') name: string) {
    throw new HttpException('test error', 404);
    return `Hello ${name}`;
}

此时我声明了@UseFilters()装饰器但是没指定用哪个过滤器,返回如下:

image.png

接着,我们传入刚刚定义的自定义过滤器,代码如下:

@Get(':name')
@UseFilters(HttpFilterFilter)
helloName(@Param('name') name: string) {
    throw new HttpException('test error', 404);
    return `Hello ${name}`;
}

再次调用接口,如下图所示:

image.png

守卫——Guards

nest.js中为我们内置了守卫功能,通过自定义的守卫功能我们可以实现对接口的权限控制。

使用

首先输入以下命令:

nest g guard common/guards/api-key --no-spec

执行完成后,可以看到在src/common/guards目录下多了api-key.guard.ts文件,打开后代码如下:

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class ApiKeyGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    return true;
  }
}

可以发现,该类实现了CanActivatecanActivate方法,通过该方法我们就可以实现对路由做一个权限控制,现在这里默认返回的ture,我们改为false后试着调接口,发现结果如下:

image.png

可以看到,当返回false后就无法再访问接口。所以,我们可以通过该功能来完成一些认证,例如:

通过定义一个token凭证,如果访问接口传入则能访问,否则不能访问。

实现如下:

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Request } from 'express';
import { Observable } from 'rxjs';

@Injectable()
export class ApiKeyGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}

  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest<Request>();
    if (request.headers.token === 'abcdefg') {
      return true;
    }
    return false;
  }
}

上面代码对token进行了一个认证判断,我们尝试调接口再看结果,如下图所示:

image.png

image.png

可以看到,当传入token时,接口正常返回,否则无法正常返回。

@SetMetadata()

通过nest.js内置的@SetMetadata()可以在路由上设置元数据,通过元数据可以方便我们在守卫中进行一个身份的判断,如下所示:

@Get()
@SetMetadata('IS_PUBLIC', true)
hello() {
    return {
      type: this.goodsType,
      mockType: this.mockType,
      greet: this.goodsService.getHello(),
      mockGreet: this.mockService.getHello(),
      env: this.configService.get('TITLE'),
    };
}

现在,我们再在守卫中加入如下逻辑:

@Injectable()
export class ApiKeyGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}

  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest<Request>();
    const isPublic = this.reflector.get('IS_PUBLIC', context.getHandler());
    if (isPublic) {
      return true;
    }
    if (request.headers.token === 'abcdefg') {
      return true;
    }
    return false;
  }
}

因为上面我们通过依赖注入的方式引入了Refactor类,但是我们的守卫却是自己在main.ts函数手动实例化的,所以这里会有问题导致报错。

下面,我们新建一个common模块,输入以下命令:

nest g mo common

然后打开common/common.module文件,编写如下代码:

import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { ApiKeyGuard } from './guards/api-key.guard';

@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: ApiKeyGuard,
    },
  ],
})
export class CommonModule {}

上面的providers是我们所熟悉的,其中APP_GUARDnest.js里面内置的类,这里的作用就是等同我们注册了一个全局守卫,只是该方式可以让ApiKeyGuard类使用依赖注入的功能。

此时,我们再不带token访问时结果依然正常返回,如下图:

image.png

拦截器——Interceptors

nest.js中拦截器可以帮我们对请求或者响应时添加额外的逻辑操作。

开始

打开终端,输入以下命令:

nest g in common/interceptors/response --no-spec

命令执行成功后,会在src/common/interceptors目录下生成response.interceptor.ts文件:

import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class ResponseInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle();
  }
}

可以发现,在nest.js中拦截器用到了rx.js库,这个库是一个用于操作异步的库,需要去专门了解了才能更懂怎么运动。
因为rx.js算独立的一个学习路线。所以,这里我们就介绍nest.js提供的直接用法吧。

先来看一下拦截器的一个作用范围,我们编写如下代码:

import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import { Observable, tap } from 'rxjs';

@Injectable()
export class ResponseInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('before');
    return next.handle().pipe(tap(() => console.log('after')));
  }
}

再在控制器的路由加上如下代码:

@Get()
@Public()
hello() {
    console.log('request');
    return {
      type: this.goodsType,
      mockType: this.mockType,
      greet: this.goodsService.getHello(),
      mockGreet: this.mockService.getHello(),
      env: this.configService.get('TITLE'),
    };
}

当我们请求接口时,发现日志打印信息如下:

image.png

可以发现,拦截器分打印的两条信息分别在请求前和请求后。

响应拦截

现在,我们就通过nest.js提供的操作方法对返回信息做一个包装,代码如下:

import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import { Observable, map } from 'rxjs';

@Injectable()
export class ResponseInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map((data) => ({
        code: 0,
        data,
        err: null,
        msg: 'ok',
      })),
    );
  }
}

map里面传入的data参数就是我们路由返回的信息。现在尝试请求接口,发现返回如下:

image.png

超时拦截

nest.js中还为我们提供了一个超时拦截的处理,打开终端执行如下命令:

nest g in common/interceptors/timeout --no-spec

打开对应文件,进行如下代码编写:

import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import { Observable, timeout } from 'rxjs';

@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(timeout(3000));
  }
}

现在我们来通过伪代码实现超过3s的返回,修改控制器中的代码如下:

@Get()
@Public()
async hello() {
    await new Promise((resolve) => setTimeout(resolve, 3000));
    return {
      type: this.goodsType,
      mockType: this.mockType,
      greet: this.goodsService.getHello(),
      mockGreet: this.mockService.getHello(),
      env: this.configService.get('TITLE'),
    };
}

再次请求接口,发现结果如下:

image.png

显然,虽然返回了错误信息,但是并没有表示是超时导致的错误,现在我们修改超时拦截器:

import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
  RequestTimeoutException,
} from '@nestjs/common';
import {
  Observable,
  timeout,
  catchError,
  TimeoutError,
  throwError,
} from 'rxjs';

@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      timeout(3000),
      catchError((err) => {
        if (err instanceof TimeoutError) {
          return throwError(() => new RequestTimeoutException());
        }
        return throwError(() => err);
      }),
    );
  }
}

再次调用接口,发现返回信息已经修改,如下所示:

image.png

管道——Pipes

nest.js中管道可以将我们传入的数据通过转换,然后输出为期望的数据;也或者对传入的参数进行一个验证,就例如之前我们已经用过ValidationPipe来对输入进行验证。

使用

打开终端输入以下命令:

nest g pipe common/pipes/parse-int --no-spec

命令执行完成以后,会在src/common/pipes目录下生成parse-int.pipe.ts文件,打开文件内容如下:

import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common';

@Injectable()
export class ParseIntPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    return value;
  }
}

通过实现PipeTransform接口的transform方法,我们就可以对传入的数据进行一个操作,例如我这里期望的是把传入的字符串数字转为一个整数,实现如下:

import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common';
import { BadRequestException } from '@nestjs/common/exceptions';

@Injectable()
export class ParseIntPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    const _value = parseInt(value);
    if (Number.isNaN(_value)) {
      throw new BadRequestException('parseInt fail.');
    }
    return _value;
  }
}

现在我们不通过全局注册来使用,而是指定某个路由的参数,如下:

@Get(':name')
@UseFilters(HttpFilterFilter)
helloName(@Param('name', ParseIntPipe) name: string) {
    console.log(name, typeof name);
    return `Hello ${name}`;
}

@Param()的第二个参数中传入自定义的Pipe对象,然后请求接口访问看到打印日志如下:

image.png

当传入错误的类型时,返回如下:

image.png

中间件——Middleware

nest.js中,中间件是处理路由或者其他模块(拦截器、过滤器等)之前的调用函数。

使用

例如,我们这里通过中间件实现一个统计请求->返回过程的耗时,打开终端输入命令:

nest g mi common/middlewares/time --no-spec

命令执行完成后,在src/common/middlewares目录下生成了一个time.middleware.ts文件,内容如下:

import { Injectable, NestMiddleware } from '@nestjs/common';

@Injectable()
export class TimeMiddleware implements NestMiddleware {
  use(req: any, res: any, next: () => void) {
    next();
  }
}

注意:任何中间件都必须执行next()方法方行,否则请求将被挂起!

现在我们需要注册中间件,这里我们通过在之前创建的common.module模块文件中进行注册,代码如下:

import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { ApiKeyGuard } from './guards/api-key.guard';
import { TimeMiddleware } from './middlewires/time.middleware';

@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: ApiKeyGuard,
    },
  ],
})
export class CommonModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(TimeMiddleware).forRoutes('*');
  }
}

上面的forRoutes('*')表示给所有路由都执行中间件。

现在我们开始时间请求时间的耗时,代码如下:

import { Injectable, NestMiddleware } from '@nestjs/common';

@Injectable()
export class TimeMiddleware implements NestMiddleware {
  use(req: any, res: any, next: () => void) {
    console.time('time');
    res.on('finish', () => console.timeEnd('time'));
    next();
  }
}

通过监听请求的响应结束事件,我们很容易就能获取到耗时,尝试请求接口,发现打印日志如下:

image.png

自定义参数装饰器

nest.js中我们可以通过createParamDecorator函数来实现自定义的装饰器,现在我们新建一个src/common/header.decorator.ts文件,并编写如下代码:

import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { Request } from 'express';
export const Header = createParamDecorator(
  (value: string, ctx: ExecutionContext) => {
    const req = ctx.switchToHttp().getRequest<Request>();
    return value ? req.headers[value] : req.headers;
  },
);

上面的value表示使用装饰器时传入的值,通过该值我们可以灵活的返回对应内容。

现在我们在控制器的路由中添加我们编写的装饰器:

@Get(':name')
@UseFilters(HttpFilterFilter)
helloName(@Param('name') name: string, @Header() header: any) {
    console.log(header);
    return `Hello ${name}`;
}

尝试请求接口,返回如下:

image.png

现在我们修改一下装饰器的参数,代码如下:

@Get(':name')
@UseFilters(HttpFilterFilter)
helloName(@Param('name') name: string, @Header('token') token: string) {
    console.log(token);
    return `Hello ${name}`;
}

再次请求接口,返回如下:

image.png

完结

到这里,我们就已经对nest.js的初级应用有一个简单的了解了,对于每个知识点更详细的用法还需要查阅资料来探索怎么使用!