写给前端的 Nest.js 教程——10分钟上手后端接口开发
前言
这个教程的所有代码我都放在了我的 GitHub 仓库:Nest-CRUD-Demo[1],欢迎大家点个 Star
!
框架简介
❝
Nest
是一个用于构建高效,可扩展的Node.js
服务器端应用程序的框架。它使用渐进式JavaScript
,内置并完全支持TypeScript
(但仍然允许开发人员使用纯JavaScript
编写代码)并结合了OOP
(面向对象编程),FP
(函数式编程)和FRP
(函数式响应编程)的元素。在底层,
❞Nest
使用强大的HTTP Server
框架,如Express
(默认)和Fastify
。Nest
在这些框架之上提供了一定程度的抽象,同时也将其API
直接暴露给开发人员。这样可以轻松使用每个平台的无数第三方模块。
我猜肯定很多同学看不懂这段话,没关系,我也暂时看不懂,但这不影响我们学会用它 CRUD
。
我们只需要知道它是一款 Node.js
的后端框架,「规范化」和「开箱即用」的特性使其在国外开发者社区非常流行,社区也非常活跃,GitHub Repo[2] 拥有 31.1k Star
。
相比于 Express
和 Koa
的千奇百怪五花八门,Nest
确实是一股清流。
不过我们国内也有很棒的 Node.js
框架,比如说 Midway
,和 Nest
一样,采用的 IoC
的机制,想了解一下的同学可以看我的小伙伴「林不渡」写的文章:《走近 MidwayJS :初识 TS 装饰器与 IoC 机制》[3],还可以到 Midway 官网[4]自行探索。
包括在 Nest
当中遇到的装饰器相关的知识,大家也可以到上面「林不渡」同学的那篇文章中了解。
前置知识
HTTP TypeScript/JavaScript
项目环境
git mongodb node.js >= 10.13.0
安装 MongoDB
这个章节的教程我就只写 Mac OS
上的安装了,毕竟上了大学就很少用 Windows
了,用 Windows
的同学可以到 `MongoDB` 官网[5]选择对应的系统版本去下载 msi
的安装包,或者「搜索引擎」里搜索一下,记得限定一下结果的时间,保证能够搜索到最新的教程。
强烈建议使用 Homebrew
来对 Mac OS
的软件包环境进行管理,没有安装的同学可以点击这里[6]下载。
由于目前 MongoDB
已经不开源了,因此我们想要安装 MongoDB
就只能安装社区版本。
brew tap mongodb/brew
brew install mongodb-community
安装好之后我们就可以启动 MongoDB
的服务了:
brew services start mongodb-community
服务启动了就不用管了,如果要关闭的话可以把 start
改成 stop
,就能够停止 MongoDB
的服务了。
构建项目
有两种方式,可以自行选择,两者没有区别:
使用 Nest CLI
安装:
npm i -g @nestjs/cli
nest new nest-crud-demo
使用 Git
安装:
git clone https://github.com/nestjs/typescript-starter.git nest-crud-demo
这两条命令的效果完全一致,就是初始化一个 Nest.js
的项目到当前文件夹下,项目的文件夹名字为 nest-crud-demo
,两种方式都可以。
「当然,我还是建议采用第一种方式,因为后面我们可以直接使用脚手架工具生成项目文件。」
启动服务
cd nest-crud-demo
npm run start:dev 或者 yarn run start:dev
就可以「以开发模式」启动我们的项目了。
这里其实有一个小小的点,就是启动的时候应该以 dev
模式启动,这样 Nest
会「自动检测我们的文件变化」,然后「自动重启服务」。
如果是直接 npm start
或者 yarn start
的话,虽然服务启动了,但是我们如果在开发的过程中修改了文件,就要手动停止服务然后重新启动,效率挺低的。
安装依赖
项目中我们会用到 Mongoose
来操作我们的数据库,Nest
官方为我们提供了一个 Mongoose
的封装,我们需要安装 mongoose
和 @nestjs/mongoose
:
npm install mongoose @nestjs/mongoose --save
安装好之后我们就可以开始编码过程了。
编写代码
创建 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
。
// 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(); // 一些异步操作
}
}
创建 Provider
nest g service user server
provider
我们可以简单地从字面意思来理解,就是「服务的提供者」。
怎么去理解这个「服务提供者」呢?举个例子,我们的 controller
接收到了一个用户的查询请求,我们不能直接在 controller
中去查询数据库并返回,而是要将查询请求交给 provider
来处理,这里我们创建了一个 UserService
,就是用来提供「数据库操作服务」的。
// user.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserService {}
当然,provider
不一定只能用来提供数据库的操作服务,还可以用来做一些用户校验,比如使用 JWT
对用户权限进行校验的策略,就可以写成一个策略类,放到 provider
中,为模块提供相应的服务。
挺多文档将 controller
和 provider
翻译为「控制器」和「提供者」,我感觉这种翻译挺生硬的,让人不知所云,所以我们姑且记忆他们的英文名吧。
controller
和 provider
都创建完后,我们又会惊奇地发现,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 {}
从这里开始,我们就要开始用到数据库了~
连接数据库
引入 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/xxx'), UserModule],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {}
这段代码里面的 mongodb://localhost/xxx
其实就是本地数据库的地址,xxx
是数据库的名字。
这时候保存文件,肯定有同学会发现控制台还是报错的,我们看一下报错信息就很容易知道问题在哪里了。
其实就是 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 {}
好了,现在一切就绪,终于可以开始编写我们的 CRUD
逻辑了!冲冲冲~
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 ) {}
// 查找所有用户
async findAll(): Promise {
const users = await this.userModel.find();
return users;
}
// 查找单个用户
async findOne(_id: string): Promise {
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 {
code: number;
data?: T;
message: string;
}
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
// GET /user/users
@Get('users')
async findAll(): Promise> {
return {
code: 200,
data: await this.userService.findAll(),
message: 'Success.'
};
}
// GET /user/:_id
@Get(':_id')
async findOne(@Param('_id') _id: string): Promise> {
return {
code: 200,
data: await this.userService.findOne(_id),
message: 'Success.'
};
}
// POST /user
@Post()
async addOne(@Body() body: CreateUserDTO): Promise {
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 {
await this.userService.editOne(_id, body);
return {
code: 200,
message: 'Success.'
};
}
// DELETE /user/:_id
@Delete(':_id')
async deleteOne(@Param('_id') _id: string): Promise {
await this.userService.deleteOne(_id);
return {
code: 200,
message: 'Success.'
};
}
}
至此,我们就完成了一个完整的 CRUD
操作,接下来我们来测试一下~
接口测试
接口测试我们用的是 Postman
,大家可以去下载一个,非常好用的接口自测工具。
数据库可视化工具我们用的是 MongoDB
官方的 MongoDB Compass
,也很不错。
GET /user/users
一开始我们的数据库中什么都没有,所以返回了一个空数组,没用用户信息。
POST /user
现在我们添加一条用户信息,服务器返回添加成功。
GET /user/:_id
添加完一条用户信息之后再查询,可算是能查询到我的信息了。
PUT /user/:_id
现在假如我想修改密码,发送一个 PUT
请求。
DELETE /user/:_id
现在我们删除一下刚才添加的用户信息。
会发现数据库中的内容已经被删除了。
完结撒花
大功告成,CRUD
就这么简单,用这个项目去参加一些学校举行的比赛,拿个奖肯定没什么问题,开箱即用(学校老师们别打我)。
总结
教程还算是用了比较通俗易懂的方式为大家讲解了如何写一个带有 CRUD
功能的后端 Node.js
应用,框架采用的是 Nest.js
。
相信大家在上面的教程中肯定有非常多不懂的部分,比如说 @Get()
、@Post()
、@Param()
、@Body()
等等的装饰器,再比如说一些 Nest.js
相关的概念。
没关系,我的建议是:「学编程先模仿,遇到不懂的地方先记住,等到自己的积累够多了,总有一天你会回过头发现自己茅塞顿开,突然懂了」。这也是我个人学习的一个小技巧。
在学习的过程中,也一定会遇到一些问题,学习编程的过程中遇到问题不能自己憋着,「一定要学会请教大佬!一定要学会请教大佬!一定要学会请教大佬」!重要的事情说三遍。
不过也别很简单的问题就去请教大佬,而且最好给一点小小的报酬,毕竟谁也没有义务帮你解决问题。
我在学习的过程中也请教了一些社区里面的大佬,同时还进入了 Nest.js
的社区答疑群,向国外友人请教学到了不少知识。
当然,这个 Demo
中也有很多可以完善的地方,比如说「错误处理」。
数据库的操作肯定是有可能出现错误的,比如说我们漏传了 required: true
的参数,数据库就会报错。
这个时候我们就要写一个 try/catch
捕获这个异常,或者干脆写一个异常的过滤器,将所有的异常统一处理(Nest.js
支持过滤器)
除此之外,既然有可能出现异常,那么我们就需要一个日志系统去捕获这个异常,方便查错纠错。
如果涉及到登录注册的部分,还有密码加解密的过程,同时还可能有权限校验问题需要进行处理。
所以后端的同学肯定不止 CRUD
啦(可算圆回来了)。
这个教程的所有代码我都放在了我的 GitHub 仓库:Nest-CRUD-Demo[7],欢迎大家点个 Star
!
参考资料
NestJS - A progressive Node.js framework[8] Nest.js 中文文档[9]
❤️爱心三连击
1.看到这里了就点个在看支持下吧,你的「点赞,在看」是我创作的动力。
2.关注公众号
程序员成长指北
,回复「1」加入Node进阶交流群!「在这里有好多 Node 开发者,会讨论 Node 知识,互相学习」!3.也可添加微信【ikoala520】,一起成长。
Reference
Nest-CRUD-Demo: https://github.com/wjq990112/Nest-CRUD-Demo
[2]GitHub Repo: https://github.com/nestjs/nest
[3]《走近 MidwayJS :初识 TS 装饰器与 IoC 机制》: https://juejin.im/post/6859314697204662279
[4]Midway 官网: https://midwayjs.org/midway/
[5]MongoDB
官网: https://mongodb.com/download-center/community
点击这里: https://brew.sh/
[7]Nest-CRUD-Demo: https://github.com/wjq990112/Nest-CRUD-Demo
[8]NestJS - A progressive Node.js framework: https://nestjs.com/
[9]Nest.js 中文文档: https://docs.nestjs.cn/
“在看转发”是最大的支持