NestJs 上手之路之一 【快速完成CRUD】

3,154 阅读9分钟

前言

众所周知,NodeJs框架有很多。比如:Express、Koa、Egg、Hapi、Meteor... 所以为什么是NestJs更吸引我?一直没有上手NodeJs去开发一些东西是因为,我会一点点JAVA,在我的认知里不管是什么语言,只要我能搭建一个后台服务就可以了。当然也没有更多时间去深入学习每一个语言和框架... NestJs确实像大家就一直说的那样,用过AngularJs的则觉得他俩很像,学过Java的则觉得和spring很像,确实我选择后者,他们确实某方面太像了,比如依赖注入就非常的copy...

介绍

Nest 是一个用于构建高效,可扩展的 Node.js 服务器端应用程序的框架。它使用渐进式 JavaScript,内置并完全支持 TypeScript(但仍然允许开发人员使用纯 JavaScript 编写代码)并结合了 OOP(面向对象编程),FP(函数式编程)和 FRP(函数式响应编程)的元素。

在底层,Nest使用强大的 HTTP Server 框架,如 Express(默认)和 Fastify。Nest 在这些框架之上提供了一定程度的抽象,同时也将其 API 直接暴露给开发人员。这样可以轻松使用每个平台的无数第三方模块。

Nest 提供了一个开箱即用的应用程序架构,允许开发人员和团队创建高度可测试,可扩展,松散耦合且易于维护的应用程序。

安装

其实学习一个新知识的时候,非常简单且快速方法就是直接看官方的文档,以下是nest.js的官方文档和中文文档地址:

nestjs.com/

docs.nestjs.cn/

请确保您的操作系统上安装了 Node.js (>= 10.13.0, v13 版本除外)

使用 Nest CLI 建立新项目非常简单。 在安装好 npm 后,您可以使用下面命令在您的 OS 终端中创建 Nest 项目:

$ npm i -g @nestjs/cli
$ nest new project-name

将会创建 project-name 目录, 安装 node_modules 和一些其他样板文件,并将创建一个 src 目录,目录中包含几个核心文件。

src
 ├── app.controller.spec.ts
 ├── app.controller.ts
 ├── app.module.ts
 ├── app.service.ts
 └── main.ts

安装过程完成后,您可以在系统命令行工具中运行以下命令,以启动应用程序:

$ npm run start

此命令启动 HTTP 服务监听定义在 src/main.ts 文件中定义的端口号。在应用程序运行后, 打开浏览器并访问 http://localhost:3000/。 你应该看到 Hello world! 信息。

CRUD生成器

在项目根目录下执行以下代码来生成资源,users是你要写的业务模块

$ nest g resource users
// 像这样传递`--no-spec`参数`nest g resource users --no-spec`来避免生成测试文件
$ nest g resource users --no-spec

生成时会让你选择生成资源的类型,我们选择REST API即可 这里生成一个处理器类而不是一个REST API控制器:

$ nest g resource users

> ? What transport layer do you use? GraphQL (code first)
> ? Would you like to generate CRUD entry points? Yes
> CREATE src/users/users.module.ts (224 bytes)
> CREATE src/users/users.resolver.spec.ts (525 bytes)
> CREATE src/users/users.resolver.ts (1109 bytes)
> CREATE src/users/users.service.spec.ts (453 bytes)
> CREATE src/users/users.service.ts (625 bytes)
> CREATE src/users/dto/create-user.input.ts (195 bytes)
> CREATE src/users/dto/update-user.input.ts (281 bytes)
> CREATE src/users/entities/user.entity.ts (187 bytes)
> UPDATE src/app.module.ts (312 bytes)

将会生成

src
 ├── users
 |  ├── dto
 |  |   ├── create-user.dto.ts
 |  |   └── update-user.dto.ts
 |  ├── entities
 |  |   └── user.entity.ts
 |  ├── users.controller.ts
 |  ├── users.module.ts
 |  └── users.service.ts
 ├── app.controller.spec.ts
 ├── app.controller.ts
 ├── app.module.ts
 ├── app.service.ts
 └── main.ts

