NestJS 开发技术总结(一)—— NestJS

516 阅读9分钟

NestJS 开发技术总结(一)—— NestJS

NestJS

介绍

Nest 是一个用于构建高效,可扩展的 Node.js 服务器端应用程序的框架。被称为 nodejs 版的 spring。

在底层,Nest 使用 Express(默认)或者 Fastify 来做 HTTP 服务框架。这样在 Nest 中也可以使用那些框架的第三方模块。

Nest 的设计深受 Angular 的启发,如果学过 Angular ,会发现他们的写法几乎一模一样。Nest 从一开始就使用 Typescript 进行开发,这也意味着 Nest 生态下的所有组件都支持 Typescript 。

官网: docs.nestjs.com/

中文:docs.nestjs.cn/

起步

首先电脑上要有 nodejs,然后执行下面的命令

$ npm i -g @nestjs/cli

$ nest new project-name

得到以下文件

以下是这些核心文件的简要概述:

进入项目的根目录执行 npm run start 启动项目,访问 http://localhost:3000/ 查看运行结果,页面上显示 hello world。

核心概念

Nest 项目主要有以下几个核心概念:控制器 Controller, 服务 Service, 模块 Module, 中间件 Middleware, 守卫 Guard, 拦截器 Interceptor, 管道 Pipe, 异常过滤器 Exception Filter。

其中,控制器是处理请求的,大部分业务逻辑都是在控制器中完成的。控制器通过调用 service 来访问数据库。为了让控制器能专注于处理业务逻辑,其他公共逻辑如日志、权限、异常处理等就交给其他组件来处理了。每个组件都有不同的分工。

简单概括一下

  1. 中间件 Middleware 是最开始处理的,它可以修改 Request 和 Response 对象,比如把 user 塞到 request 里面,让后面的组件可以直接从 request 里面拿到用户信息。
  2. 守卫 Guard 一般是做权限控制的,它可以决定一个请求是否要交给控制器处理,如果守卫返回的是false,则请求不会经过控制器。
  3. 拦截器 Interceptor 能在请求进入控制器前和进入控制器后做一些数据处理,比如给返回结果统一包一层{ code: 0, message: 'ok', data: {...}}
  4. 管道 Pipe 是用来处理控制器的参数的,可以在 Pipe 里面进行参数类型校验和参数类型转换。
  5. 随后控制器开始处理业务逻辑,包括访问数据库,返回请求结果等等。
  6. 在上面的任何一个环节如果抛出了异常而且没有进行捕获(try catch)的话,都会来到异常过滤器 Exception Filter 中进行处理。Exception Filter 拥有最后修改响应结果的能力。

它们之间的关系如下图所示:

控制器(Controller)

为了更好的掌握控制器的写法,运行 nest g resource users, 然后打开 users.controller.ts 文件

@Controller('users')

export class UsersController {

  constructor(private readonly usersService: UsersService) {}



  @Post()

  create(@Body() createUserDto: CreateUserDto) {

    return this.usersService.create(createUserDto);

  }



  @Get()

  findAll() {

    return this.usersService.findAll();

  }



  @Get(':id')

  findOne(@Param('id') id: string) {

    return this.usersService.findOne(+id);

  }



  @Patch(':id')

  update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {

    return this.usersService.update(+id, updateUserDto);

  }



  @Delete(':id')

  remove(@Param('id') id: string) {

    return this.usersService.remove(+id);

  }

}

首先,在 UsersController 上面有一个 @Controller('users') 装饰器,表示 url 以 /users 开头的请求将由这个控制器来处理。

其次,每个方法上也有一个装饰器 @Get@Post@Patch或者 @Delete,可以猜到

  • url 为 /users 的 GET 请求将由 findAll 方法处理。
  • url 为 /users 的 POST 请求将由 create 方法处理。
  • url 为 /users/1 的 GET 请求将由 findOne 方法处理。
  • url 为 /users/1 的 PATCH 请求将由 update 方法处理。
  • url 为 /users/1 的 DELETE 请求将由 remove 方法处理。

