NestJS 是一个 NodeJS 的后端服务框架,它与传统的 NodeJS 框架不一样的是采用了控制反转(IOC)和依赖注入(DI)的模式进行开发。当然,如果你对 NestJS 不熟悉的话也没关系,看完本节相信你就能入门 NestJS。
你没看错,入门就是那么简单!所以一定要仔细阅读本篇文章,后续章节如果出现没提到的部分,也会进行详细讲解,做到边实战边学习。所以说即使你现在不会 NestJS,看完本专栏之后你也能在其它项目中熟练使用 NestJS 了
ok,你准备好了吗?接下来就让我们开始后端部分的学习吧。
第一个 Nest 程序
首先我们全局安装 Nest
npm i -g @nestjs/cli
然后新建一个项目就叫 fs-admin,打开终端执行nest new xxx命令创建一个 nest 项目
nest new admin_nest
选择 npm,等待一会就会发现我们的 nest_cli 项目创建完成了,它的目录结构如下
我们的代码一般都会写在 src 目录下,这里介绍一下 src 中各种文件是干嘛的
- app.controller.ts
这里控制层,这里主要是写路由相关代码以及处理前端传来的一些参数(后面会介绍如何接收参数)
- app.service.ts
这里是业务层,在这里写一些与业务相关的逻辑。比如对数据库的 CRUD 就可以写到这里
- app.module.ts
它可以组织应用程序中的许多功能,如控制器、服务以及可以导入其他模块等
- main.ts
这个是整个程序的入口文件
执行命令
npm run start:dev
就可以启动服务了,浏览器打开http://localhost:3000/即可发送一个 get 请求到app.controller.ts中的 getHello 函数,调用app.service.ts里的 getHello 函数返回Hello World!
这里我们发现 app.controller.ts 的 appService 并没有实例化就可以直接使用了,其实这里是因为在 app.module.ts 已经进行了依赖注入(providers)这里已经将其处理好了
装饰器
在上面的代码中我们看到了譬如@Controller(),@get(),@Module()之类的代码,其实这些就是装饰器,你可以把它看作一个函数就行了,比如@Controller()它属于一个类装饰器,他会把下面的类当作参数传入然后进行一些处理从而实现处理路由的功能,这里简单举个例子帮助大家了解一下装饰器
const Get: ClassDecorator = (target: any) => {
target.prototype.type = "get";
};
@Get
class QueryData {
type: string;
constructor(public url: string) {}
}
const getData = new QueryData("xxxx");
console.log(getData.type); //get
这里可以看出类装饰器会将整个 QueryData 类当成参数传递给 Get 函数,在 Get 函数中对这个类进行一些处理。当然除了类装饰器,还用属性装饰器,方法装饰器等,它们的原理也都类似
为了更好的演示 Nest 中装饰器的功能,我们需要新建一个模块,NestJS 给我们提供了一些命令可以创建对应文件,非常方便,比如
- 生成一个 module (nest g mo) 。
- 生成一个 controller (nest g co) 。
- 生成一个 service (nest g s) 。
你可以执行nest -h来查阅这些命令及它们的缩写
我们可以执行nest g res user生成一个 user 模块,包括它的module,controller,service,执行之前可以在 nest-cli.json 中配置
"generateOptions": {
"spec": false
}
这样就不会生成测试文件(我们这里还不需要),执行命令我们可以选择 REST API 的形式,这样我们 src 下就会出现了 user 模块,同时在 app.module.ts 也进行了自动导入,也就是说创建完毕之后直接就可以使用了
这里我们可以看到user.controller.ts引入了很多装饰器,并且已经给我们写好了 CRUD 的模板
其中@Controller('user')是路由装饰器,它定义的是请求路径是user。
而像@Post,@Get,@Patch等就是对应的请求方式装饰器,比如你用 POST 请求调用http://localhost:3000/user就会进入@Post()下面的 create()方法,当然你也可以这样写@Post('login'),这样请求路径就会变成/user/login了
@Body,@Params则是请求参数装饰器,我们可以从中获取到前端传来的参数
拿第一个 Post 请求举例,发送 post 请求我们可以使用postman,apifox等工具进行测试,这里我使用apifox进行演示
我们可以先打印一下@Body()装饰的 createUserDto,然后发送一个 Post 请求
这时候我们会发现请求返回了ok,并且控制台打印了传过去的参数
看到这有小伙伴就会问了 CreateUserDto 干啥的?其实它是用来描述数据形状的,也就是说它可以定义应该接受前端传来的什么参数,参数类型等,比如在 create-user.dto.ts 中可以这样定义
export class CreateUserDto {
username: string;
}
如果你想获取 Get 请求传来的参数可以使用@Query,获取 Header 中的参数可以使用@Header 等等,这些装饰器有很多,后面的实战过程中遇到会作一个详细说明。
统一的异常过滤
有时候前端会进行一些错误的请求,这时候我们需要返回给他一个异常告知他的请求有问题,我们可以使用 NestJS 内置的异常处理 HttpException 比如
throw new HttpException('您无权登录', HttpStatus.FORBIDDEN);
客户端就会收到
{
"statusCode": 403,
"message": "您无权登录"
}
但是这样不够灵活,比如我们并不想使用statusCode这个字段等,这时候我们可以自定义一个异常过滤器
nest g filter common/filter/http-exception
将 http-exception.filter.ts 修改为
//http-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
} from '@nestjs/common';
import { Request, Response } from 'express';
@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({
code: status,
timestamp: new Date().toISOString(),
path: request.url,
describe: exception.message,
});
}
}
最后还需要在 main.ts 中将其注册为全局过滤器
这时候我们找个接口抛出一个错误码试一下
在 apifox 请求一下就可以看到,错误信息变成了我们自定义的格式了
统一的请求成功拦截器
我们还需要一个返回格式的拦截器对请求成功(状态码为 2xx)的数据进行一个格式化,比如返回这样的格式
{
data:业务参数,
code:状态码,
describe:状态描述
...
}
这就需要用的 Nest 中的拦截器了(Interceptor),我们可以使用命令
nest g interceptor common/interceptor/transform
这样就在 common 目录下创建了一个拦截器,根据官网提供的例子修改一下transform.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { map } from "rxjs/operators";
export interface Response<T> {
data: T;
}
@Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, Response<T>>
{
intercept(
context: ExecutionContext,
next: CallHandler
): Observable<Response<T>> {
return next
.handle()
.pipe(map((data) => ({ code: 200, data, describe: "请求成功" })));
}
}
同样的在 main.ts 中进行全局注册一下
此时我们尝试返回一个成功的请求看下结果
可以发现返回的结果已经被格式化了
自定义业务异常
到这里细心的同学可能会发现,每次我们抛出异常的时候抛出的都是请求状态码的异常,前端只能在捕获错误 catch 中获得返回的信息。但是一般情况下除了状态码的异常之外前端还需要一些业务的异常码,比如以上面我们定义的 code 字段为例,用户登录时密码错误可以返回 code 为 10001,验证码错误可以返回 10002 之类的,但是这些个请求的状态码应该是正确的 200 而不是抛出异常状态码。因此我们的程序还需要能自定义业务异常
我们先创建 common/enums/api-error-code.enum.ts 用于存放我们的业务状态码,这里简单写几个
export enum ApiErrorCode {
SUCCESS = 200, // 成功
USER_ID_INVALID = 10001, // 用户id无效
USER_NOTEXIST = 10002, // 用户id无效
COMMON_CODE = 20000, //通用错误码,想偷懒就返回这个
}
然后我们在http-exception目录下新建api.exception.ts用于处理业务异常,创建一个ApiException类继承HttpException,接受三个参数错误信息,错误码code,http状态码(默认是200)
import { HttpException, HttpStatus } from '@nestjs/common';
import { ApiErrorCode } from '../../enums/api-error-code.enum';
export class ApiException extends HttpException {
private errorMessage: string;
private errorCode: ApiErrorCode;
constructor(
errorMessage: string,
errorCode: ApiErrorCode,
statusCode: HttpStatus = HttpStatus.OK,
) {
super(errorMessage, statusCode);
this.errorMessage = errorMessage;
this.errorCode = errorCode;
}
getErrorCode(): ApiErrorCode {
return this.errorCode;
}
getErrorMessage(): string {
return this.errorMessage;
}
}
然后修改http-exception.filter.ts,可以判断exception是否在 ApiException 原型链上来确定是调用的是 ApiException 还是 HttpException
我们尝试抛出一个业务异常看下
请求看下结果
可以看到状态码是 200,code 是 10001,符合我们的预期。ok,到这里我们就已经完成了我们项目的基本配置
总结
本节主要介绍了 NestJS 的基本使用,以及项目的一些基本配置:
- NestJS 装饰器
- 统一的异常过滤器
- 统一的请求成功拦截器
- 自定义的业务异常
其中提到了过滤器以及拦截器,这里可能会有一些同学不了解它们是干嘛的,可以看一下我写过的一篇文章一篇文章带你了解 NestJS 中的 AOP 架构(中间件,拦截器,守卫,异常过滤器,管道)相信你一定有所收获