Nest框架尝鲜

我试过很多NodeJS框架,从最早的Express到Koa/Egg,这些框架都是十分简单易写,但是不是我理想中的后端框架(详见 我想要的Web框架 )。Nest是最符合我的预期的。

Nest从请求的纬度上讲可以将应用代码逻辑大体分为三种,控制器(Controller),提供者(Provider),中间件(Middleware)。而从功能纬度上讲,将应用分割为若干模块(Module),并通过exports等方式向其他模块提供自己的内部服务。

先从控制器这个概念入手

控制器这个概念太常见了,Nest中也大同小异,而且Nest中的做法和SpringMVC中做法几乎一样:

@Controller('users')
export class UsersController {} 

我们通过 @Controller 装饰类,并通过以下方式

import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';

@Module({
 controllers: [UsersController]
})
export class AppModule {} 

注册到App模块中,那么应用中就已经有了一个Users控制器了,现在我们可以为控制器编写业务代码:

import { Controller, Get, Req, Query, Headers, Param, Post } from '@nestjs/common';

@Controller('users')
export class UsersController {
 constructor(private readonly userService: UsersService) {}
 @Get()
 findAll(@Req() req, @Query() query, @Headers() headers) {
   return [query, headers];
 }
 @Get('/:id')
 findOne(@Param() params) {
   return params;
 }
} 

控制器端点的设计也基本上照搬了SpringMVC的设计(😀也可能不是照搬这个框架),同样利用 @Get (还有 @Post @Put 等 ) 代表当前方法对应什么请求方法和请求路径。而且还可以通过参数装饰器装饰各种参数,例如用 @Req 装饰req,用 @Query 装饰query(查询参数)等等,这样的好处显而易见,参数可以随意排列,不必担心传错参数。

不过Nest不仅仅利用了装饰器,其实它底层是基于Express的,所以如何上述模式能够满足要求时,我们可以利用Express的能力,例如上述代码中我们其实很难更改我们http status,不过我们可以利用Express的 res 参数:

import { Controller, Get, Req, Query, Headers, Param, Post, Body, Put, Res, HttpStatus } from '@nestjs/common';
import UpdateUserDto from './dto/update-user.dto';

@Controller('users')
export class UsersController {
 @Get()
 findAll(@Req() req, @Query() query, @Headers() headers) {
   return [query, headers];
 }
 @Get('/:id')
 findOne(@Param() params) {
   return params;
 }
 @Put()
 update(@Body() updateUserDto: UpdateUserDto, @Res() res) {
   res.status(HttpStatus.UNAUTHORIZED).send();
 }
} 

Provider是个大概念

Service,Repository,Factory都可以被认为是Provider。所有Provider都是可注入的组件,以Service为例,

import { Injectable } from '@nestjs/common';
import User from './interfaces/user';
import UpdateUserDto from './dto/update-user.dto';

@Injectable()
export class UsersService {
 private readonly users: User[] = [];
 create(user: User): void {
   this.users.push(user);
 }
 findAll(): User[] {
   return this.users;
 }
 update(index: number, userInfo: UpdateUserDto): User {
   this.users[index] = userInfo;
   return this.users[index];
 }
} 

我们通过 @Injectable 将其标记为Provider,现在你可以将UsersService注入到任意Controller,任意Service中,以之前的UsersController为例:

import { Controller, Get, Req, Query, Headers, Param, Post, Body, Put, Res, HttpStatus } from '@nestjs/common';
import CreateUserDto from './dto/create-user.dto';
import UpdateUserDto from './dto/update-user.dto';
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
 constructor(private readonly userService: UsersService) {}
 @Get()
 findAll(@Req() req, @Query() query, @Headers() headers) {
   return [query, headers];
 }
 @Get('/:id')
 findOne(@Param() params) {
   return params;
 }
 @Post()
 create(@Body() createUserDto: CreateUserDto) {
   this.userService.create(createUserDto);
 }
 @Put()
 update(@Body() updateUserDto: UpdateUserDto, @Res() res) {
   res.status(HttpStatus.UNAUTHORIZED).send();
 }
} 

