
背景介绍
Nest
(NestJS
) 是一个用于搭建高性能、可伸缩的Node.js
服务端应用的框架。它使用渐进式的JavaScript
,内置且完全支持Typescript
(但开发人员可以使用纯JavaScript
进行编程) , 同时结合了OOP
(面向对象编程),FP
(函数式编程)和FRP
(响应式编程)的原理。
Controller
在传统Node.js
服务端应用中,controller
用于处理route
对应的客户端请求。每个controller
可以拥有多个route
,不同的route
会执行不同的action
(行为)。在Nest
中,我们可以使用内置的decorators
来对request
, response
, controller
、route
、action
等元素进行定制化的修饰。从而抽离重复代码,提高开发效率。如下所示:
// 省略依赖代码
@Controller('cats') // 模块名称,用于定义route前缀:/cats
export class CatsController {
// 修改该route的成功响应时的状态码为204
@HttpCode(204)
// 修改该response的响应头参数
@Header('Cache-Control', 'none')
// 修饰该route([POST] /cats)的请求方法:POST,@Get、@Post、@Delete同理
@Post()
// @Bady 修饰参数,此处将会传入request.body,@Query()、@Param同理
create(@Body() createCatDto: CreateCatDto) {
// 返回响应的数据
return 'This action adds a new cat';
}
@Redirect("http://www.fzzf.top") // 路由重定向
@Get(":id") // [GET] /cats/:id
show(@Query("") query: ListAllEntities) {
// do something
}
}
对于controller
的返回值,Nest
支持基于async/await
返回的promise
,也支持基于RxJS
返回的Observable Streams
,示例如下:
import { of } from 'rxjs';
// controller代码省略
@Put(":id")
async update(@Param() params: UpdateCatDto): Promise<any> {
return this.catsService.update(params);
}
// 注意不要加async
@Get()
index(@Query() query: IndexCatDto): Observable<any[]> {
return of(this.catsService.find(query));
}
Provider
在Nest
中,provider
被定义为拥有特定功能方法集合的类。通过装饰器@Injectable()
修饰后成为可以作为依赖注入到Nest Module
的服务。处于相同的作用域的provider
之间只要,其中一个provider
也可以作为依赖注入到另一个provider
中。如下代码所示:
// cats.service
import { Injectable } from '@nestjs/common';
@Injectable()
export class CatsService {
constructor(
private readonly otherService: OtherService
)
async create(values: any): Promise<any> {
return this.otherService.create(values);
}
}
我们可以声明一个module
(参考下一节),将provider
注入到controller
中。依赖注入的细节由Nest
框架内部完成,这里只介绍基本的依赖注入姿势,如下面代码所示:
// cats.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';
@Module({
controllers: [CatsController], // 当前module的controller
providers: [CatsService], // 需要注入的provider
})
export class CatModule {}
// cats.controller
import { CatsService } from './cats/cats.service';
@Controller('cats')
export class CatsController {
constructor(
private readonly catsService: CatsService // 依赖注入
) { }
@Post()
create(@Body() createCatDto: CreateCatDto) {
// 返回响应的数据
return this.catsService.create(createCatDto);
}
}
Module
module
是Nest
应用的基础组成单位。一个Nest
应用至少拥有一个module(root module)
。简单来说,在Nest
中,n个功能module
通过依赖注入组成一个业务module
,n个业务
module
组成一个Nest
应用程序。无论是controller
还是provider
都需要通过module
注册才能完成依赖的注入。下面的代码介绍了基础的module
声明姿势。
@Module({
// 依赖的其他module类
imports: [],
// 业务模块所需的controller类,它将觉得module如何是实例化为业务模块还是功能模块
controller: [],
// 提供依赖的类
provider: [],
// 导出模块中provider,可以被其他模块依赖注入
exports: [],
})
export class AppModule {}
这里我们可以通过传递exports
参数,才完成module
之间层级依赖,以及不同module
中的provider
能够被复用。如下面代码所示:
// request.module.ts
@Module({
// 注册provider
provider: [RequestService],
// 导出注册的provider
exports: [RequestService],
})
export class RequestModule {}
// api.module.ts
@Module({
// 导入一个RequestModule,Nest会注册其中export的provider
imports: [RequestModule],
// 注册controller
controllers: [ApiController],
})
export class ApiModule {}
// api.controller.ts
@Controller('api')
export class ApiController {
constructor(
private readonly requestService: RequestService // 依赖注入
) { }
}
除此之外,我们还可以通过装饰器@Global
将module A
修饰为一个全局模块,这样任意module
不需要import
就可以注入module A
中export
的依赖。同时我们也可以通过特定参数实现module
的动态导入。
Middleware
Nest
是一个上层框架,底层依赖于express
框架(或hapi),所以Nest
中middleware
等同于express
中的middleware
,我们可以使用它来访问request
和response
,以及对处理程序做一些前置或后置操作。它的声明方式如下:
- 声明
// 方式1
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';
@Injectable()
export class CatchMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: Function) {
console.log('catch...');
next();
}
}
// 方式2
export function catchError(req, res, next) {
console.log(`catch...`);
next();
};
- 使用
// 方式1
@Module({
imports: [CatsModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(CatchMiddleware)
.forRoutes(CatsController);
}
}
// 方式2
const app = await NestFactory.create(AppModule);
app.use(catchError);
await app.listen(3000);
Exception filters
Nest
内置了处理程序未处理的异常捕获层(exception-filter
)以及友好的异常处理对象。我们通过声明exception
来对catch
全局或局部的异常抛出,如全局的error
。如下面代码所示:
http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
// @Catch()参数为空时,catch所有类型的异常
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
// 获取当前处理程序所在的上下文对象
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
// 拦截并处理响应
response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
// 使用
@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
同时我们还可以继承Nest
内置的exception
对象来规范化系统异常的格式,如下代码所示:
export class ForbiddenException extends HttpException {
constructor() {
super('Forbidden', HttpStatus.FORBIDDEN);
}
}
// 错误打印
{
"status": 403,
"error": "This is a custom message"
}
Pipes
Pipe
在Nest
中用于对controller
中处理程序的request
做前置处理。
- 格式化请求输入,如req.query、req.param、req.body等;
- 校验请求输入,若正确则按原样传递,反正则抛出异常
exception
; 这里声明方式可以参考官方文档。
Guards
在Nest
中,Guard
被设计拥有单一指责的前置处理程序,用于通知route
对应处理函数是否需要执行。官方文档中推荐使用Guard
来实现
authorization
相关的功能,如权限校验、角色校验等,来替代传统express
应用程序中使用middleware
实现的思路。
官方认为middleware
本质上是很“愚蠢”的,因为它无法知道在调用next
之后将执行哪一个处理函数。而Guard
可以通过ExecutionContext
获取当前request
的执行上下文,清楚的知道哪一个handler
将要执行,因此可以保证开发者在正确的request
或response
周期添加处理逻辑。
它的声明方式如下:
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
// 注意函数声明
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return validateRequest(request);
}
}
同时guard
也支持全局、单个controller
或单个handle method
进行前置处理。详情可以参考官方文档。
Interceptors
在Nest
中,Interceptor
的设计灵感来自于[AOP](https://en.wikipedia.org/wiki/Aspect-oriented_programming)
,用于给某个处理函数模块(如controller
)添加前置/后置操作。实现功能如:输入/输出的额外操作、exception
的过滤、在特定条件下重载当前处理函数的逻辑等。它的声明方式如下:
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class HttpInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// context当前执行上下文
// next下一个将要执行的处理函数
return next.handle().pipe(
map((value: any) => ({
code: 0,
data: value,
}))
);
}
}
// 使用
UseInterceptors(HttpInterceptor)
export class CatsController {}
Request Lifecycle
以上介绍的各种Nest
组件被注册之后由Nest
框架管理,它们在一个请求的某个声明周期阶段被执行。具体执行顺序如下:
- 请求输入
- 全局
middleware
- 根模块(
root module
)middleware
- 全局
Guards
Controller Guards
route
对应处理函数上使用@UseGuards()
注册的Guard
- 全局
interceptors
(controller前置处理) controller
的interceptors
(controller前置处理)route
对应处理函数上使用@UseInterceptors()
注册的interceptors
(controller前置处理)- 全局
pipes
controller
的pipes
route
对应处理函数上使用@UsePipes()
注册的Pipes
route
对应处理函数参数注册的Pipes
(如:@Body(new ValidationPipe())route
对应处理函数route
对应处理函数依赖的Serivce
route
对应处理函数上使用@UseInterceptors()
注册的interceptors
(controller后置处理)controller
的interceptors
(controller后置处理)- 全局
interceptors
(controller后置处理) route
对应处理函数注册的Exception filters
controller
注册的Exception filters
- 全局注册的
Exception filters
- 响应输出