一 概述
文章主要分为三部分:
1 NestJS 与Egg对比
2 NestJS 基础知识介绍
3 使用NestJS快速搭建应用,实现CURD操作
官网链接:
e-link文档 Egg官网 NestJS 官网 NestJS中文文档(非官方)
二 Nest 与Egg区别
1、Slogan
Egg:为企业框架和应用而生。
NestJS :用于构建高效、可伸缩的服务端应用程序的渐进式 Node.js 框架。
从两个框架的口号上可以看出,Egg更加关注企业维度,NestJS更注重项目维度。
2、技术分析
Egg:
1、环境:Egg开发文档比较完善,东西写的也很齐全,国内社区丰富,公司内部有对应的node团队能支持。
2、可选用JS以及TS 开发,两者都是基于Classify开发,对于刚刚接触服务端开发的前端更友好。
3、约定优于配置,减少开发负担,团队学习成本和协作成本更低。
4、高度可扩展的插件机制,方便定制插件,方便项目的开发。
5。内置集群:使用Cluster,自带进程守护,多进程以及进程通讯等功能。
NestJS:
1、环境:开发文档一般,有中文文档但是不和官网同步,国内社区相对少,公司内部没有团队支持。
2、使用TS语法编程结合OOP(面向对象编程),FP(函数式编程)和FRP(函数式响应编程)的元素,学习成本会高于Egg,更偏向于JAVA和Angular框架的风格,对新手前端不友好。
3、模块加载方面使用loC模式:模块容器-依赖注入(通过装饰器和元数据实现),在熟练之后开发效率和维护性会更高。
4、NestJS的框架和配套功能非常完善,列如:认证、鉴权、加密、文档、微服务、CLI工具等已经集成在Nest框架中,无需单独引入第三方功能模块。
综合对比
NestJS作为node框架,更加自由以及更偏向于后端的开发模式,Egg作为深度定制过的框架,自定义程度上回弱于NestJS,但是更容易上手,且公司内部已有封装好的功能组件,使用起来会更加方便。
上述对比并不能说哪一个框架更好,从作为一个纯前端学习的角度来说,是在接触了NestJS之后,才去了解了OOP,FP,FRP等偏向后端的扩展知识,而且在实际的开发学习中,NestJS的便捷性和逻辑性会更强,NestJS 自带的CLI能快速生成CURD模块,开发过程中很方便,极大的减少了我们重复的工作量。
目前egg好像不再维护了,听说去年核心团队成员都被裁。
三 Nest基础知识
注:很多概念在官网写的非常详细,下面几项是我以前端初学者的角度去看必备的概念,本节主要介绍一些Nest的基础概念以及语法
1 Contrillers 控制器
控制器:
控制器负责处理传入的请求和向客户端返回的响应,目的是接收应用的特定请求
路由:
路由机制控制哪个控制器接收哪些请求。每个控制器有多个路由,不同的路由可以执行不同的操作。
星号作为路由通配符在路由中使用
Request:
@Req()装饰器是NEst提供对底层平台的请求对象(request)的访问方式,把nest请求对象打开到程序层面,用于访问和处理客户端返回的请求细节,nest提供的装饰器对照列表如下,具体用法在后面的代码案例中展示:
资源:
Nest 为所有标准的 HTTP 方法提供了相应的装饰器:@Put()、@Delete()、@Patch()、@Options()、以及 @Head()。此外,@All() 则用于定义一个用于处理所有 HTTP 请求方法的处理程序
**状态码:
**在处理函数外添加 @HttpCode(…) 装饰器来更改
Headers:
自定义响应头,可以使用 @header() 装饰器或类库特有的响应对象,(并直接调用 res.header())
重定向:
1、使用 @Redirect() 装饰器。
2、特定于库的响应对象(并直接调用 res.redirect())
几个需要注意的点:
使用 CLI 创建控制器, $ nest g controller cats
HttpCode Header Param 以及get的方法 需要从 @nestjs/common 包导入。
代码案例:
import{ Controller, Get, Query, Post, Body, Put, Param, Delete }from'@nestjs/common';// 导入控制器参数等,固定写法import{ CreateCatDto, UpdateCatDto, ListAllEntities }from'./dto';// 导入创建的三个类 创建,更新ODT
@Controller('cats')// 使用@Controller() 装饰器定义控制器 路由前缀为catsexportclassCatsController{
@Post()// post装饰器处理http的post请求方法,create(@Body() createCatDto: CreateCatDto){return'This action adds a new cat';}
@Get()// get装饰器处理http的get请求方法,findAll(@Query() query: ListAllEntities){return`This action returns all cats (limit: ${query.limit} items)`;}
@Get(':id')// 路由参数 可以通过params.id来访问 为了接受动态数据作为请求的一部分findOne(@Param('id') id: string){//@Param() 用于修饰一个方法的参数,并在该方法内将路由参数作为被修饰的方法参数的属性return`This action returns a #${id} cat`;}// @Body() 参数处理程序没有接受到任何客户端参数的问题 实现请求负载,调用UpdateCatDto类
@Put(':id')update(@Param('id') id: string, @Body() updateCatDto: UpdateCatDto){return`This action updates a #${id} cat`;}
@Delete(':id')remove(@Param('id') id: string){return`This action removes a #${id} cat`;}}
2 Providers 依赖注入
依赖注入:
依赖注入是实现控制反转的一种设计方法,控制反转是思想,依赖注入是具体的事项方式,简称DI,类之间的依赖关系由容器来负责。简单来讲a依赖b,但a不创建(或销毁)b,仅使用b,b的创建(或销毁)交给容器。
3 Modules 模块
模块是具有 @Module() 装饰器的类。 @Module() 装饰器提供了元数据,Nest 用它来组织应用程序结构
功能模块
当两个类CatsController,CatsService属于同一个应用程序域,可以写为一个模块,语法如下
全局模块使用@Global() 装饰器 使用全局模块后,只能注册一次,不需要在inports数组中导入CatsModule
代码案例:
import{ Module }from'@nestjs/common';import{ CatsController }from'./cats.controller';import{ CatsService }from'./cats.service';// 模块被创建后,就能被其他模块重复使用。// @Global() 可选。增加后就变成了全局模块
@Module({controllers:[CatsController],providers:[CatsService],})exportclassCatsModule{}// 导出模块
然后导入根模块便可直接使用
import{ Module }from'@nestjs/common';import{ CatsModule }from'./cats/cats.module';
@Module({imports:[CatsModule],})exportclassApplicationModule{}
4 Middleware 中间件
概念:中间件是在路由处理程序之前调用的函数,可以访问请求和响应对象,以及应用程序请求响应周期中的next()中间件函数, next() 中间件函数通常由名为 next 的变量表示。
具体作用:
1 执行任何代码
2 对请求和响应对象进行更改
3 结束请求-响应周期
4 调用堆栈中的下一个中间件函数
5 如果当前中间件函数没有结束请求-响应周期,必须调用next()将控制传递给下一个中间件函数,否则请求会被挂起
代码案例:
@Injectable() 装饰器的类中实现自定义 Nest中间件 ,同时装饰器的类应该实现NestMiddleware接口
import{ Injectable, NestMiddleware }from'@nestjs/common';import{ Request, Response, NextFunction }from'express';
@Injectable()// 使用NestMiddleware接口处理LoggerMiddleware返回的数据,里面是具体逻辑exportclassLoggerMiddlewareimplementsNestMiddleware{use(req: Request,res: Response,next: NextFunction){
console.log('Request...');next();}}
nest的中间件依旧支持依赖注入,需要通过constructor使用
中间件不能在 @Module() 装饰器中使用。需要 configure() 方法来设置。实现 NestModule 接口。下面这个例子是把 LoggerMiddleware 设置在 ApplicationModule 层上。
import{ Module, NestModule, MiddlewareConsumer }from'@nestjs/common';import{ LoggerMiddleware }from'./common/middleware/logger.middleware';// 导入自定义的中间件import{ CatsModule }from'./cats/cats.module';
@Module({imports:[CatsModule],})exportclassAppModuleimplementsNestModule{configure(consumer: MiddlewareConsumer){// configure方法用于设置模块中间件。consumer.apply(LoggerMiddleware)// 把LoggerMiddleware设置在ApplicationModule 层上。// LoggerMiddleware可以是函数,如果是函数的话称为函数中间件// 这个配置是把包含路径的对象和请求方法都传递给forRoutes 做进一步处理.forRoutes({path:'cats',method: RequestMethod.GET});}}
中间件也支持函数式中间件 多个中间件 全局中间件,下面是三类中间件的写法
exportclassAppModuleimplementsNestModule{configure(consumer: MiddlewareConsumer){// configure方法用于设置模块中间件。
consumer
// 把LoggerMiddleware类设置在ApplicationModule 层上。// LoggerMiddleware可以是函数,如果是函数的话称为函数中间件.apply(LoggerMiddleware)// 多个中间件使用,逗号分开// .apply(cors(), helmet(), logger).forRoutes(CatsController);// 这个配置是把包含路径的对象和请求方法都传递给forRoutes 做进一步处理.forRoutes({path:'cats',method: RequestMethod.GET});}}// 全局中间件需要使用到INestApplication实例提供的 use()方法:const app =await NestFactory.create(AppModule);
app.use(logger);await app.listen(3000);
5 Exception filters异常过滤器
nest的内置了异常过滤器,并不需要单独引入,作用是捕获所有应用程序中抛出的异常,并做出相应
Nest提供了一个内置的 HttpException 类,它从 @nestjs/common 包中导入。然后在具体的方法中new,并返回信息
import{ HttpException, HttpStatus }from'@nestjs/common';......
@Get()asyncfindAll(){thrownewHttpException({status: HttpStatus.FORBIDDEN,// HttpStatus是辅助枚举器 根据http code码判断错误error:'This is a custom message',// 返回到客户端的信息}, HttpStatus.FORBIDDEN);}
nest过滤器也支持自定义过滤器和全局过滤器,
6 Pipes 管道
管道是nest特色之一,是具有 @Injectable() 装饰器的类。管道应实现 PipeTransform 接口。
主要有两个应用场景
- 转换:管道将输入数据转换为所需的数据输出(例如,将字符串转换为整数)
- 验证:对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常
管道支持自定义 但是nest也自带了九个开箱即用的内置管道 都能直接从@nestjs/common 包中导出。(一直在更新,上一回看还是8个)
ValidationPipe 验证管道
ParseIntPipe 将字符串数字转整数
ParseFloatPipe 将字符串数字转浮点数
ParseBoolPipe 转布尔值
ParseArrayPipe 转化成数组类型
ParseUUIDPipe 解析字符串并验证是否为UUID
ParseEnumPipe 转化成枚举类型
DefaultValuePipe 设置参数默认值
ParseFilePipe 转化成文件类型
代码案例:ParseIntPipe
需求: 把管道和特定的路由处理程序方法关联,确保在路由被调用之前被运行,绑定管道如下:
import{ParseIntPipe }from'@nestjs/common';......
@Get(':id')asyncfindOne(@Param('id', ParseIntPipe) id: number){returnthis.catsService.findOne(id);}// 如果接受的路由参数不是int类型的 会抛出下面的异常{"statusCode":400,"message":"Validation failed (numeric string is expected)","error":"Bad Request"}
同样的 管道也支持自定义管道,全局管道,由于篇幅限制,这里就不展开说了,具体方法参考:管道使用方法链接
7 Guards Interceptors 守卫 拦截器
守卫是一个使用 @Injectable() 装饰器的类。 守卫应该实现 CanActivate 接口。
守卫有一个单独的责任。根据运行时出现的某些条件(例如权限,角色,访问控制列表等)来确定给定的请求是否由路由处理程序处理。称为授权
守卫在每个中间件之后执行,但在任何拦截器或管道之前执行。
还是通过一个案例来展开:
授权是守卫的一个功能 当用户有足够的权限时 才能调用特定的路由 ,下面案例中构建的 AuthGuard 假设用户是经过身份验证的(因此,请求头附加了一个token)。它将提取和验证token,并使用提取的信息来确定请求是否可以继续。
import{ Injectable, CanActivate, ExecutionContext }from'@nestjs/common';import{ Observable }from'rxjs';
@Injectable()exportclassAuthGuardimplementsCanActivate{// CanActivate守卫接口canActivate(// canActivate() 函数接收单个参数 ExecutionContext 实例。ExecutionContext 继承自 ArgumentsHostcontext: ExecutionContext,// ExecutionContext执行上下文): boolean | Promise<boolean>| Observable<boolean>{const request = context.switchToHttp().getRequest();returnvalidateRequest(request);}}
8 其他
nest可以通过cli来快速搭建项目,创建类,控制器等,这也是nest的特色之一
安装cli
$ npm install -g @nestjs/cli
$ nest generate --help // 可获取所有的cli命令
// 例如
$ nest new my-nest-project --dry-run
newn搭建一个新的标准模式应用程序,包含所有需要运行的样板文件。generateg根据原理图生成或修改文件。build将应用程序或 workspace 编译到输出文件夹中。start编译并运行应用程序(或 workspace 中的默认项目)。add导入已打包为nest的库,运行其安装示意图。infoi显示已安装的nest包和其他有用的系统信息。
cli常用命令:
$ nest g service cats // 创建服务类
$ nest g controller cats // 创建控制器
四 Nest快速搭建应用
1 构建项目
// 推荐使用nestCLI安装 方法一
npm i -g @nestjs/cli
nest newnest-crud-demo
// git安装 (不推荐) 方法二
git clone https://github.com/nestjs/typescript-starter.git nest-crud-demo
// 两条命令的效果完全一致
项目目录如下
具体含义:
app.controller.ts单个路由的基本控制器(Controller)app.controller.spec.ts针对控制器的单元测试app.module.ts应用程序的根模块(Module)app.service.ts具有单一方法的基本服务(Service)main.ts应用程序的入口文件,它使用核心函数 NestFactory 来创建 Nest 应用程序的实例。
2 启动服务
cd nest-crud-demo
npm run start:dev 或者 yarn run start:dev
// 启动的时候应该以 dev 模式启动,这样 Nest 会「自动检测我们的文件变化」,然后「自动重启服务」。// 如果是直接 npm start 或者 yarn start 的话,修改文件需要重新启动服务
3 安装依赖
注:本文采用的数据库为mongdb作为示例。没有安装mongdb环境的需要先安装数据库后才可进行连接。
Nest 官方提供了 Mongoose 的封装,我们需要安装 mongoose 和 @nestjs/mongoose:
nest g module user server
4 编写代码
创建 Module
目标是创建一个 User 模块,写一个用户增删改查
nest g module user server
脚手架会自动在 src/server/user 文件夹下创建一个 user.module.ts,这是 Nest 的模块文件,Nest 用它来组织整个应用程序的结构。
// user.module.tsimport{ Module }from'@nestjs/common';
@Module({})exportclassUserModule{}
同时还会在根模块 app.module.ts 中引入 UserModule 这个模块,相当于一个树形结构,在根模块中引入了 User 模块。
执行上面的终端命令之后,会发现app.module.ts 中的代码已经发生了变化,在文件顶部自动引入了 UserModule,同时也在 @Module 装饰器的 imports 中引入了 UserModule。这就是nest的方便之处,自动生成模块化代码。
// app.module.tsimport{ Module }from'@nestjs/common';import{ AppController }from'./app.controller';import{ AppService }from'./app.service';import{ UserModule }from'./server/user/user.module';// 自动引入
@Module({imports:[UserModule],// 自动引入controllers:[AppController],providers:[AppService]})exportclassAppModule{}
创建Controller
nest g controller user server
在 Nest 中,controller 就类似前端的「路由」,负责处理「客户端传入的请求」和「服务端返回的响应」。
举个例子,我们如果要通过 http://localhost:3000/user/users 获取所有的用户信息,那么我们可以在 UserController 中创建一个 GET 方法,路径为 users 的路由,这个路由负责返回所有的用户信息。
// user.controller.ts
import { Controller, Get } from '@nestjs/common';
@Controller('user')
export class UserController {
@Get('users')
findAll(): string {
return "All User's Info"; // [All User's Info] 暂时代替所有用户的信息
}
}
这就是 controller 的作用,负责分发和处理「请求」和「响应」。
当然,也可以把 findAll 方法写成异步方法,像这样:
// user.controller.ts
import { Controller, Get } from '@nestjs/common';
@Controller('user')
export class UserController {
@Get('users')
async findAll(): Promise<any> {
return await this.xxx.xxx(); // 一些异步操作
}
}
创建Providers
nest g service user server
providers 我们可以简单地从字面意思来理解,就是「服务的提供者」。
举个例子,我们的 controller 接收到了一个用户的查询请求,我们不能直接在 controller 中去查询数据库并返回,而是要将查询请求交给 provider 来处理,这里我们创建了一个 UserService,就是用来提供「数据库操作服务」的。
// user.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserService {}
当然,providers 不一定只能用来提供数据库的操作服务,还可以用来做一些用户校验,比如使用 JWT 对用户权限进行校验的策略,就可以写成一个策略类,放到 provider 中,为模块提供相应的服务。
controller 和 providers 都创建完后,我们又会惊奇地发现,user.module.ts 文件中多了一些代码,变成了这样:
// user.module.ts
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';
@Module({
controllers: [UserController],
providers: [UserService]
})
export class UserModule {}
5 连接数据库
引入Mongoose模块
连接数据之前,我们要先在根模块,也就是 app.module.ts 中引入 Mongoose 的连接模块:
// app.module.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './server/user/user.module';
@Module({
imports: [MongooseModule.forRoot('mongodb://localhost/3000'), UserModule],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {}
这时候保存文件,会发现控制台还是报错的,报错信息显示 mongoose 模块没有类型声明文件,这就很容易解决了,安装一下就好:
npm install @types/mongoose --dev 或者 yarn add @types/mongoose --dev
安装完之后服务就正常重启了。
引入Mongoose模块
这里我们先要创建一个数据表的格式,在 src/server/user 文件夹下创建一个 user.schema.ts 文件,定义一个数据表的格式:
// user.schema.ts
import { Schema } from 'mongoose';
export const userSchema = new Schema({
_id: { type: String, required: true }, // 覆盖 Mongoose 生成的默认 _id
user_name: { type: String, required: true },
password: { type: String, required: true }
});
然后将我们的 user.module.ts 文件修改成这样:
// user.module.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { UserController } from './user.controller';
import { userSchema } from './user.schema';
import { UserService } from './user.service';
@Module({
imports: [MongooseModule.forFeature([{ name: 'Users', schema: userSchema }])],
controllers: [UserController],
providers: [UserService]
})
export class UserModule {}
6 CRUD
我们打开 user.service.ts 文件,为 UserService 类添加一个构造函数,让其在实例化的时候能够接收到数据库 Model,这样才能在类中的方法里操作数据库。
// user.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { CreateUserDTO, EditUserDTO } from './user.dto';
import { User } from './user.interface';
@Injectable()
export class UserService {
constructor(@InjectModel('Users') private readonly userModel: Model<User>) {}
// 查找所有用户
async findAll(): Promise<User[]> {
const users = await this.userModel.find();
return users;
}
// 查找单个用户
async findOne(_id: string): Promise<User> {
return await this.userModel.findById(_id);
}
// 添加单个用户
async addOne(body: CreateUserDTO): Promise<void> {
await this.userModel.create(body);
}
// 编辑单个用户
async editOne(_id: string, body: EditUserDTO): Promise<void> {
await this.userModel.findByIdAndUpdate(_id, body);
}
// 删除单个用户
async deleteOne(_id: string): Promise<void> {
await this.userModel.findByIdAndDelete(_id);
}
}
因为 mongoose 操作数据库其实是异步的,所以这里我们使用 async 函数来处理异步的过程。
上面新增的两个文件一个是 user.interface.ts,另一个是 user.dto.ts,需要创建一下:
// user.interface.ts
import { Document } from 'mongoose';
export interface User extends Document {
readonly _id: string;
readonly user_name: string;
readonly password: string;
}
// user.dto.ts
export class CreateUserDTO {
readonly _id: string;
readonly user_name: string;
readonly password: string;
}
export class EditUserDTO {
readonly user_name: string;
readonly password: string;
}
其实就是对数据类型做了一个定义。
现在,可以到 user.controller.ts 中设置路由了,将「客户端的请求」进行处理,调用相应的服务实现相应的功能:
// user.controller.ts
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put
} from '@nestjs/common';
import { CreateUserDTO, EditUserDTO } from './user.dto';
import { User } from './user.interface';
import { UserService } from './user.service';
interface UserResponse<T = unknown> {
code: number;
data?: T;
message: string;
}
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
// GET /user/users
@Get('users')
async findAll(): Promise<UserResponse<User[]>> {
return {
code: 200,
data: await this.userService.findAll(),
message: 'Success.'
};
}
// GET /user/:_id
@Get(':_id')
async findOne(@Param('_id') _id: string): Promise<UserResponse<User>> {
return {
code: 200,
data: await this.userService.findOne(_id),
message: 'Success.'
};
}
// POST /user
@Post()
async addOne(@Body() body: CreateUserDTO): Promise<UserResponse> {
await this.userService.addOne(body);
return {
code: 200,
message: 'Success.'
};
}
// PUT /user/:_id
@Put(':_id')
async editOne(
@Param('_id') _id: string,
@Body() body: EditUserDTO
): Promise<UserResponse> {
await this.userService.editOne(_id, body);
return {
code: 200,
message: 'Success.'
};
}
// DELETE /user/:_id
@Delete(':_id')
async deleteOne(@Param('_id') _id: string): Promise<UserResponse> {
await this.userService.deleteOne(_id);
return {
code: 200,
message: 'Success.'
};
}
}
7 接口测试
接口测试用的是 Postman。数据库可视化工具用的是 MongoDB 官方的 MongoDB Compass。
GET /user/users
GET /user/users 一开始我们的数据库中什么都没有,所以返回了一个空数组,没用用户信息。
POST /user
POST /user
现在我们添加一条用户信息,服务器返回添加成功。
Added
GET /user/:_id
GET /user/:_id
添加完一条用户信息之后再查询,可算是能查询到我的信息了。
PUT /user/:_id
PUT /user/:_id
现在假如我想修改密码,发送一个 PUT 请求。
Edited
DELETE /user/:_id
DELETE /user/:_id
现在我们删除一下刚才添加的用户信息。
Deleted
会发现数据库中的内容已经被删除了。
至此,已经初步实现了对数据库的增删改查操作。
版权归原作者 十九万里 所有, 如有侵权,请联系我们删除。