介绍
Nest 是一个用于构建高效,可扩展的 Node.js 服务器端应用程序的框架。如果你正在寻找一个成熟的 TypeScript 框架,那它不可否认是目前最值得推荐的框架之一。
🔍 通常,我们在考虑要不要使用一个框架时,会从几个方面去考量。
-
📌 框架的热度,通常表现为 github start 数量。
-
📌 框架的生态/社区是否完善。
-
📌 框架的维护是否持续,出现的问题是不是有及时修复。
📈 结合上面几个维度,我们针对 Nest 来分析:
-
💚 Nest Github Start 65k, 热度次于 Express ,排行第二。
-
💚 Nest 底层使用了 Express(默认)和 Fastify,能轻松使用每个平台的第三方模块,意味着同时拥有了 Express 社区和 Fastify 社区。
-
💚 Nest Github Issues open 才 53 个,已关闭了 5222 个!平均每月一个版本!而且每个打开的 Issues 都有相应的回复。说明其维护响应速度是相当的快。
在开始学习之前,如果你还没看过 学习 Nestjs 前,你需要了解什么是依赖注入(原理详解) - 掘金,强烈建议你先看一下。依赖注入是 NestJs 最核心的实现,了解其原理学习起来更容易理解。
安装
使用 Nest CLI 构建项目。
npm i -g @nestjs/cli
nest new project-name
目录结构
nest 最核心的就是 控制器、提供者、模块。一个应用会有一个 APP 模块,而 App 模块中又包含多个功能子模块,每个子模块又由多个控制器和提供者组成。目录结构如下:
src
├── cats ------------------------ 模块文件夹
│ ├──dto --------------------- 数据传输对象(http 参数)
│ │ └──create-cat.dto.ts
│ ├── interfaces ------------- 接口 (数据库 / http 返回参数)
│ │ └──cat.interface.ts
│ ├──cats.service.ts --------- 服务
│ └──cats.controller.ts ------ 控制器
└──cats.module.ts ---------- 模块
├──app.module.ts ---------------- App 模块
└──main.ts
控制器 Controllers
控制器负责处理传入的请求和向客户端返回响应。也是路由创建和处理的地方。
使用 @Controller() 创建一个控制器
@Controller('cats')
export class CatsController {
constructor(private 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() params): string {
console.log(params.id);
return `This action returns a #${params.id} cat`;
}
}
如上我们创建了一个 CatsController 控制器,当客户端使用 GET 请求 /cats 时,就会调用 findAll 方法,这里方法名可以是任意的,主要和方法的装饰器有关。@Get() 指定了该方法接收 GET 请求。同理:当客户端使用 POST 请求 /cats 时,会调用 create 方法。
@Get(':id')装饰器里的参数可以是子路由,表示 /cats/:id/。
@Redirect('https://nestjs.com', 301)可以重定向更多的装饰器请参考:Documentation | NestJS - A progressive Node.js framework
提供者(服务) Providers
为控制器提供各种服务,如创建、编辑、删除等业务逻辑。以及与数据库层面的交互。
使用 @Injectable() 注册一个服务
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;
}
}
通常我们会在这里一下数据库的
CRUD操作。
在对应控制器中使用该服务
@Controller('cats')
export class CatsController {
constructor(private catsService: CatsService) {}
@Post()
async create(@Body() createCatDto: CreateCatDto) {
// 调用服务创建
this.catsService.create(createCatDto);
}
}
模块 Modules
每一个 nest 应用都有一个 root 模块,也就是下图中的 Application 模块。模块的功能是去组织的你的组件,维护项目的依赖关系。当然,除了 root 模块,其他模块并不是必须的,但还是强烈推荐使用模块去管理你的项目。
模块
可以将多个控制器、提供者、管道、中间件等组织在一起,并可以在其他模块中引用和重复使用。
模块的共享
每一个模块都有自己的作用域(私有域), 所以不同模块之间是隔离的,这就意味着没有引入模块,就不能使用模块上的服务,所有要通过 exports 配合 imports 才能实现共享。
@module({
imports: [], // 引入模块服务
exports: [], // 导出模块服务
providers: [UserService], // 服务
controllers: [UserController], // 控制器
})
export class UserModule {
}
- 共享的是模块上的所有服务。
- imports 导入模块、exports 导出服务。引入模块,实际是引入的模块上已导出的所有服务。
例如 Animal模块要使用Cat模块 的服务,Cat模块首先要将自己的服务导出,Animal模块 再引入Cat模块。
// ----------- cat.module.ts(导出服务) --------------------
@Module({
providers: [CatService1, CatService2],
controllers: [CatController],
exports: [CatService1], // 导出服务
})
export class CatModule {}
// ----------- animal.module.ts(引入模块) --------------------
@Module({
imports: [CatModule], // 引入模块
providers: [AnimalService1],
controllers: [AnimalController],
})
export class AnimalModule {}
// ----------- animal.controller.ts(使用共享服务) ----------------
import { CatsService } from '../cats/cats.service'
@Controller('animal')
export class AnimalController {
// 使用共享服务
constructor(private catsService: CatsService) {}
}
注意:模块B 想要使用 模块A 的服务,有两个必要条件
步骤一:模块A 导出 了该服务
步骤二:模块B 导入了模块A
如果没满足上面两个条件,模块 A 直接使用 模块B 的服务,会报错:
Error: Nest can't resolve dependencies of the CatsController (?).
Please make sure that the argument ShareService at index [0]
is available in the CatsModule context.
全局共享
有时候我们定义了一个模块,其他模块想使用这个模块的服务,可以手动引入,而另一种方式就定义为 @Global 装饰器,使模块成为全局作用域。其他模块就可以无需导出,而使用服务。
不建议这么用,
imports会使模块依赖更加透明。
// ----------- cat.module.ts --------------------
@Global()
@Module({
providers: [CatService1, CatService2],
controllers: [CatController],
exports: [CatService1], // 导出服务
})
export class CatModule {}
// ----------- animal.module.ts --------------------
@Module({
// imports: [CatModule], // 引入模块
providers: [AnimalService1],
controllers: [AnimalController],
})
export class AnimalModule {}
中间件 Middleware
作用于请求到请求处理函数之间。可以在路由处理函数前做一些处理,例如日志记录、身份验证和授权等。
中间件绑定在模块上
应用中间件
nest 的中间件分为两种,class 中间件和函数中间件。需要配置中间件的作用范围。
// ----------------------- logger.middleware.ts ---------------
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
// class-中间件
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log('Request...');
next();
}
}
// 函数-中间件
export function logger(req, res, next) {
console.log(`Request...`);
next();
};
// ----------------------- app.module.ts ---------------
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [CatsModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
// 使用中间件
.apply(LoggerMiddleware)
// 中间件应用的路由
.forRoutes('cats');
}
}
上面定义了一个 LoggerMiddleware中间件,在 app 模块中注册,并指定了中间件应用的路由范围为 cats, 那么当请求进入 '/cats/...' 就会进入中间件,那么我们可以在该中间件中记录一些请求和返回体信息。
全局中间件
如果我们想一次性将中间件绑定到每个注册路由,可以在 main.ts 中 create 后的 app 实例中注册。
// ----------------------- main.ts ---------------
const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);
异常过滤器 Exception filters
作用:负责处理整个应用程序中抛出的异常,使客户端能收到友好的响应提示。
例如:后端系统程序错误,接口返回 500 ,并提示 Internal server error。
内置异常-写法1
开发中常规的异常处理器,NestJs 已经帮我们内置好了,只需要会用就行。例如用户未鉴权,调用接口我们一般会返回一个 403 的状态了,一段提示 。
// ----------------------- cats.controller.ts ---------------
import { HttpException, HttpStatus } from '@nestjs/common'
@Get()
async findAll() {
if (notLogin) { // 这里是伪代码,仅说明使用场景
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
return ['数据']
}
响应结果:
{
"statusCode": 403,
"message": "Forbidden"
}
内置异常-写法2
HttpException 构造函数第一个参数除了是字符串,也可以接收一个对象,就可以自定义异常响应结果的内容。如下:
import { HttpException, HttpStatus } from '@nestjs/common'
@Get()
async findAll() {
if (notLogin) { // 这里是伪代码,仅说明使用场景
throw new HttpException({
success: false,
status: HttpStatus.FORBIDDEN,
erro: '未登录,请先登录!'
}, HttpStatus.FORBIDDEN);
}
return ['数据']
}
响应结果:
{
"success": false,
"status": 403,
"error": "未登录,请先登录!"
}
更多内置异常处理器
HttpException 属于核心异常处理器,下面还有一些内置的继承的可用异常处理器。
BadRequestExceptionUnauthorizedExceptionNotFoundExceptionForbiddenExceptionNotAcceptableExceptionRequestTimeoutExceptionConflictExceptionGoneExceptionPayloadTooLargeExceptionUnsupportedMediaTypeExceptionUnprocessableExceptionInternalServerErrorExceptionNotImplementedExceptionBadGatewayExceptionServiceUnavailableExceptionGatewayTimeoutException
自定义异常
大多数情况下,内置异常是够用的,如果确实需要创建自定义异常,操作如下:
// forbidden.exception.ts
export class ForbiddenException extends HttpException {
constructor() {
super('Forbidden', HttpStatus.FORBIDDEN);
}
}
自定义 ForbiddenException 必须继承 HttpException。
使用
import { ForbiddenException } from '/forbidden.exception.ts'
@Get()
async findAll() {
throw new ForbiddenException();
}
管道 Pipe
管道作用于客户端和控制器处理之前,可以简单理解为是对请求参数的校验和操作。
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
上的代码,ParseIntPipe 是一个内置管道,会校验 id 参数的类型是否为整型,请求 api/ccc 时,由于 ccc 不是一个整型,就会返回异常。也就不会继续执行 findOne 方法。
{
"statusCode": 400,
"message": "Validation failed (numeric string is expected)",
"error": "Bad Request"
}
如果我们想自定义响应内容,可以改成:
new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE })
@Get(':id')
async findOne(
@Param(
'id',
new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE })
)
id: number,
) {
return this.catsService.findOne(id);
}
@nestjs/common 中的内置管道:
ValidationPipeParseIntPipeParseFloatPipeParseBoolPipeParseArrayPipeParseUUIDPipeParseEnumPipeDefaultValuePipeParseFilePipe
守卫 guards
绑定在控制器上
守卫的功能类似于中间件,区别在于:
1、守卫是绑定在控制器上,中间件是绑定在模块上。因为模块上可能有很多控制器 ,固然该模块上的中间件就能作用到这些控制器。而守卫和控制器是一一绑定的。正因为如此,“守卫比中间件更了解控制器” ,能读取控制器的上的更多信息,而这是中间件做不到的。
2、它们的职责不同,中间件流转在控制器之间,而守卫是控制器的最后一道防卫。
3、守卫执行时机:在中间件之后、拦截和管道之前执行。
比喻:
如果把中间件和守卫比做足球比赛中的后卫和守门员,把中间件比作球门。中间件游走在各个足球传递的中间,只需要记录或者阻止 A-B, 而守门员则是控制器的最后一道防线,防止足球进入球门。
示例:
import { Injectable, CanActivate, UseGuards, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
// 注册守卫
@Injectable()
export class RolesGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
return true;
}
}
// ----------------------- cats.controller.ts ---------------
// 绑定守卫
@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}
每个守卫必须实现一项canActivate()功能。该函数应返回一个布尔值
- 如果返回
true,则请求将被处理。 - 如果返回
false,Nest 将拒绝该请求。
通过 canActivate 的 context参数,我们可以拿到执行下文的一些信息。例如定义不同的角色,作用在不同的控制器上,通过守卫获取到不用的角色,然后处理。
// 定义元数据
import { Reflector } from '@nestjs/core';
export const Roles = Reflector.createDecorator<string[]>();
// 在控制器 create 方法上绑定元数据
@Post()
@Roles(['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
// 守卫中获取下文执行的元数据信息
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// 获取下文中执行方法的角色元数据信息
const roles = this.reflector.get(Roles, context.getHandler());
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
return matchRoles(roles, user.roles);
}
}
上面,使用 Reflector.createDecorator 定义了元数据信息,并绑定到控制器的 create 方法上,然后在守卫里通过 context.getHandler() 获取方法上的元数据信息。如果守卫作用在控制器上,可以通过 context.getClass() 执行下文的控制器类。
拦截器 Interceptors
它们用于在函数执行之前或之后增加额外的逻辑:
-
转换函数返回的结果
-
转换函数抛出的异常
-
绑定额外的逻辑到方法的入口和出口:这允许我们在请求处理流程中的特定点执行某些操作,例如,记录日志、计算方法执行时间等。
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, UseInterceptors } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
// 定义拦截器
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before...');
const now = Date.now();
return next
.handle()
.pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`)),
);
}
}
// 绑定拦截器
@UseInterceptors(LoggingInterceptor)
export class CatsController {}
案例:
-
返回结果是 null 转成 '' 空字符串。
-
返回结果缓存起来,如果请求参数不变,返回缓存结果。
-
请求超时后自定义返回结果。
流操作:
由于handle()返回 RxJS Observable,我们有多种操作符可供选择来操作流。
-
tab() :流正常或异常终止时调用,不会以其他方式干扰响应周期。
-
map(): 响应映射,处理返回结果。
-
catchError(): 异常映射,处理抛出异常
总结
本文从项目结构上解释了 NestJS 中的核心概念。帮助大家总整体去理解 NestJS 开发运作模式。我们可以严格按照官方的规范走,这会帮我们省下不少时间。