最后,在 findOne 方法的参数前面也有 @Param('id') 装饰器,它表示把路由 @Get(':id') 中的参数 id 的值作为函数的第一个参数传给 findOne。除了@Param 之外,还有@Query、@Body、@Headers 等多种装饰器,具体可以看官方文档的说明。我们还可以自己创建装饰器,比如 @User 来拿到用户信息,这在后面介绍权限控制的时候会讲到。

服务(Provider)

服务有很多种,其中一种就是上面的 UsersService

@Injectable()

export class UsersService {

  create(createUserDto: CreateUserDto) {

    return 'This action adds a new user';

  }



  findAll() {

    return `This action returns all users`;

  }



  findOne(id: number) {

    return `This action returns a #${id} user`;

  }



  update(id: number, updateUserDto: UpdateUserDto) {

    return `This action updates a #${id} user`;

  }



  remove(id: number) {

    return `This action removes a #${id} user`;

  }

}

在创建 Service 时,需要在类上添加 @Injectable() 装饰器,并添加到 UsersModule 的 providers 中,然后就可以在 UserController 中使用了。

@Module({

  controllers: [UsersController],

  providers: [UsersService],

})

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

export class UsersController {

  constructor(private readonly usersService: UsersService) {}

}

在 UsersController 的构造函数中,我们通过依赖注入的方式,拿到了 UsersService 的实例 usersService。在constructor 的参数前面加上 private 关键字,相当于下面的这种写法。

export class UsersController {

  private usersService;

  constructor(usersService: UsersService) {

    this.usersSerivce = usersService;

  }

}

模块(Module)

模块是具有 @Module() 装饰器的类。 @Module() 装饰器提供了元数据,Nest 用它来组织应用程序结构。Angular 中习惯将模块分为功能模块(FeatureModule)和共享模块(SharedModule),在 Nest 中也一样,然而它们之间并没有本质上的区别。功能模块常常是按照业务来拆分的,例如用户模块,商品模块,广告模块,每个模块有各自的 Controller、Service。一般来说,Controller 只能使用它当前模块的 Service,假如 AdModule 想要调用 UsersModule 中的 usersService,就需要 UsersModule 将 UsersService 暴露出来,这样 UsersModule 就变成了一个共享模块。

@Module({

  controllers: [UsersController],

  providers: [UsersService],

  exports: [UsersService],

})

export class UsersModule {}

然后在 AdModule 中将整个 UsersModule 导入

@Module({

  imports: [UsersModule],

  controllers: [AdController],

  providers: [AdService],

})

export class AdModule {}

中间件(Middleware)

守卫 (Guard)

拦截器( Interceptor)

管道 (Pipe)

异常过滤器 (Exception Filter)

关于 中间件 Middleware, 守卫 Guard, 拦截器 Interceptor, 管道 Pipe, 异常过滤器 Exception Filter 的具体内容大家可以看官方文档再结合项目中的代码进行学习。官方文档永远是我们最好的学习资料。

依赖注入

在 Provider 那一章我们提到了依赖注入,因为依赖注入是 Nest 中非常重要的概念,所以接下来我会对依赖注入进行详细的讲解。

先看一个例子,如果把 UsersController 中 update 方法的两个参数顺序换一下,改成

  @Patch(':id')

  update(@Body() updateUserDto: UpdateUserDto, @Param('id') id: string) {

    return this.usersService.update(+id, updateUserDto);

  }

一般来说,如果我们修改了函数的定义,那么在函数调用的地方我们肯定也要做相应的修改,不然函数在执行的时候肯定会出错。但是在 Nest 中我们却不用这么做。 update 函数修改之后,我们没有修改调用 update 的地方,结果 update 的两个参数 updateUserDto 和 id 依然拿到了正确的值。也就是说,虽然是 Nest 调用了 UsersController 的 update 函数,但是 update 的每个参数不是 Nest 说了算 ,而是 update 函数自己说了算,update 想要什么参数,Nest 就会给它什么参数。

这种设计思想叫做控制反转(IoC),它是通过依赖注入(DI)来实现的。控制反转是 java 的 spring 框架的核心思想。在 Nest 中,到处都能使用依赖注入(DI),现在回去看看核心概念里的那张大图,在 Providers 旁边的解释中有一句话,它们(指Providers)可以通过依赖注入(DI)注入到以上任何一个组件(指 Middleware 等)中。