我们通过constructor的方式为控制器注入了UsersService。这种方式在Spring中很常见,很显然,和Spring框架一样,Nest也是一个拥有控制反转能力的框架。

Nest将Express中的Middleware细化了

除了可以添加middleware,Nest还可以添加Filter(过滤器),Pipe(管道),Guard(守卫),Interceptor(拦截器)。很明显这些概念均出现在请求处理之前及请求响应之后。那么它们在Nest中分别有什么用?基于什么目的,Nest造了这么多的概念?

Middleware的用法Nest做了一层封装,但是很明显其根本方法没有变化,都是一个MiddlewareFunction:

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

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
 private readonly logger = new Logger('log', true);
 resolve(...args: any[]): MiddlewareFunction {
   return (req, res, next) => {
     this.logger.log('Request...');
     next();
   };
 }
} 

你还可以和Express一样,只使用一个函数

export function LoggerMiddleware (req, res, next) {} 

其使用也简单

export class AppModule implements NestModule {
 configure(consumer: MiddlewareConsumer) {
   consumer
     .apply(LoggerMiddleware)
     .forRoutes('users');
 }
} 

利用MiddlewareConsumer来应用中间件,其中apply中可以添加一个或多个中间件,而forRoutes方法则指定哪些路由可以应用该中间件。forRoutes使用也很灵活,可以使用通配符,可以指定控制器,可以指定请求方法:

forRoutes('*') // 匹配所有路径
forRoutes('user(12)?') // 匹配user和user12
forRoutes(UsersController)
forRoutes({ path: 'users', method: RequestMethod.GET }) 

Nest中的Filter用于异常捕捉,和Java Web中Servlet区别很大。在Nest中,我们通过HttpException抛出错误,其返回的结果通常是

{
   "statusCode": 401,
   "error": "Unauthorized",
   "message": "未授权"
} 

但是我们可能会想要自己定制自己的错误返回信息,Nest中的Filter为此而生:

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

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
 catch(exception: HttpException, host: ArgumentsHost) {
   const ctx = host.switchToHttp();
   const response = ctx.getResponse();
   const request = ctx.getRequest();
   const status = exception.getStatus();

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

这里我们定制了错误返回信息,下面我们只需要绑定该ExceptionFilter即可,绑定的方式和层级有很多种:

我们使用 @UseFilters 可以绑定在控制器方法上

@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
 throw new ForbiddenException();
} 

我们也可以绑定在模块上

@Module({
 imports: [AuthModule, UsersModule],
 providers: [
   {
     provide: APP_FILTER,
     useClass: HttpExceptionFilter,
   },
 ],
}) 

甚至可以绑定在全局中

async function bootstrap() {
 const app = await NestFactory.create(ApplicationModule);
 app.useGlobalFilters(new HttpExceptionFilter());
 await app.listen(3000);
}
bootstrap(); 

现在我们的返回信息就变成这样了:

{
   "statusCode": 401,
   "timestamp": "2018-09-25T02:20:46.644Z",
   "path": "/users"
} 

另外你也许注意到,我们往 @Catch 中传入了 HttpException ,这限定了我们catch的范围,我们其实也可以不传任何异常参数,让Filter捕捉任何异常。

Pipe在Nest中则通常用于验证传输数据,转换传输数据类型。在Pipe中你可以使用Schema验证,也可以使用Class验证。Schema验证可以使用Joi库,不过我更喜欢Class验证(Nest内部做了相关的集成)。

Class验证主要使用了 class-validatorclass-transformer 两个库,主要思想是将传输数据转换成DtoClass对象,然后根据该DtoClass的meta信息进行验证。其在Pipe中的集成如下:

import { Injectable, PipeTransform, ArgumentMetadata, BadRequestException, Logger } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';

@Injectable()
export class ValidatePipe implements PipeTransform<any> {
 private readonly logger = new Logger('Validate');
 async transform(value, { metatype }: ArgumentMetadata) {
   if (!metatype || !this.toValidate(metatype)) {
     return value;
   }
   const object = plainToClass(metatype, value);
   const errors = await validate(object);
   if (errors.length > 0) {
     throw new BadRequestException('Validation failed');
   }
   return value;
 }
 private toValidate(metatype): boolean {
   const types = [String, Boolean, Number, Array, Object];
   return !types.find((type) => metatype === type);
 }
} 

