Nest.js:给你看个不一样的 Node.js
Posted Qunar技术沙龙
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Nest.js:给你看个不一样的 Node.js相关的知识,希望对你有一定的参考价值。
姚建龙
个人介绍:姚建龙,2014年加入去哪儿网技术团队。目前主要负责大住宿RN相关业务,参与开发了酒店eb系统、酒店touch站、easy住地图选房等项目。对跨平台实现方案、混合开发等很感兴趣。
简介
NestJs 是一款用于构建高效且可伸缩 Web 应用程序的渐进式 Node.js 框架。看下官方给的简介,NestJs 模块化的体系结构,允许开发者使用任何其他的库,从而提供灵活性;为 Nodejs 提供一个适应性强大的生态系统;利用最新的js特性,为 nodejs 提供更加方便的设计模式和成熟的解决方案。
在利用 NestJs 框架中,开发者可以体验到 NestJs 清晰的模块组织方式,隔离应用的各个工作区间;在各种工作区间都涉及不一样的设计模式,比如面相对象的SOLID原则在整个应用构建中的体现,AOP、DI、IoC 等在各个核心部件中的应用,OOP、FP、FRP 在各模块编码中的完美结合,都给开发中带来不一样的体验。
设计哲学
NestJs 的设计是为了解决 Node.js 体系结构的问题,NestJs 旨在为 Node.js 提供一个开箱即用的应用程序体系结构,允许轻松创建高可测试性、可伸缩、松散耦合和易于维护的应用程序。
NestJs 底层使用 express 进行构建,推荐使用 Typescript 进行代码编写的同时也支持原生 javascript 的降级编写方式,在代码的组织结构上借鉴了 Angular 的组织方式,NestJs 基础库中设计了大量的装饰器,同时支持开发中自定义业务需要的装饰器。
核心组件
NestJs 主要有 8 个组件(Controller 控制器、Component 组件、Module 模块、Middlewares 中间件、Exception Filters 异常过滤器、Pipes 管道、Guards 守卫、Interceptors 拦截器),主要通过 Controller、Component、Module 三个最核心的组件构成。
其中 Controller 是传统意义的控制器,工程启动时,控制台可以清晰的看到应用的路由配置信息,Provider 一般做 Service,比如数据的 CRUD 都可以封装在 Service 中,每一个 Service 就是一个 Provider,也是主要的业务代码逻辑部分,Module 表示应用或者模块,一个 Module 可以拥有多个 Controller。
[Nest] 7209 - 2018-8-31 20:38:52 [NestFactory] Starting Nest application...
[Nest] 7209 - 2018-8-31 20:38:52 [InstanceLoader] ApplicationModule dependencies initialized +9ms
[Nest] 7209 - 2018-8-31 20:38:52 [InstanceLoader] CatsModule dependencies initialized +1ms
[Nest] 7209 - 2018-8-31 20:38:52 [RoutesResolver] CatsController {/cats}: +21ms
[Nest] 7209 - 2018-8-31 20:38:52 [RouterExplorer] Mapped {/, POST} route +3ms
[Nest] 7209 - 2018-8-31 20:38:52 [RouterExplorer] Mapped {/, GET} route +0ms
[Nest] 7209 - 2018-8-31 20:38:52 [RouterExplorer] Mapped {/:id, GET} route +1ms
[Nest] 7209 - 2018-8-31 20:38:52 [NestApplication] Nest application successfully started +1ms
Controller
控制器层负责处理传入的请求,并返回对客户端的响应,这部分和传统的控制器没有差异。
NestJs 中通过 @Controller() 装饰器来创建控制器,可以通过 @Get()、@Post 等对类方法进行修饰,说明请求方式,以此实现 NestJs 路由的去中心化。
import {
Body,
Controller,
Get,
Param,
Post,
} from '@nestjs/common';
import { CatsService } from './cats.service';
import { CreateCatDto } from './dto/create-cat.dto';
import { Cat } from './interfaces/cat.interface';
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
@Get(':id')
findOne(
@Param('id', new ParseIntPipe())
id,
) {
// logic
}
}
NestJs 支持 asnyc/await,极大的方便开发者,同时,NestJs 为开发者提供了丰富的修饰器方法用于解析及处理 http 请求,使得开发者更快更便捷的构建自己的控制器。
Provider
几乎所有的东西都可以被认为是 Provider(service, repository, factory, helper 等等)。他们都可以注入依赖关系 constructor,也就是说,他们可以创建各种关系。但事实上,提供者不过是一个用 @Injectable() 装饰器注解的简单类。
注:Nest4.x 为 Component,Nest 5.x 更新为 Provider
值得注意的是 NestJs 推荐使用面对象的方式来设计和组织依赖性,因此强烈建议遵循 SOLID 原则。
HINT:Since Nest enables the possibility to design and organize the dependencies in a more OO-way, we strongly recommend following the SOLID principles.
在 NestJs 中,经常使用依赖注入,当我们设计好一个 Provider 之后,就可以通过 Controller 类构造函数注入。
import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';
@Injectable()
export class CatsService {
private readonly cats: Cat[] = [];
create(cat: Cat) {
this.cats.push(cat);
}
findAll(): Cat[] {
return this.cats;
}
}
Modules
模块是具有 @Module() 装饰器的类。 @Module() 装饰器提供了元数据,NestJs 用它来组织应用程序结构。
当我们定义编写完某块业务逻辑的 Controller 和 Provider 之后,我们就可以通过 @Module() 将对应模块的逻辑组织起来,NestJs 同时提供模块导出、动态模块、共享模块的支持,可以很方便的解决业务场景中常见的单例等问题。
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}
至此,如上述代码,我们就通过 Controller、Provider 以及 Module 就构建出了一个简单的应用。 然鹅,我们的实际的应用需求并不仅限于此,在真实的世界里,我们还得处理数据的校验、异常的捕获及处理、权限管理、日志记录等等一系列实际的问题。别担心,NestJs 早已为我们想好了一切。
Middleware
NestJs 的中间件对 express 的中间件进行了一层封装。中间件在路由处理器之前被调用。 中间件可以访问请求和响应对象,以及应用程序请求响应周期中的下一个中间件功能。下一个中间件函数通常由名为 next 的变量表示。
与 express 不同的是,NestJs 的中间件在使用时,需要通过实现 NestModule 的 configure 方法来引入,中间件可以这对单独的路由或者控制器进行处理。
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middlewares/logger.middleware';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [CatsModule],
})
export class ApplicationModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('/cats’); // ‘/cats’ 变更为 CatsController 则是对控制器进行中间件配置
}
}
与此同时,NestJs 同样也支持函数式的中间件、异步中间件。
// 异步中间件
import { Injectable, NestMiddleware, MiddlewareFunction } from '@nestjs/common';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
async resolve(name: string): Promise<MiddlewareFunction> {
await someAsyncJob();
return async (req, res, next) => {
await someAsyncJob();
console.log(`[${name}] Request...`); // [ApplicationModule] Request...
next();
};
}
}
// 函数式中间件
export function logger(req, res, next) {
console.log(`Request...`);
next();
};
全局中间件配置,需要在 NestFactory 创建的实例上进行配置,这里配置方法和 express 类似
const app = await NestFactory.create(ApplicationModule);
app.use(logger);
await app.listen(3000);
Exception Filters
健壮的应用应该有完善的异常处理逻辑,并且有针对的将异常划分为不同的层次,未捕获到异常时,需要给用户良好的响应,NestJs 提供 HttpException 来处理 http 请求的异常。
NestJs 通过 @ UseFilters() 设置需要使用的过滤器,通过 @Catch() 来捕获异常抛给对应的异常过滤器处理。
// 设置异常过滤器
@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
// 捕获异常,并给异常过滤器处理
import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common';
import { HttpException } 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();
response
.status(status)
.json({
statusCode: exception.getStatus(),
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
好的异常系统设计应该是分层次的,同样的,NestJs 支持开发者自定义异常、设置异常的处理顺序、设置全局异常处理等。
import { HttpExceptionFilter } from './exceptions/http-exception.filter';
import { AnyExceptionFilter } from './exceptions/any-exception.filter’;
const app = await NestFactory.create(ApplicationModule);
// 异常捕获顺序根据 useGlobalFilters 顺序决定
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalFilters(new AnyExceptionFilter());
await app.listen(3000);
Pipe
管道是具有 @Injectable() 装饰器的类。管道应实现 PipeTransform 接口。
管道将输入数据转换为所需的输出,比如请求数据格式化、请求数据校验抛异常处理等等。
// create-cat.dto.ts
import { IsString, IsInt } from 'class-validator';
export class CreateCatDto {
@IsString()
readonly name: string;
@IsInt()
readonly age: number;
@IsString()
readonly breed: string;
}
// validation.pipe.ts
import { PipeTransform, Pipe, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
async transform(value, metadata: ArgumentMetadata) {
const { metatype } = metadata;
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);
}
}
// cats.controler.ts
@Post()
@UsePipes(new ValidationPipe())
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
同样地,管道也可以设置对全局路由进行处理(@useGlobalPipes)。
Guard
守卫有一个单独的责任。它确定请求是否应该由路由处理程序处理。到目前为止, 访问限制逻辑大多在中间件内。这样很好, 因为诸如 token 验证或将 req 对象附加属性与特定路由没有强关联。但中间件是非常笨拙的。它不知道调用 next() 函数后应该执行哪个处理程序。另一方面, 守卫可以访问 ExecutionContext 对象, 所以我们确切知道将要评估什么。
注:守卫是在每个中间件之后执行的, 但在管道之前。
守卫是一个使用 @Injectable() 装饰器的类。 守卫需要实现 canActivate 方法。
// 角色装饰器 roles.decorator.ts
import { ReflectMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => ReflectMetadata('roles', roles);
// 用角色装饰器修饰路由 cats.controller.ts
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
// 基于权限判断的守卫: roles.guard.ts
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();
}
}
从这里可以看出,首先拿到执行上下文的关键就是这里的反射器,通过反射拿到待执行处理的路由访问权限信息,判断该请求是否具备访问改路由的权限,从而很好的将权限校验的逻辑从业务逻辑中分离出来。
Interceptor
拦截器是 @Injectable() 装饰器注解的类。拦截器应该实现 NestInterceptor 接口。 拦截器具有一系列有用的功能,这些功能受面向切面编程(AOP)技术的启发。它们可以:
在函数执行之前/之后绑定额外的逻辑 转换从函数返回的结果 转换从函数抛出的异常 根据所选条件完全重写函数 (例如, 缓存目的)
NestJs 的拦截器借助于 rxjs 强大功能来实现。
// 请求/响应日志记录 logging.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
call$: Observable<any>,
): Observable<any> {
console.log('Before...');
const now = Date.now();
return call$.pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`)),
);
}
}
// UseInterceptors来用装饰控制器
@UseInterceptors(LoggingInterceptor)
export class CatsController {}
响应顺序
综上所述,NestJs 各部件调用顺序可以整理如下:
总结:
根据上述的各部件功能拆分,NestJs 工程组织结构大致可以形成如下的组织结构:
.
├── jest.json
├── node_modules
├── package-lock.json
├── package.json
├── src
│ ├── app.module.ts // 模块聚合
│ ├── cats // 业务模块
│ │ ├── cats.controller.spec.ts
│ │ ├── cats.controller.ts
│ │ ├── cats.module.ts
│ │ ├── cats.service.ts
│ │ ├── dto
│ │ └── interfaces
│ ├── common
│ │ ├── decorators // 自定义装饰器等等
│ │ ├── filters // 过滤器:异常处理等等
│ │ ├── guards // 守卫:权限校验等等
│ │ ├── interceptors // 拦截器:AOP处理
│ │ ├── middlewares // 中间件处理
│ │ └── pipes // 管道处理,DTO数据处理等等
│ └── main.ts // 主入口
├── tsconfig.json
├── tslint.json
└── yarn.lock
NestJs 不单单针对 Node.js 进行框架封装,同时 NestJs 官方针对 GrapQL、MicroService、Websocket 等服务提供了不同的解决方案。写到这里,是否很多东西都有 Java spring 的影子,是的,后端的框架发展以及很成熟,相反,前端发展就庞杂了,回头发现不管框架语言如何的变化,不变的是编程的思想,前端要走的路还有很远。
以上是关于Nest.js:给你看个不一样的 Node.js的主要内容,如果未能解决你的问题,请参考以下文章