ps: Provider 也可以注入到 Provider 中。

为什么要用依赖注入

举个例子,在不使用依赖注入时,UsersController 依赖 UsersService,UsersController 通过调用 UsersService 的构造函数,自己创建 UsersService 的实例。

export class UsersService {

  constructor() { }

}
export class UsersController {

  private usersService;

  

  constructor() {

    this.usersSerivce = new UsersService();

  }

}

假如后来 UsersService 的构造函数发生了变化,需要传入一个 UserRepository,那 UsersController 也要跟着改写。

export class UsersService {

  private userRepository UserRepository;

  

  constructor(userRepository: UserRepository) {

    this.userRepository = userRepository; 

  }

}
export class UsersController {

  private usersService;

  

  constructor() {

    const userRespository = new UserRepository();

    this.usersSerivce = new UsersService(userRespository);

  }

}

使用依赖注入后,把依赖的实例化工作交给 IoC 容器(在这里就是 Nest)进行处理,在创建 UsersController 的时候,容器先检查 UsersController 的所有依赖,当找到 UsersService 依赖项时,它将利用 UsersService令牌(token)查找容器内是否已经有 UsersService 的实例,默认情况下 Nest 采用的是单例模式,如果没有,则创建一个并缓存,如果有就直接返回缓存的结果。

依赖注入可以让控制器和服务解耦,使模块更容易测试

此外,依赖注入可以帮助我们解决循环依赖的问题。

思考:依赖注入和直接使用 import 导入实例对象有什么区别?

何如实现依赖注入

这里面有两个很关键的问题,Nest 是如何分析依赖,又是如何查找依赖的。

我们先来回答第二个问题,前面说了查找依赖用的是 Token,那 Token 从哪里来的?其实,我们在模块中声明 providers 的时候,使用的是 provider 的简写

@Module({

  controllers: [UsersController],

  providers: [UsersService],

})

它的完整形式如下,其中 provide 的值 UsersService 就是依赖项 UsersService 的 Token。

providers: [

  {

    provide: UsersService,

    useClass: UsersService,

  },

];

以及,在 UsersController 中,Token 的完整的写法是

@Controller('users')

export class UsersController {

  constructor(@Inject(UsersService) private usersService: UsersService) {}

}

provider 除了可以使用 useClass 还可以使用 useValue,useFactory,useExisting。

举个例子

export class SiteGuard implements CanActivate {

  constructor(@Inject('SITE_ID') private readonly siteId: SITE_ID) {}

}

在 SiteGuard 中我需要一个 siteId,但是 SiteGuard 本身不是我创建的,是 Nest 创建的,我要告诉 SiteGuard 这个 siteId 的值,只能通过 provider 把 siteId 的值注册到容器中,然后容器再把 siteId 的值注入到 SiteGuard 中。具体做法是

@Module({

  providers: [

    {

      provide: 'SITE_ID',

      useValue: '1',

    },

    {

      provide: APP_GUARD,

      useClass: SiteGuard,

    }

  ],

})

export class AppModule {

}

在这个例子中,字符串 'SITE_ID' 就是 Token,我们用这个 token 创建了一个简单的依赖,字符串'1' ,然后在 SiteGuard 中通过 @Inject('SITE_ID') 告诉容器我需要这个依赖。

现在来回答第一个问题,Nest 是如何分析依赖的

我们知道 Typescirpt 在编译成 javascript 之后,类型信息就丢失了

@Controller('users')

export class UsersController {

  constructor(private readonly usersService: UsersService) {}

}

通过 @Inject(UsersService) 的方式是可以告诉 Nest UsersController 的依赖是什么,但是我们明明没有这么写,只通过类型信息,Nest 是怎么在运行的时候知道 UsersController 的依赖的呢?答案是 reflect-metadata

通过安装 reflect-metadata 并在 tsconfig.json 里配置 emitDecoratorMetadata 选项,就可以在运行时通过 Reflect.getMetadata("design:type", target, propertyKey)拿到 typescript 的类型信息

参考资料

www.typescriptlang.org/docs/handbo…

jkchao.github.io/typescript-…