dto是我们新增和修改的时候来定义实体的结构,entities大多事使我们查询一个对象返回的时候定义的实体,里面的字段有些会不同,所以就直接区分开来。这是我认为的观点。。。

如下是一个生成的控制器 (REST API):

@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);
  }

  @Put(':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);
  }
}

在下面我们可以看到,不仅生成了所有变更和查询的样板文件,也把他们绑定到了一起,我们可以使用UsersServiceUser Entity, 和DTO

import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

@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`;
  }
}

至此为止,一个users的crud就生成好了,在编写接口连接数据库之前,为了后期更好的呈现我们的接口,我们先把接口文档接入。

Swagger

OpenAPI(Swagger)规范是一种用于描述 RESTful API 的强大定义格式。 Nest 提供了一个专用模块来使用它。

安装

首先,您必须安装所需的包(安装各种包的时候使用npm和yarn都可以,但是最好从一开始创建项目到后来安装包都尽量用同一种方式即可):

$ npm install --save @nestjs/swagger swagger-ui-express

如果你正在使用 fastify ,你必须安装 fastify-swagger 而不是 swagger-ui-express :

$ npm install --save @nestjs/swagger fastify-swagger

引导(Bootstrap)

安装过程完成后,打开引导文件(主要是 main.ts )并使用 SwaggerModule 类初始化 Swagger:

import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ApplicationModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(ApplicationModule);

  const options = new DocumentBuilder()
    .setTitle('Cats example')
    .setDescription('The cats API description')
    .setVersion('1.0')
    .addTag('cats')
    .build();
  const document = SwaggerModule.createDocument(app, options);
  SwaggerModule.setup('api', app, document);

  await app.listen(3000);
}
bootstrap();

现在,您可以运行以下命令来启动 HTTP 服务器:

$ npm run start

应用程序运行时,打开浏览器并导航到 http://localhost:3000/api 。 你应该可以看到 Swagger UI

img

SwaggerModule 自动反映所有端点。同时,为了展现 Swagger UI,@nestjs/swagger依据平台使用 swagger-ui-express 或 fastify-swagger

【友情提示】SwaggerModule.setup('api', app, document)中的'api'咱们可以换成一个又好的地址比如'doc'为很多后台接口有的以api开头,避免冲突咱们可以给文档接口地址起其他的名字。

装饰器

接下来我们整理一下我们的接口,并用装饰器来给我们接口做一下注释,由于我是用的article这个命名的,所以演示的都是以这个为基础。

image.png

image.png 以上是常用的接口描述,如果想了解更多可参考## Swagger

Mongoose

由于我写的例子是用的MondoDB数据库,而Mongoose 是最受欢迎的MongoDB 对象建模工具。

接口编写以及连接数据库操作

入门

在开始使用这个库前,我们必须安装所有必需的依赖关系

$ npm install --save mongoose
$ npm install --save-dev @types/mongoose

我们需要做的第一步是使用 connect() 函数建立与数据库的连接。connect() 函数返回一个 Promise,因此我们必须创建一个 异步提供者

在src下面创建一个文件夹database,然后在此文件夹下创建database.providers.ts、database.module.ts

database.providers.ts

import * as mongoose from 'mongoose';

export const databaseProviders = [
  {
    provide: 'DATABASE_CONNECTION',
    useFactory: (): Promise<typeof mongoose> =>
      mongoose.connect('mongodb://localhost:27017/nest-js'),
  },
];

然后,我们需要导出这些提供者,以便应用程序的其余部分可以 访问 它们。

database.module.ts

import { Module } from '@nestjs/common';
import { databaseProviders } from './database.providers';

@Module({
  providers: [...databaseProviders],
  exports: [...databaseProviders],
})
export class DatabaseModule {}

现在我们可以使用 @Inject() 装饰器注入 Connection 对象。依赖于 Connection 异步提供者的每个类都将等待 Promise 被解析。

模型注入

使用Mongoose,一切都来自Schema。 让我们定义 ArticleSchema :

src=>article=>schemas=>article.schema.ts

schemas/article.schema.ts

import * as mongoose from 'mongoose';

export const ArticleSchema = new mongoose.Schema({
  title: String,
  content: String,
  status: Number,
});

ArticleSchema 属于 article 目录。此目录代表 ArticleModule 。

现在,让我们创建一个 模型 提供者:

src=>article=>article.providers.ts

article.providers.ts

import { Connection } from 'mongoose';
import { ArticleSchema } from './schemas/article.schema';

export const articleProviders = [
  {
    provide: 'ARTICLE_MODEL',
    useFactory: (connection: Connection) =>
      connection.model('ARTICLE', ArticleSchema),
    inject: ['DATABASE_CONNECTION'],
  },
];

请注意,在实际应用程序中,您应该避免使用魔术字符串。ARTICLE_MODEL 和 DATABASE_CONNECTION 都应保存在分离的 constants.ts 文件中。

现在我们可以使用 @Inject() 装饰器将 ARTICLE_MODEL 注入到 ArticleService 中:

article.service.ts

import { Model } from 'mongoose';
import { Injectable, Inject } from '@nestjs/common';
import { CreateArticleDto } from './dto/create-article.dto';
import { UpdateArticleDto } from './dto/update-article.dto';
import { Article } from './entities/article.entity';

@Injectable()
export class ArticleService {
  constructor(
    @Inject('ARTICLE_MODEL')
    private articleModel: Model<Article>,
  ) {}
  async create(createArticleDto: CreateArticleDto): Promise<Article> {
    const createdArticle = new this.articleModel(createArticleDto);
    return createdArticle.save();
  }

  async findAll(): Promise<Article[]> {
    return this.articleModel.find().exec();
  }

  async findOne(id: string): Promise<Article> {
    return this.articleModel.findById(id);
  }

  async update(id: string, updateArticleDto: UpdateArticleDto) {
    await this.articleModel.findByIdAndUpdate(id, updateArticleDto);
    return {
      code: 200,
      msg: 'success',
    };
  }

  async remove(id: string) {
    await this.articleModel.findByIdAndDelete(id);
    return {
      code: 200,
      msg: 'success',
    };
  }
}

在上面的例子中,我们使用了 Article 接口。 此接口扩展了来自 mongoose 包的 Document :

src=>article=>entities=>article.entity.ts

import { Document } from 'mongoose';
export class Article extends Document {
  readonly title: string;
  readonly content: string;
  readonly status: number;
}

数据库连接是 异步的,但 Nest 使最终用户完全看不到这个过程。ArticleModel 正在等待数据库连接时,并且ArticleService 会被延迟,直到存储库可以使用。整个应用程序可以在每个类实例化时启动。

这是一个最终的 ArticleModule` :

