寫給前端的 Nest.js 教程——10分鐘上手后端接口開發(fā)
前言
這個(gè)教程的所有代碼我都放在了我的 GitHub 倉庫:Nest-CRUD-Demo[1],歡迎大家點(diǎn)個(gè) Star!
框架簡介
?
Nest是一個(gè)用于構(gòu)建高效,可擴(kuò)展的Node.js服務(wù)器端應(yīng)用程序的框架。它使用漸進(jìn)式JavaScript,內(nèi)置并完全支持TypeScript(但仍然允許開發(fā)人員使用純JavaScript編寫代碼)并結(jié)合了OOP(面向?qū)ο缶幊蹋?code style="overflow-wrap: break-word;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(53, 148, 247);background: rgba(59, 170, 250, 0.1);padding-right: 2px;padding-left: 2px;border-radius: 2px;height: 21px;line-height: 22px;">FP(函數(shù)式編程)和FRP(函數(shù)式響應(yīng)編程)的元素。在底層,
?Nest使用強(qiáng)大的HTTP Server框架,如Express(默認(rèn))和Fastify。Nest在這些框架之上提供了一定程度的抽象,同時(shí)也將其API直接暴露給開發(fā)人員。這樣可以輕松使用每個(gè)平臺(tái)的無數(shù)第三方模塊。
我猜肯定很多同學(xué)看不懂這段話,沒關(guān)系,我也暫時(shí)看不懂,但這不影響我們學(xué)會(huì)用它 CRUD。
我們只需要知道它是一款 Node.js 的后端框架,「規(guī)范化」和「開箱即用」的特性使其在國外開發(fā)者社區(qū)非常流行,社區(qū)也非常活躍,GitHub Repo[2] 擁有 31.1k Star。
相比于 Express 和 Koa 的千奇百怪五花八門,Nest 確實(shí)是一股清流。
不過我們國內(nèi)也有很棒的 Node.js 框架,比如說 Midway,和 Nest 一樣,采用的 IoC 的機(jī)制,想了解一下的同學(xué)可以看我的小伙伴「林不渡」寫的文章:《走近 MidwayJS :初識(shí) TS 裝飾器與 IoC 機(jī)制》[3],還可以到 Midway 官網(wǎng)[4]自行探索。
包括在 Nest 當(dāng)中遇到的裝飾器相關(guān)的知識(shí),大家也可以到上面「林不渡」同學(xué)的那篇文章中了解。
前置知識(shí)
HTTP TypeScript/JavaScript
項(xiàng)目環(huán)境
git mongodb node.js >= 10.13.0
安裝 MongoDB
這個(gè)章節(jié)的教程我就只寫 Mac OS 上的安裝了,畢竟上了大學(xué)就很少用 Windows 了,用 Windows 的同學(xué)可以到 `MongoDB` 官網(wǎng)[5]選擇對(duì)應(yīng)的系統(tǒng)版本去下載 msi 的安裝包,或者「搜索引擎」里搜索一下,記得限定一下結(jié)果的時(shí)間,保證能夠搜索到最新的教程。
強(qiáng)烈建議使用 Homebrew 來對(duì) Mac OS 的軟件包環(huán)境進(jìn)行管理,沒有安裝的同學(xué)可以點(diǎn)擊這里[6]下載。
由于目前 MongoDB 已經(jīng)不開源了,因此我們想要安裝 MongoDB 就只能安裝社區(qū)版本。
brew?tap?mongodb/brew
brew?install?mongodb-community
安裝好之后我們就可以啟動(dòng) MongoDB 的服務(wù)了:
brew?services?start?mongodb-community
服務(wù)啟動(dòng)了就不用管了,如果要關(guān)閉的話可以把 start 改成 stop,就能夠停止 MongoDB 的服務(wù)了。
構(gòu)建項(xiàng)目
有兩種方式,可以自行選擇,兩者沒有區(qū)別:
使用 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
這兩條命令的效果完全一致,就是初始化一個(gè) Nest.js 的項(xiàng)目到當(dāng)前文件夾下,項(xiàng)目的文件夾名字為 nest-crud-demo,兩種方式都可以。
「當(dāng)然,我還是建議采用第一種方式,因?yàn)楹竺嫖覀兛梢灾苯邮褂媚_手架工具生成項(xiàng)目文件?!?/strong>
啟動(dòng)服務(wù)
cd?nest-crud-demo
npm?run?start:dev?或者?yarn?run?start:dev
就可以「以開發(fā)模式」啟動(dòng)我們的項(xiàng)目了。
這里其實(shí)有一個(gè)小小的點(diǎn),就是啟動(dòng)的時(shí)候應(yīng)該以 dev 模式啟動(dòng),這樣 Nest 會(huì)「自動(dòng)檢測(cè)我們的文件變化」,然后「自動(dòng)重啟服務(wù)」。
如果是直接 npm start 或者 yarn start 的話,雖然服務(wù)啟動(dòng)了,但是我們?nèi)绻陂_發(fā)的過程中修改了文件,就要手動(dòng)停止服務(wù)然后重新啟動(dòng),效率挺低的。
安裝依賴
項(xiàng)目中我們會(huì)用到 Mongoose 來操作我們的數(shù)據(jù)庫,Nest 官方為我們提供了一個(gè) Mongoose 的封裝,我們需要安裝 mongoose 和 @nestjs/mongoose:
npm?install?mongoose?@nestjs/mongoose?--save
安裝好之后我們就可以開始編碼過程了。
編寫代碼
創(chuàng)建 Module
我們這次就創(chuàng)建一個(gè) User 模塊,寫一個(gè)用戶增刪改查,帶大家熟悉一下這個(gè)過程。
nest?g?module?user?server
腳手架工具會(huì)自動(dòng)在 src/server/user 文件夾下創(chuàng)建一個(gè) user.module.ts,這是 Nest 的模塊文件,Nest 用它來組織整個(gè)應(yīng)用程序的結(jié)構(gòu)。
//?user.module.ts
import?{?Module?}?from?'@nestjs/common';
@Module({})
export?class?UserModule?{}
同時(shí)還會(huì)在根模塊 app.module.ts 中引入 UserModule 這個(gè)模塊,相當(dāng)于一個(gè)樹形結(jié)構(gòu),在根模塊中引入了 User 模塊。
執(zhí)行上面的終端命令之后,我們會(huì)驚訝地發(fā)現(xiàn),app.module.ts 中的代碼已經(jīng)發(fā)生了變化,在文件頂部自動(dòng)引入了 UserModule,同時(shí)也在 @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';?//?自動(dòng)引入
@Module({
??imports:?[UserModule],?//?自動(dòng)引入
??controllers:?[AppController],
??providers:?[AppService]
})
export?class?AppModule?{}
創(chuàng)建 Controller
nest?g?controller?user?server
在 Nest 中,controller 就類似前端的「路由」,負(fù)責(zé)處理「客戶端傳入的請(qǐng)求」和「服務(wù)端返回的響應(yīng)」。
舉個(gè)例子,我們?nèi)绻ㄟ^ http://localhost:3000/user/users 獲取所有的用戶信息,那么我們可以在 UserController 中創(chuàng)建一個(gè) GET 方法,路徑為 users 的路由,這個(gè)路由負(fù)責(zé)返回所有的用戶信息。
//?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]?暫時(shí)代替所有用戶的信息
??}
}
這就是 controller 的作用,負(fù)責(zé)分發(fā)和處理「請(qǐng)求」和「響應(yīng)」。
當(dāng)然,也可以把 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();?//?一些異步操作
??}
}
創(chuàng)建 Provider
nest?g?service?user?server
provider 我們可以簡單地從字面意思來理解,就是「服務(wù)的提供者」。
怎么去理解這個(gè)「服務(wù)提供者」呢?舉個(gè)例子,我們的 controller 接收到了一個(gè)用戶的查詢請(qǐng)求,我們不能直接在 controller 中去查詢數(shù)據(jù)庫并返回,而是要將查詢請(qǐng)求交給 provider 來處理,這里我們創(chuàng)建了一個(gè) UserService,就是用來提供「數(shù)據(jù)庫操作服務(wù)」的。
//?user.service.ts
import?{?Injectable?}?from?'@nestjs/common';
@Injectable()
export?class?UserService?{}
當(dāng)然,provider 不一定只能用來提供數(shù)據(jù)庫的操作服務(wù),還可以用來做一些用戶校驗(yàn),比如使用 JWT 對(duì)用戶權(quán)限進(jìn)行校驗(yàn)的策略,就可以寫成一個(gè)策略類,放到 provider 中,為模塊提供相應(yīng)的服務(wù)。
挺多文檔將 controller 和 provider 翻譯為「控制器」和「提供者」,我感覺這種翻譯挺生硬的,讓人不知所云,所以我們姑且記憶他們的英文名吧。
controller 和 provider 都創(chuàng)建完后,我們又會(huì)驚奇地發(fā)現(xiàn),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?{}
從這里開始,我們就要開始用到數(shù)據(jù)庫了~
連接數(shù)據(jù)庫
引入 Mongoose 根模塊
連接數(shù)據(jù)之前,我們要先在根模塊,也就是 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 其實(shí)就是本地?cái)?shù)據(jù)庫的地址,xxx 是數(shù)據(jù)庫的名字。
這時(shí)候保存文件,肯定有同學(xué)會(huì)發(fā)現(xiàn)控制臺(tái)還是報(bào)錯(cuò)的,我們看一下報(bào)錯(cuò)信息就很容易知道問題在哪里了。
其實(shí)就是 mongoose 模塊沒有類型聲明文件,這就很容易解決了,安裝一下就好:
npm?install?@types/mongoose?--dev?或者?yarn?add?@types/mongoose?--dev
安裝完之后服務(wù)就正常重啟了。
引入 Mongoose 分模塊
這里我們先要?jiǎng)?chuàng)建一個(gè)數(shù)據(jù)表的格式,在 src/server/user 文件夾下創(chuàng)建一個(gè) user.schema.ts 文件,定義一個(gè)數(shù)據(jù)表的格式:
//?user.schema.ts
import?{?Schema?}?from?'mongoose';
export?const?userSchema?=?new?Schema({
??_id:?{?type:?String,?required:?true?},?//?覆蓋?Mongoose?生成的默認(rèn)?_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?{}
好了,現(xiàn)在一切就緒,終于可以開始編寫我們的 CRUD 邏輯了!沖沖沖~
CRUD
我們打開 user.service.ts 文件,為 UserService 類添加一個(gè)構(gòu)造函數(shù),讓其在實(shí)例化的時(shí)候能夠接收到數(shù)據(jù)庫 Model,這樣才能在類中的方法里操作數(shù)據(jù)庫。
//?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;
??}
??//?查找單個(gè)用戶
??async?findOne(_id:?string):?Promise?{
????return?await?this.userModel.findById(_id);
??}
??//?添加單個(gè)用戶
??async?addOne(body:?CreateUserDTO):?Promise<void>?{
????await?this.userModel.create(body);
??}
??//?編輯單個(gè)用戶
??async?editOne(_id:?string,?body:?EditUserDTO):?Promise<void>?{
????await?this.userModel.findByIdAndUpdate(_id,?body);
??}
??//?刪除單個(gè)用戶
??async?deleteOne(_id:?string):?Promise<void>?{
????await?this.userModel.findByIdAndDelete(_id);
??}
}
因?yàn)?mongoose 操作數(shù)據(jù)庫其實(shí)是異步的,所以這里我們使用 async 函數(shù)來處理異步的過程。
好奇的同學(xué)會(huì)發(fā)現(xiàn),這里突然出現(xiàn)了兩個(gè)文件,一個(gè)是 user.interface.ts,另一個(gè)是 user.dto.ts,我們現(xiàn)在來創(chuàng)建一下:
//?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;
}
其實(shí)就是對(duì)數(shù)據(jù)類型做了一個(gè)定義。
現(xiàn)在,我們可以到 user.controller.ts 中設(shè)置路由了,將「客戶端的請(qǐng)求」進(jìn)行處理,調(diào)用相應(yīng)的服務(wù)實(shí)現(xiàn)相應(yīng)的功能:
//?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.'
????};
??}
}
至此,我們就完成了一個(gè)完整的 CRUD 操作,接下來我們來測(cè)試一下~
接口測(cè)試
接口測(cè)試我們用的是 Postman,大家可以去下載一個(gè),非常好用的接口自測(cè)工具。
數(shù)據(jù)庫可視化工具我們用的是 MongoDB 官方的 MongoDB Compass,也很不錯(cuò)。
GET /user/users

