前言
众所周知,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的官方文档和中文文档地址:
请确保您的操作系统上安装了 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);
}
}
在下面我们可以看到,不仅生成了所有变更和查询的样板文件,也把他们绑定到了一起,我们可以使用UsersService
, User 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
SwaggerModule
自动反映所有端点。同时,为了展现 Swagger UI,@nestjs/swagger
依据平台使用 swagger-ui-express
或 fastify-swagger
。
【友情提示】SwaggerModule.setup('api', app, document)
中的'api'
咱们可以换成一个又好的地址比如'doc'
为很多后台接口有的以api开头,避免冲突咱们可以给文档接口地址起其他的名字。
装饰器
接下来我们整理一下我们的接口,并用装饰器来给我们接口做一下注释,由于我是用的article
这个命名的,所以演示的都是以这个为基础。
以上是常用的接口描述,如果想了解更多可参考## 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的地址:
此文只是我在学习的道路上一个小小的笔记,写的不好请多指教,谢谢!