上述类其实在Nest框架内部中有集成,所以我们不用做不必要的集成。我接着讲讲Pipe的使用,和Filter一样也有多种方式和层级:

// 在参数中
@Post()
async create(@Body(new ValidationPipe()) createCatDto: CreateCatDto) {
 this.catsService.create(createCatDto);
}

// 在方法中
@Post()
@UsePipes(new ValidationPipe())
async create(@Body() createCatDto: CreateCatDto) {
 this.catsService.create(createCatDto);
}

// 在模块中
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';

@Module({
 providers: [
   {
     provide: APP_PIPE,
     useClass: CustomGlobalPipe,
   },
 ],
})
export class ApplicationModule {}

// 在全局应用中
async function bootstrap() {
 const app = await NestFactory.create(ApplicationModule);
 app.useGlobalPipes(new ValidationPipe());
 await app.listen(3000);
}
bootstrap(); 

Guard,保卫者,主要用于权限控制,与Middleware相似,本质不同之处在于,可以获取到当前正在访问的Controller或其中的方法。个人认为作者之所以这样设计就是为了从相关处理逻辑中获取元信息,从而实现基于装饰器的权限控制。创建一个Guard如下:

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

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

要应用该Guard的方式也跟上述组件差不多:

// 在Controller中
@Controller('cats')
@UseGuards(new RolesGuard())
export class CatsController {}
// 在模块中应用
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';

@Module({
 providers: [
   {
     provide: APP_GUARD,
     useClass: RolesGuard,
   },
 ],
})
export class ApplicationModule {}
// 在全局应用中
const app = await NestFactory.create(ApplicationModule);
app.useGlobalGuards(new RolesGuard()); 

那么如果仅仅是这样,岂不是Middleware也能实现?何以要新起一个概念?

关键在于 ExecutionContext ,这个对象代表了请求的上下文,你可以通过这个对象获取执行方法,执行的类等,非常强大,所以基于此我们可以做些权限认证:

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

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

 canActivate(context: ExecutionContext): boolean {
   const roles = this.reflector.get<string[]>('roles', context.getHandler());
   if (!roles) {
     return true;
   }
   const request = context.switchToHttp().getRequest();
   const user = request.user;
   const hasRole = () => user.roles.some((role) => roles.includes(role));
   return user && user.roles && hasRole();
 }
} 

</string[]>

请求的user信息,我们可以通过中间件从数据库或者从配置文件中获取。

Nest的Interceptor(拦截器)和Java中的概念差不多,都可以在请求处理逻辑前后添加处理逻辑。相比较Guard,Interceptor同样能获取到上下文,而且利用RxJS(没学过,这个也是我学习Nest的难点,也许学Angular的同学会觉得简单点)可以对响应体做很多处理。目前的已知的用法就有:1. 响应体的数据映射 2. 执行额外操作 3. 异常处理 4. 重写流。这些操作都完全借助RxJS的能力。下面演示一个执行额外操作的例子,主要借助了tap操作符:

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

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
 private readonly logger = new Logger('LoggingInterceptor');
 intercept(
   context: ExecutionContext,
   call$: Observable<any>,
 ): Observable<any> {
   this.logger.log('Before...');
   const now = Date.now();
   return call$.pipe(
     tap(() => this.logger.log(`After...${Date.now() - now}ms`)),
   );
 }
} 

我们的使用拦截器的方法也和上述概念类似,有使用 @UseInteceptors ,也有使用 useGlobalInterceptors 方法的,也有在模块中使用的。

Provider不仅限于内置

provider可以进行定制,我们以集成TypeORM为例,

疑问一:

Nest是如何通过装饰器将各个端点处理方法和请求方法及路径结合起来的?Nest是如何启动串联整个应用的?

疑问二:

Nest如何实现注入的?

疑问三:

Nest为何要设计Module?entity如何在各个模块之间共享?如果能共享,如何保证耦合度?应该如何合理分割Module?