article.module.ts

import { Module } from '@nestjs/common';
import { ArticleService } from './article.service';
import { ArticleController } from './article.controller';
import { articleProviders } from './article.providers';
import { DatabaseModule } from '../database/database.module';

@Module({
  imports: [DatabaseModule],
  controllers: [ArticleController],
  providers: [ArticleService, ...articleProviders],
})
export class ArticleModule {}

不要忘记将 ArticleModule 导入到根 AppModule 中。

app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ArticleModule } from './article/article.module';

@Module({
  imports: [
    ArticleModule,
  ],
  controllers: [AppController],
  providers: [
    AppService
  ],
})
export class AppModule {}

最后要修改article.controller.ts

article.controller.ts

1、将所有接口改成异步

import {
  Controller,
  Get,
  Post,
  Body,
  Patch,
  Param,
  Delete,
} from '@nestjs/common';
import { ApiTags, ApiOperation } from '@nestjs/swagger';
import { ArticleService } from './article.service';
import { CreateArticleDto } from './dto/create-article.dto';
import { UpdateArticleDto } from './dto/update-article.dto';

@Controller('article')
@ApiTags('文章')
export class ArticleController {
  constructor(private readonly articleService: ArticleService) {}

  @Post()
  @ApiOperation({ summary: '新增文章信息' })
  async create(@Body() createArticleDto: CreateArticleDto) {
    return await this.articleService.create(createArticleDto);
  }

