Node框架之NestJS入门学习

Posted 十九万里

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Node框架之NestJS入门学习相关的知识,希望对你有一定的参考价值。

一 概述

文章主要分为三部分:

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() 装饰器定义控制器 路由前缀为cats
export class CatsController 
  @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],
)
export class CatsModule 
 // 导出模块 

然后导入根模块便可直接使用

import  Module  from '@nestjs/common';
import  CatsModule  from './cats/cats.module';

@Module(
  imports: [CatsModule],
)
export class ApplicationModule 

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返回的数据,里面是具体逻辑
export class LoggerMiddleware implements NestMiddleware 
  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],
)
export class AppModule implements NestModule 
  configure(consumer: MiddlewareConsumer)  // configure方法用于设置模块中间件。
    consumer
      .apply(LoggerMiddleware) // 把LoggerMiddleware设置在ApplicationModule 层上。
      // LoggerMiddleware可以是函数,如果是函数的话称为函数中间件
       // 这个配置是把包含路径的对象和请求方法都传递给forRoutes 做进一步处理
      .forRoutes( path: 'cats', method: RequestMethod.GET );
  


中间件也支持函数式中间件 多个中间件 全局中间件,下面是三类中间件的写法


export class AppModule implements NestModule 
  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()
async findAll() 
  throw new HttpException(
    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')
async findOne(@Param('id', ParseIntPipe) id: number) 
  return this.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()
export class AuthGuard implements CanActivate  // CanActivate守卫接口
  canActivate(
    // canActivate() 函数接收单个参数 ExecutionContext 实例。ExecutionContext 继承自 ArgumentsHost
    context: ExecutionContext,  // ExecutionContext执行上下文
  ): boolean | Promise<boolean> | Observable<boolean> 
    const request = context.switchToHttp().getRequest();
    return validateRequest(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 new nest-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.ts
import  Module  from '@nestjs/common';
 
@Module()
export class UserModule 

同时还会在根模块 app.module.ts 中引入 UserModule 这个模块,相当于一个树形结构,在根模块中引入了 User 模块。
执行上面的终端命令之后,会发现app.module.ts 中的代码已经发生了变化,在文件顶部自动引入了 UserModule,同时也在 @Module 装饰器的 imports 中引入了 UserModule。这就是nest的方便之处,自动生成模块化代码。

// app.module.ts
import  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]
)
export class AppModule 

创建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
会发现数据库中的内容已经被删除了。
至此,已经初步实现了对数据库的增删改查操作。

以上是关于Node框架之NestJS入门学习的主要内容,如果未能解决你的问题,请参考以下文章

前端学习入门之React

前端框架之VUE

vuejs基础入门之环境搭建

2021: 值得关注/学习的前端框架和工具库汇总

node进阶一文带你快速入门koa框架

使用 NestJS 框架分析概念实现