起步
从这里开始,我们将对nest.js的一个基本应用做一个简单的讲解,包含安装和基本概念的介绍以及简单应用。
介绍
Nest (NestJS) 是一个用于构建高效、可扩展的 Node.js 服务器端应用程序的开发框架。它利用 JavaScript 的渐进增强的能力,使用并完全支持 TypeScript (仍然允许开发者使用纯 JavaScript 进行开发),并结合了 OOP (面向对象编程)、FP (函数式编程)和 FRP (函数响应式编程)。
在底层,Nest 构建在强大的 HTTP 服务器框架上,例如 Express (默认),并且还可以通过配置从而使用 Fastify !
Nest 在这些常见的 Node.js 框架 (Express/Fastify) 之上提高了一个抽象级别,但仍然向开发者直接暴露了底层框架的 API。这使得开发者可以自由地使用适用于底层平台的无数的第三方模块。
-- 摘自官网
安装
首先,我们需要安装nest提供的脚手架工具,进入终端输入以下命令:
npm i -g @nestjs/cli
安装完成后,执行nest -v命令,看到终端显示版本号后就表示安装成功,如下图:
HelloWorld
当我们完成nest脚手架的安装后,就可以进行项目的创建了,如下所示,我们这里创建一个项目名为hello的nest应用:
nest new hello-word
执行完成命名后,看到如下界面就表示项目新建成功了:
安装完成后,使用VSCode编辑打开项目后,我们主要注意src目录下面的文件,可以发现在src目录下有5个默认的文件,如下图:
这里就对这5个文件进行一个简单的解释:
- app.controller.spec.ts:这是一个用作测试的文件
- app.controller.ts:这是一个控制器文件
- app.module.ts: 这是根模块文件
- app.service.ts:这是一个提供功能服务的文件
- main.ts:这是nest项目的入口文件
首先,打开main.ts可以发现以下内容:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
可以看到内容十分简单,就是引入根模块文件app.module然后通过NestFactory这个对象创建一个应用程序,并且监听端口3000来启动服务。
再看app.module.ts文件,可以发现该模块通过装饰器语法来对app.controller和app.service进行了一个依赖,如下图:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
接着,我们再进入app.controller文件里面看看该控制器里面有什么内容,文件代码如下:
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
可以看到该文件定义了一个构造函数,并且通过形参的方式接收了一个AppService对象,然后再名为getHello的路由里面,调用了该服务的getHello方法,并且将结果值进行返回。
这里讲解一下控制器和路由的基本概念
控制器是作为应用接受特定请求的一个类,而路由则是决定控制器可以接受哪些请求的方法。在nest.js应用中,控制器由@Controller()定义,路由由@Get()、@Post()等定义。
打开app.service文件,可以发现就是一个通过装饰器定义了的类,如下:
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}
我们先不管这些装饰器和构造函数等的用法,通过代码可以知道在基本控制器中:定义了一个路由,并且该路由调用了服务里面的getHello方法,然后将结果值Hello World!进行返回。
接下来,就通过执行命令来启动服务看一下效果,执行以下命令:
npm run start:dev
然后打开浏览器,在地址栏输入:http://localhost:3000后,可以看到页面和我们推论一样,输出的一个HelloWorld的字符串,如下图:
至此,我们就已经通过这个默认的HelloWorld的例子认识了nset.js应用了,接下来我们就来创建一个自己的控制器和对应的路由来学习如何进行接口请求和响应。
请求和响应
现在,我们有一个业务:通过访问接口地址来完成商品的添加修改删除和查看。
好,现在就开始新建一个命名为goods的控制器,输入以下命令:
nest g co goods --no-spec
执行完成后,可以发现在src目录下给我们生成了一个goods目录,并且在goods目录下面还会有一个goods.controller.ts文件,这个就是我们编写控制器逻辑的文件。
nest g co co_name表示使用nest脚手架的功能新建一个控制器,g表示生成,co表示控制器,co_name就是对应的名称。
如果没有加--no-spec参数的话,还会生成一个goods.controller.spec.ts的测试文件
注意:通过该方式生成的控制器,会自动的添加到app.module.ts模块中去,而不需要我们去手动引入。
查看
好,接下来我们就要写查看的接口了,通常查询是一个GET方式的请求,所以这里我们使用nestjs提供的@Get()装饰器来定义一个路由,并且返回商品信息,代码如下:
import { Controller, Get } from '@nestjs/common';
@Controller('goods')
export class GoodsController {
private goods = []; // 没有数据库,暂时通过该方式定义数据
@Get()
findAll() {
return this.goods;
}
}
可以看到上面,我们通过@Get装饰器来修饰findAll方法,然后返回了我们已经定义好了的goods数据,接下来通过请求:http://localhost:/goods接口,返回结果如下:
可以看到,请求成功,并且在响应里面返回了我们一个空数组[]对象,这是因为我们目前的goods就是一个空数组对象。
补充说明:这里GoodsController通过@Controller('goods')装饰之后,所有的路由路径都会拼上
/goods这个前缀。
添加
好的,现在查看已经实现了,但是苦于没有数据,所以这里我们就先来完成如何添加一条数据,因为添加一般采用的都是POST方式进行的,所以我们这里采用@Post()装饰器来定义一个路由,实现POST请求的参数接收。
在编写添加逻辑之前,我们先看看如何接收POST请求的参数,代码如下:
import { Body, Controller, Get, Post } from '@nestjs/common';
@Controller('goods')
export class GoodsController {
private goods = [];
// Get...
@Post()
addGoods(@Body() goods) {
console.log(goods);
}
}
可以发现这里使用的是nestjs提供的@Body()装饰器来实现POST请求参数接收的,现在我们打印一下数据看看goods是一个什么样的数据,如下:
这是请求接口携带的参数:
这是终端输出的goods对象:
知道如何接收参数后,我们就不难写出添加商品的代码了。这里,我们先将商品的类型做了一个简单的类型定义,然后通过@Post装饰器定义addGoods这个POST方式的路由,并且在代码块里面将传入的数据进行了添加如下所示:
import { Body, Controller, Get, Post } from '@nestjs/common';
type Goods = {
id: number;
name: string;
price: number;
keywords: string[];
};
@Controller('goods')
export class GoodsController {
private goods: Array<Goods> = [];
@Get()
findAll() {
return this.goods;
}
@Post()
addGoods(@Body() goods: Goods) {
this.goods.push({
id: this.goods.length + 1,
...goods,
});
return this.goods;
}
}
再次执行http://localhost:3000/goods的POST请求方式,发现返回给我们的商品对象已经有了新的数据了,如图:
修改
修改和添加其实类似的,因为他们接收参数的方式都是通过@Body装饰器来实现的,所以我们只需要关注怎么修改数据即可,代码如下:
import { Body, Controller, Get, Post, Put } from '@nestjs/common';
type Goods = {
id: number;
name: string;
price: number;
keywords: string[];
};
@Controller('goods')
export class GoodsController {
private goods: Array<Goods> = [];
// Get...
// Post...
@Put()
updateGoods(@Body() goods: Goods) {
const idx = this.goods.findIndex((item) => item.id === goods.id);
this.goods[idx] = goods;
return this.goods;
}
}
这里我们先通过POST请求添加一个商品信息后,然后再通过PUT请求来修改商品的数据,来进行是否添加成功,过程如下图:
- 进行数据添加:
- 然后进行数据修改:
可以看到,我们成功的将价格为10的面包修改为了6并且返回成功,说明修已经成功了。
删除
删除接口往往是通过Delete方式请求的,所以这里我们需要使用nestjs提供的@Delete()装饰器来实现。
删除数据需要传入数据id来作为参数,以便我们能精准的知道需要删除哪条数据,这里我们通过动态路由的方式并配合@Params来接收参数,先来看看如何接收参数:
@Controller('goods')
export class GoodsController {
// TODO...
@Delete(':id')
removeGoods(@Param() param) {
console.log(param);
}
}
这里的:id表示一个占位符,可以通过任意参数的形式拼接上来后,然后使用@Param装饰器来接收,现在看看打印的结果:
此外,还有一种更为简洁的方式来实现参数的接收,代码如下:
@Controller('goods')
export class GoodsController {
// TODO...
@Delete(':id')
removeGoods(@Param('id') id) {}
}
这样,就能直接获取到对象的id属性了,@Body这类的装饰器也支持这种写法。知道如何接收参数后,我们就能很容易的写出删除接口了,代码如下:
@Controller('goods')
export class GoodsController {
// TODO...
@Delete(':id')
removeGoods(@Param('id') id) {
const idx = this.goods.findIndex((item) => item.id === id);
this.goods.splice(idx, 1);
return this.goods;
}
}
然后,通过访问接口看看接口是否正常:
- 通过
POST方式先增加一个商品:
- 通过
DELETE方式删除对应id的商品:
可以发现一切正常,商品数据已经被删除了。
响应
到这里,我们现在的所有返回的数据都是通过return的方式直接返回的,那如何通过json格式返回呢?这里首先要明白,nestjs默认采用的是express来处理这些逻辑的。
在nestjs中通过使用@Res()装饰器定义了一个响应对象,使用该对象就可以指定响应的方式了,代码如下:
@Controller('goods')
export class GoodsController {
// TODO...
@Get('find')
findById(@Query('id') id, @Res() res) {
const data = this.goods.find((item) => item.id === +id);
res.json({
code: 0,
data,
});
}
}
我们再来看看接口访问的情况:
- 通过
POST方式添加一条数据:
- 然后通过
GET方式查询并看到返回格式已经变成了一个json对象了:
对于一些请求,我们会对其进行异常的返回处理,利用nest.js内置的异常类,我们可以很容易的实现这一点,代码如下:
removeGoods(id) {
const idx = this.goods.findIndex((item) => item.id === id);
if (idx < 0) {
throw new HttpException('没有对应的数据', HttpStatus.NOT_FOUND);
}
this.goods.splice(idx, 1);
return this.goods;
}
当删除一个物品时,当没有对应的物品,则抛出一个异常并返回,响应如图:
服务
我们上面的例子,无论是参数接收还是逻辑处理都是在控制器中进行的,这样的代码组织并不友好,所以我们需要把不同的逻辑操作放到一个个中的服务里面去,当使用对应的功能时再引入不同的服务。
新建服务
通过nest.js脚手架命令,我们很容易就能新建一个服务出来,执行以下命令:
nest g s goods --no-spec
一切成功的话,就会发现在src目录下多了一个goods.service.ts文件了。打开该文件,会发现服务类是由装饰器@Injectable()来定义的,服务将通过providers的方式引用,打开app.module.ts文件发现服务已经被自动引入了,这是脚手架的功能。
在控制器中使用引入的服务的话,需要通过在构造函数中定义对应的服务,然后使用this访问即可。
编写服务
现在有了这个服务文件后,我们就可以将之前在控制器中的逻辑操作代码移动到该服务中去,然后再控制器中只需要调用对应的方法即可,更改后的代码如下:
// goods.controller.ts
import { GoodsService } from './goods.service';
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
Query,
Res,
} from '@nestjs/common';
@Controller('goods')
export class GoodsController {
constructor(private readonly goodsService: GoodsService) {}
@Get()
findAll() {
return this.goodsService.findAll();
}
@Get('find')
findById(@Query('id') id, @Res() res) {
const data = this.goodsService.findById(+id);
res.status(200).json({
code: 0,
data,
});
}
@Post()
addGoods(@Body() goods) {
return this.goodsService.addGoods(goods);
}
@Put()
updateGoods(@Body() goods) {
return this.goodsService.updateGoods(goods);
}
@Delete(':id')
removeGoods(@Param('id') id) {
return this.goodsService.removeGoods(+id);
}
}
// goods.service.ts
import { Injectable } from '@nestjs/common';
type Goods = {
id: number;
name: string;
price: number;
keywords: string[];
};
@Injectable()
export class GoodsService {
private goods: Array<Goods> = [];
findAll() {
return this.goods;
}
findById(id: number) {
return this.goods.find((item) => item.id === id);
}
addGoods(goods: Goods) {
this.goods.push({
id: this.goods.length + 1,
...goods,
});
return this.goods;
}
updateGoods(goods: Goods) {
const idx = this.goods.findIndex((item) => item.id === goods.id);
this.goods[idx] = goods;
return this.goods;
}
removeGoods(id) {
const idx = this.goods.findIndex((item) => item.id === id);
this.goods.splice(idx, 1);
return this.goods;
}
}
这样就完成服务的抽离了,编写好的服务能帮助我们更好的组织代码和服用代码。
实体对象
目前我们的商品类型是通过type定义的,但是在nest.js中有一种更好的写法,叫做实体对象的写法,其实该对象就是一个普通类,现在我们创建一个商品实体对象。
- 在
src/goods目录下新建entities目录; - 在
entites目录下新建goods.entity.ts文件; - 在
goods.entity.ts文件中编写如下代码:
export default class Goods {
id: number;
name: string;
price: number;
keywords: string[];
}
- 然后将之前的
Goods类型替换为该实体。
虽然一切还是正常的进行,但貌似这个实体功能并没有什么作用,这点在后续里面会应用到,这里明白使用实体对象作为类型即可。
数据传送对象
上面的例子中我们使用@Body()装饰器来接收参数,但是该参数其实是没有一个类型限制的。限制,我们通过数据传输对象就可以实现在接收参数时,对参数的一个限制和转换了。
创建DTO
通过nest.js的脚手架命令,我们可以输入以下内容进行DTO文件的创建:
nest g class goods/dto/create-goods.dto --no-spec
可以发现,在src/goods目录下多了一个dto目录,并且该目录下多了一个create-goods.dto.ts文件,该文件的编写和实体对象类似,如下:
export class CreateGoodsDto {
name: string;
price: number;
keywords: string[];
}
现在,我们只需要将POST请求中的@Body()装饰器修饰的参数类型定义为此即可(服务里面涉及的参数一并修改):
@Post()
addGoods(@Body() createGoodsDto: CreateGoodsDto) {
return this.goodsService.addGoods(createGoodsDto);
}
再次访问POST接口,我们尝试把price属性修改为字符串看看是什么结果,结果如下图:
发现响应一切正常,只是希望得到的price属性变为了字符串类型。那么,这个数据传输对象是个鸡肋嘛?并不是,现在我们搭配nest.js内置的ValidationPipe就能实现奇效!
ValidationPipe
使用ValidationPipe非常容易,我们只需要在main.ts中编写如下代码即可:
import { ValidationPipe } from '@nestjs/common/pipes/validation.pipe';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
通过app.useGlobalPipes(new ValidationPipe());这行代码我们就已经注册好了ValidationPipe对象。
接下来如果想要ValidationPipe发挥作用的话,还需要安装两个依赖:
npm i --save class-validator class-transformer
安装完成依赖后,现在我们再进入到create-goods.dto.ts文件中进行以下配置:
import { IsString } from 'class-validator';
import { IsNumber } from 'class-validator/types/decorator/decorators';
export class CreateGoodsDto {
@IsString() // 表明该字段是string类型
name: string;
@IsNumber() // 表明该字段是nunmber类型
price: number;
@IsString({ each: true }) // 表明该字段是string[]类型
keywords: string[];
}
然后接着我们再访问接口,发现结果如下图:
响应结果抛出了一个状态码为400的错误,并且提示price必须为一个number类型。
ValidationPipe还可以进行参数的筛选,我们看下这个请求例子:
我们发现,请求参数多了一个brand属性,这个是在我们的数据传送对象中未定义的,但是仍然被接收到了,如果我们不想接收数据传送对象以外的参数的话,只需要进行如下配置即可:
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
}),
);
接下来再次发送请求,发现多余的字段已经不存在了:
ValidationPipe的功能还不止于此,除了配合数据传送对象之外,他还可以将参数进行一个类型的转换,看一下之前的一个请求:
@Get('find')
findById(@Query('id') id:number, @Res() res) {
console.log(typeof id);
const data = this.goodsService.findById(+id);
res.status(200).json({
code: 0,
data,
});
}
我们这里传入id到服务中时,进行了一个类型的转换,为啥会这样呢?我们打印看一下id的参数就明白了,如下图:
即使我们对接收参数的类型进行了number的限制,但是依然获取到一个string类型的变量,这是因为通过浏览器地址中的参数本身就是string类型的。
现在,我们在ValidationPipe中进行如下配置:
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
}),
);
接着,再看一下请求时打印的参数信息:
注意:转换的功能会对应用产生轻微的影响
模块
到现在为止,我们写的控制器、服务都是注册到app.module中的,而非一个单独的模块,如果对于一个复杂的应用来说,这种组织是不合理的。
所以现在我们新建一个module来存放这些文件,在终端输入以下命令:
nest g mo goods
可以看到在src/goods目录下多了一个goods.module.ts文件,打开该文件进行如下配置:
import { GoodsController } from './goods.controller';
import { GoodsService } from './goods.service';
import { Module } from '@nestjs/common';
@Module({
controllers: [GoodsController], // 引入的控制器
providers: [GoodsService], // 注入的服务
})
export class GoodsModule {}
接着,再将app.module.ts里面引入的GoodsController和GoodsService删除掉,转而引入GoodsModule模块(脚手架工具创建的模块会自动引入)。
这样,我们就叫一个独立的goods模块抽离出来了。
完结
以上就是对nest.js的基础介绍。
大致从nest.js介绍 -> HelloWorld -> 请求和响应 -> 服务 -> 实体对象 -> 数据传输对象 -> 模块 进行了一个简单的讲解。