  @Get()
  @ApiOperation({ summary: '查询文章列表' })
  async findAll() {
    return await this.articleService.findAll();
  }

  @Get(':id')
  @ApiOperation({ summary: '查询文章信息' })
  async findOne(@Param('id') id: string) {
    return await this.articleService.findOne(id);
  }

  @Patch(':id')
  @ApiOperation({ summary: '修改文章信息' })
  async update(
    @Param('id') id: string,
    @Body() updateArticleDto: UpdateArticleDto,
  ) {
    return await this.articleService.update(id, updateArticleDto);
  }

  @Delete(':id')
  @ApiOperation({ summary: '删除文章信息' })
  async remove(@Param('id') id: string) {
    return await this.articleService.remove(id);
  }
}

2、createArticleDto和updateArticleDto需要加上字段

create-article.dto.ts

import { ApiProperty } from '@nestjs/swagger';
import { IsInt, IsString, MaxLength } from 'class-validator';

export class CreateArticleDto {
  @ApiProperty({ description: '标题', example: '文章标题' })
  title: string;

  @ApiProperty({ description: '内容' })
  content: string;

  @ApiProperty({ description: '状态: 0启用 1禁用' })
  status: number;
}

update-article.dto.ts

import { PartialType, ApiProperty } from '@nestjs/swagger';
import { CreateArticleDto } from './create-article.dto';

export class UpdateArticleDto extends PartialType(CreateArticleDto) {
  // 这里首先继承CreateArticleDto的字段,
  // 如果需要其他的字段可以像下面注释的id字段一样添加
  // @ApiProperty({ description: 'id' })
  // id: string;
}

到此为止所有的增删改查已经完成,启动你的数据库,运行你的项目测试一下增删改查吧。 此时的目录结构将会改变成

src
 ├── article
 |  ├── dto
 |  |   ├── create-article.dto.ts
 |  |   └── update-article.dto.ts
 |  ├── entities
 |  |   └── article.entity.ts
 |  ├── schemas
 |  |   └── article.schemas.ts
 |  ├── article.controller.ts
 |  ├── article.module.ts
 |  ├── article.providers.ts
 |  └── article.service.ts
 ├── app.controller.spec.ts
 ├── app.controller.ts
 ├── app.module.ts
 ├── app.service.ts
 └── main.ts

类验证器

基本使用

本节中的技术需要 TypeScript ,如果您的应用是使用原始 JavaScript编写的,则这些技术不可用。

让我们看一下验证的另外一种实现方式

Nest 与 class-validator 配合得很好。这个优秀的库允许您使用基于装饰器的验证。装饰器的功能非常强大,尤其是与 Nest 的 Pipe 功能相结合使用时,因为我们可以通过访问 metatype 信息做很多事情,在开始之前需要安装一些依赖。

$ npm i --save class-validator class-transformer

安装完成后,我们就可以向 CreateCatDto 类添加一些装饰器。

所以我简单的把create-article.dto.ts修改了一下

import { ApiProperty } from '@nestjs/swagger';
import { IsInt, IsString, MaxLength } from 'class-validator';

export class CreateArticleDto {
  @ApiProperty({ description: '标题', example: '文章标题' })
  @IsString()
  @MaxLength(20)
  title: string;

  @ApiProperty({ description: '内容' })
  @IsString()
  @MaxLength(250)
  content: string;

  @ApiProperty({ description: '状态: 0启用 1禁用' })
  @IsInt()
  status: number;
}

扩展