一開始我們的數(shù)據(jù)庫中什么都沒有,所以返回了一個(gè)空數(shù)組,沒用用戶信息。
POST /user

現(xiàn)在我們添加一條用戶信息,服務(wù)器返回添加成功。

GET /user/:_id

添加完一條用戶信息之后再查詢,可算是能查詢到我的信息了。
PUT /user/:_id

現(xiàn)在假如我想修改密碼,發(fā)送一個(gè) PUT 請(qǐng)求。

DELETE /user/:_id

現(xiàn)在我們刪除一下剛才添加的用戶信息。

會(huì)發(fā)現(xiàn)數(shù)據(jù)庫中的內(nèi)容已經(jīng)被刪除了。
完結(jié)撒花
大功告成,CRUD 就這么簡單,用這個(gè)項(xiàng)目去參加一些學(xué)校舉行的比賽,拿個(gè)獎(jiǎng)肯定沒什么問題,開箱即用(學(xué)校老師們別打我)。
總結(jié)
教程還算是用了比較通俗易懂的方式為大家講解了如何寫一個(gè)帶有 CRUD 功能的后端 Node.js 應(yīng)用,框架采用的是 Nest.js。
相信大家在上面的教程中肯定有非常多不懂的部分,比如說 @Get()、@Post()、@Param()、@Body() 等等的裝飾器,再比如說一些 Nest.js 相關(guān)的概念。
沒關(guān)系,我的建議是:「學(xué)編程先模仿,遇到不懂的地方先記住,等到自己的積累夠多了,總有一天你會(huì)回過頭發(fā)現(xiàn)自己茅塞頓開,突然懂了」。這也是我個(gè)人學(xué)習(xí)的一個(gè)小技巧。
在學(xué)習(xí)的過程中,也一定會(huì)遇到一些問題,學(xué)習(xí)編程的過程中遇到問題不能自己憋著,「一定要學(xué)會(huì)請(qǐng)教大佬!一定要學(xué)會(huì)請(qǐng)教大佬!一定要學(xué)會(huì)請(qǐng)教大佬」!重要的事情說三遍。
不過也別很簡單的問題就去請(qǐng)教大佬,而且最好給一點(diǎn)小小的報(bào)酬,畢竟誰也沒有義務(wù)幫你解決問題。
我在學(xué)習(xí)的過程中也請(qǐng)教了一些社區(qū)里面的大佬,同時(shí)還進(jìn)入了 Nest.js 的社區(qū)答疑群,向國外友人請(qǐng)教學(xué)到了不少知識(shí)。
當(dāng)然,這個(gè) Demo 中也有很多可以完善的地方,比如說「錯(cuò)誤處理」。
數(shù)據(jù)庫的操作肯定是有可能出現(xiàn)錯(cuò)誤的,比如說我們漏傳了 required: true 的參數(shù),數(shù)據(jù)庫就會(huì)報(bào)錯(cuò)。
這個(gè)時(shí)候我們就要寫一個(gè) try/catch 捕獲這個(gè)異常,或者干脆寫一個(gè)異常的過濾器,將所有的異常統(tǒng)一處理(Nest.js 支持過濾器)
除此之外,既然有可能出現(xiàn)異常,那么我們就需要一個(gè)日志系統(tǒng)去捕獲這個(gè)異常,方便查錯(cuò)糾錯(cuò)。
如果涉及到登錄注冊(cè)的部分,還有密碼加解密的過程,同時(shí)還可能有權(quán)限校驗(yàn)問題需要進(jìn)行處理。
所以后端的同學(xué)肯定不止 CRUD 啦(可算圓回來了)。
這個(gè)教程的所有代碼我都放在了我的 GitHub 倉庫:Nest-CRUD-Demo[7],歡迎大家點(diǎn)個(gè) Star!
參考資料
NestJS - A progressive Node.js framework[8] Nest.js 中文文檔[9]
??愛心三連擊
1.看到這里了就點(diǎn)個(gè)在看支持下吧,你的「點(diǎn)贊,在看」是我創(chuàng)作的動(dòng)力。
2.關(guān)注公眾號(hào)
程序員成長指北,回復(fù)「1」加入Node進(jìn)階交流群!「在這里有好多 Node 開發(fā)者,會(huì)討論 Node 知識(shí),互相學(xué)習(xí)」!3.也可添加微信【ikoala520】,一起成長。
Reference
Nest-CRUD-Demo: https://github.com/wjq990112/Nest-CRUD-Demo
[2]GitHub Repo: https://github.com/nestjs/nest
[3]《走近 MidwayJS :初識(shí) TS 裝飾器與 IoC 機(jī)制》: https://juejin.im/post/6859314697204662279
[4]Midway 官網(wǎng): https://midwayjs.org/midway/
[5]MongoDB 官網(wǎng): https://mongodb.com/download-center/community
點(diǎn)擊這里: 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/
“在看轉(zhuǎn)發(fā)”是最大的支持