现在我们来创建一个 ValidationPipe 类。

validate.pipe.ts

import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';

@Injectable()
export class ValidationPipe implements PipeTransform<any> {
  async transform(value: any, { metatype }: ArgumentMetadata) {
    if (!metatype || !this.toValidate(metatype)) {
      return value;
    }
    const object = plainToClass(metatype, value);
    const errors = await validate(object);
    if (errors.length > 0) {
      throw new BadRequestException('Validation failed');
    }
    return value;
  }

  private toValidate(metatype: Function): boolean {
    const types: Function[] = [String, Boolean, Number, Array, Object];
    return !types.includes(metatype);
  }
}

由于 ValidationPipe 被创建为尽可能通用,所以我们将把它设置为一个全局作用域的管道,用于整个应用程序中的每个路由处理器。

main.ts

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

在 混合应用中 useGlobalPipes() 方法不会为网关和微服务设置管道, 对于标准(非混合) 微服务应用使用 useGlobalPipes() 全局设置管道。

全局管道用于整个应用程序、每个控制器和每个路由处理程序。就依赖注入而言,从任何模块外部注册的全局管道(如上例所示)无法注入依赖,因为它们不属于任何模块。为了解决这个问题,可以使用以下构造直接为任何模块设置管道:

app.module.ts

import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_PIPE,
      useClass: ValidationPipe
    }
  ]
})
export class AppModule {}

请注意使用上述方式依赖注入时,请牢记无论你采用那种结构模块管道都是全局的,那么它应该放在哪里呢?使用 ValidationPipe 定义管道 另外,useClass 并不是处理自定义提供者注册的唯一方法。在这里了解更多。

热重载

安装

首先,我们安装所需的软件包:

$ npm i --save-dev webpack-node-externals run-script-webpack-plugin webpack

配置(Configuration)

然后,我们需要创建一个 webpack-hmr.config.js,它是webpack的一个配置文件,并将其放入根目录。

const nodeExternals = require('webpack-node-externals');
const { RunScriptWebpackPlugin } = require('run-script-webpack-plugin');

module.exports = function (options, webpack) {
  return {
    ...options,
    entry: ['webpack/hot/poll?100', options.entry],
    externals: [
      nodeExternals({
        allowlist: ['webpack/hot/poll?100'],
      }),
    ],
    plugins: [
      ...options.plugins,
      new webpack.HotModuleReplacementPlugin(),
      new webpack.WatchIgnorePlugin({
        paths: [/.js$/, /.d.ts$/],
      }),
      new RunScriptWebpackPlugin({ name: options.output.filename }),
    ],
  };
};

此函数获取包含默认 webpack 配置的原始对象,并返回一个已修改的对象和一个已应用的 HotModuleReplacementPlugin 插件。

热模块更换

为了启用 HMR,请打开应用程序入口文件( main.ts )并添加一些与 Webpack相关的说明,如下所示:

declare const module: any;

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);

  if (module.hot) {
    module.hot.accept();
    module.hot.dispose(() => app.close());
  }
}
bootstrap();

就这样。为了简化执行过程,请将这两行添加到 package.json 文件的脚本中。

"start:dev": "nest build --webpack --webpackPath webpack-hmr.config.js --watch"

现在只需打开你的命令行并运行下面的命令:

$ npm run start:dev

小结

到现在一个简单的CRUD以及完成了!

插件扩展

在b站的全站之巅的大神大佬自创插件nestjs-mongoose-crud大家可以去下载使用看看,用这个插件之后稍微配置一下就会自动生成了简单的CRUD的东西,例子在我代码的role模块里。

总结

结合官方文档,这是我把基本用到的东西抽取总结了,这只是开始的第一步,当然还有很多复杂的东西没用到,以后还会一步步更新使用。

以下是我代码demo的地址:

gitee.com/wd_591/nest…

此文只是我在学习的道路上一个小小的笔记,写的不好请多指教,谢谢!

image.png