寫給前端的 Nest.js 教程——10分鐘上手后端接口開發(fā)
前言
很多后端的同學都說:「你們前端不就是切個圖嘛,憑啥跟我們后端的同學平起平坐???」
這下前端的同學可以站起來了:「你們后端不也就是 CRUD 嘛,憑啥瞧不起我們前端的同學啊?」
今天就寫一下最近做「畢業(yè)設(shè)計」用到的框架:Nest.js 的基礎(chǔ)教程吧,簡單教大家做一下 CRUD(小白向,大佬輕噴)。
挖個坑,這應(yīng)該是最基礎(chǔ)的「第一章」吧,「如果大家覺得好就多點贊評論」,過 「200 點贊」就加更一些,「爭取讓大家從前端走向全棧吧」。
這個教程的所有代碼我都放在了我的 GitHub 倉庫:Nest-CRUD-Demo[1],歡迎大家點個 Star!
?同時也歡迎大家關(guān)注 「「Hello FE」」,里面有非常多其他的精品好文,不論是還在學習前端的同學還是已經(jīng)工作了一段時間的朋友,都可以閱讀一下(關(guān)注還有小驚喜,鏈接過期了可以在后臺回復,我看到了會回復新的鏈接)。
?
框架簡介
?
Nest是一個用于構(gòu)建高效,可擴展的Node.js服務(wù)器端應(yīng)用程序的框架。它使用漸進式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使用強大的HTTP Server框架,如Express(默認)和Fastify。Nest在這些框架之上提供了一定程度的抽象,同時也將其API直接暴露給開發(fā)人員。這樣可以輕松使用每個平臺的無數(shù)第三方模塊。
我猜肯定很多同學看不懂這段話,沒關(guān)系,我也暫時看不懂,但這不影響我們學會用它 CRUD。
我們只需要知道它是一款 Node.js 的后端框架,「規(guī)范化」和「開箱即用」的特性使其在國外開發(fā)者社區(qū)非常流行,社區(qū)也非常活躍,GitHub Repo[2] 擁有 31.1k Star。
相比于 Express 和 Koa 的千奇百怪五花八門,Nest 確實是一股清流。
不過我們國內(nèi)也有很棒的 Node.js 框架,比如說 Midway,和 Nest 一樣,采用的 IoC 的機制,想了解一下的同學可以看我的小伙伴「林不渡」寫的文章:《走近 MidwayJS :初識 TS 裝飾器與 IoC 機制》[3],還可以到 Midway 官網(wǎng)[4]自行探索。
包括在 Nest 當中遇到的裝飾器相關(guān)的知識,大家也可以到上面「林不渡」同學的那篇文章中了解。
前置知識
HTTP TypeScript/JavaScript
項目環(huán)境
git mongodb node.js >= 10.13.0
安裝 MongoDB
這個章節(jié)的教程我就只寫 Mac OS 上的安裝了,畢竟上了大學就很少用 Windows 了,用 Windows 的同學可以到 `MongoDB` 官網(wǎng)[5]選擇對應(yīng)的系統(tǒng)版本去下載 msi 的安裝包,或者「搜索引擎」里搜索一下,記得限定一下結(jié)果的時間,保證能夠搜索到最新的教程。
強烈建議使用 Homebrew 來對 Mac OS 的軟件包環(huán)境進行管理,沒有安裝的同學可以點擊這里[6]下載。
由于目前 MongoDB 已經(jīng)不開源了,因此我們想要安裝 MongoDB 就只能安裝社區(qū)版本。
brew?tap?mongodb/brew
brew?install?mongodb-community
安裝好之后我們就可以啟動 MongoDB 的服務(wù)了:
brew?services?start?mongodb-community
服務(wù)啟動了就不用管了,如果要關(guān)閉的話可以把 start 改成 stop,就能夠停止 MongoDB 的服務(wù)了。
構(gòu)建項目
有兩種方式,可以自行選擇,兩者沒有區(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
這兩條命令的效果完全一致,就是初始化一個 Nest.js 的項目到當前文件夾下,項目的文件夾名字為 nest-crud-demo,兩種方式都可以。
「當然,我還是建議采用第一種方式,因為后面我們可以直接使用腳手架工具生成項目文件。」
啟動服務(wù)
cd?nest-crud-demo
npm?run?start:dev?或者?yarn?run?start:dev
就可以「以開發(fā)模式」啟動我們的項目了。
這里其實有一個小小的點,就是啟動的時候應(yīng)該以 dev 模式啟動,這樣 Nest 會「自動檢測我們的文件變化」,然后「自動重啟服務(wù)」。
如果是直接 npm start 或者 yarn start 的話,雖然服務(wù)啟動了,但是我們?nèi)绻陂_發(fā)的過程中修改了文件,就要手動停止服務(wù)然后重新啟動,效率挺低的。
安裝依賴
項目中我們會用到 Mongoose 來操作我們的數(shù)據(jù)庫,Nest 官方為我們提供了一個 Mongoose 的封裝,我們需要安裝 mongoose 和 @nestjs/mongoose:
npm?install?mongoose?@nestjs/mongoose?--save
安裝好之后我們就可以開始編碼過程了。
編寫代碼
創(chuàng)建 Module
我們這次就創(chuàng)建一個 User 模塊,寫一個用戶增刪改查,帶大家熟悉一下這個過程。
nest?g?module?user?server
腳手架工具會自動在 src/server/user 文件夾下創(chuàng)建一個 user.module.ts,這是 Nest 的模塊文件,Nest 用它來組織整個應(yīng)用程序的結(jié)構(gòu)。
//?user.module.ts
import?{?Module?}?from?'@nestjs/common';
@Module({})
export?class?UserModule?{}
同時還會在根模塊 app.module.ts 中引入 UserModule 這個模塊,相當于一個樹形結(jié)構(gòu),在根模塊中引入了 User 模塊。
執(zhí)行上面的終端命令之后,我們會驚訝地發(fā)現(xiàn),app.module.ts 中的代碼已經(jīng)發(fā)生了變化,在文件頂部自動引入了 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?{}
創(chuàng)建 Controller
nest?g?controller?user?server
在 Nest 中,controller 就類似前端的「路由」,負責處理「客戶端傳入的請求」和「服務(wù)端返回的響應(yīng)」。
舉個例子,我們?nèi)绻ㄟ^ http://localhost:3000/user/users 獲取所有的用戶信息,那么我們可以在 UserController 中創(chuàng)建一個 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 的作用,負責分發(fā)和處理「請求」和「響應(yī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ù)的提供者」。
怎么去理解這個「服務(wù)提供者」呢?舉個例子,我們的 controller 接收到了一個用戶的查詢請求,我們不能直接在 controller 中去查詢數(shù)據(jù)庫并返回,而是要將查詢請求交給 provider 來處理,這里我們創(chuàng)建了一個 UserService,就是用來提供「數(shù)據(jù)庫操作服務(wù)」的。
//?user.service.ts
import?{?Injectable?}?from?'@nestjs/common';
@Injectable()
export?class?UserService?{}
當然,provider 不一定只能用來提供數(shù)據(jù)庫的操作服務(wù),還可以用來做一些用戶校驗,比如使用 JWT 對用戶權(quán)限進行校驗的策略,就可以寫成一個策略類,放到 provider 中,為模塊提供相應(yīng)的服務(wù)。
挺多文檔將 controller 和 provider 翻譯為「控制器」和「提供者」,我感覺這種翻譯挺生硬的,讓人不知所云,所以我們姑且記憶他們的英文名吧。
controller 和 provider 都創(chuàng)建完后,我們又會驚奇地發(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ù)據(jù)庫的地址,xxx 是數(shù)據(jù)庫的名字。
這時候保存文件,肯定有同學會發(fā)現(xiàn)控制臺還是報錯的,我們看一下報錯信息就很容易知道問題在哪里了。
其實就是 mongoose 模塊沒有類型聲明文件,這就很容易解決了,安裝一下就好:
npm?install?@types/mongoose?--dev?或者?yarn?add?@types/mongoose?--dev
安裝完之后服務(wù)就正常重啟了。
引入 Mongoose 分模塊
這里我們先要創(chuàng)建一個數(shù)據(jù)表的格式,在 src/server/user 文件夾下創(chuàng)建一個 user.schema.ts 文件,定義一個數(shù)據(jù)表的格式:
//?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?{}
好了,現(xiàn)在一切就緒,終于可以開始編寫我們的 CRUD 邏輯了!沖沖沖~
CRUD
我們打開 user.service.ts 文件,為 UserService 類添加一個構(gòu)造函數(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;
??}
??//?查找單個用戶
??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 操作數(shù)據(jù)庫其實是異步的,所以這里我們使用 async 函數(shù)來處理異步的過程。
好奇的同學會發(fā)現(xiàn),這里突然出現(xiàn)了兩個文件,一個是 user.interface.ts,另一個是 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ù)據(jù)類型做了一個定義。
現(xiàn)在,我們可以到 user.controller.ts 中設(shè)置路由了,將「客戶端的請求」進行處理,調(diào)用相應(yīng)的服務(wù)實現(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.'
????};
??}
}
至此,我們就完成了一個完整的 CRUD 操作,接下來我們來測試一下~
接口測試
接口測試我們用的是 Postman,大家可以去下載一個,非常好用的接口自測工具。
數(shù)據(jù)庫可視化工具我們用的是 MongoDB 官方的 MongoDB Compass,也很不錯。
GET /user/users

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

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

GET /user/:_id

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

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

DELETE /user/:_id

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

會發(fā)現(xiàn)數(shù)據(jù)庫中的內(nèi)容已經(jīng)被刪除了。
完結(jié)撒花
大功告成,CRUD 就這么簡單,用這個項目去參加一些學校舉行的比賽,拿個獎肯定沒什么問題,開箱即用(學校老師們別打我)。
總結(jié)
教程還算是用了比較通俗易懂的方式為大家講解了如何寫一個帶有 CRUD 功能的后端 Node.js 應(yīng)用,框架采用的是 Nest.js。
相信大家在上面的教程中肯定有非常多不懂的部分,比如說 @Get()、@Post()、@Param()、@Body() 等等的裝飾器,再比如說一些 Nest.js 相關(guān)的概念。
沒關(guān)系,我的建議是:「學編程先模仿,遇到不懂的地方先記住,等到自己的積累夠多了,總有一天你會回過頭發(fā)現(xiàn)自己茅塞頓開,突然懂了」。這也是我個人學習的一個小技巧。
在學習的過程中,也一定會遇到一些問題,學習編程的過程中遇到問題不能自己憋著,「一定要學會請教大佬!一定要學會請教大佬!一定要學會請教大佬」!重要的事情說三遍。
不過也別很簡單的問題就去請教大佬,而且最好給一點小小的報酬,畢竟誰也沒有義務(wù)幫你解決問題。
我在學習的過程中也請教了一些社區(qū)里面的大佬,同時還進入了 Nest.js 的社區(qū)答疑群,向國外友人請教學到了不少知識。
當然,這個 Demo 中也有很多可以完善的地方,比如說「錯誤處理」。
數(shù)據(jù)庫的操作肯定是有可能出現(xiàn)錯誤的,比如說我們漏傳了 required: true 的參數(shù),數(shù)據(jù)庫就會報錯。
這個時候我們就要寫一個 try/catch 捕獲這個異常,或者干脆寫一個異常的過濾器,將所有的異常統(tǒng)一處理(Nest.js 支持過濾器)
除此之外,既然有可能出現(xiàn)異常,那么我們就需要一個日志系統(tǒng)去捕獲這個異常,方便查錯糾錯。
如果涉及到登錄注冊的部分,還有密碼加解密的過程,同時還可能有權(quán)限校驗問題需要進行處理。
所以后端的同學肯定不止 CRUD 啦(可算圓回來了)。
這個教程的所有代碼我都放在了我的 GitHub 倉庫:Nest-CRUD-Demo[7],歡迎大家點個 Star!
參考資料
NestJS - A progressive Node.js framework[8] Nest.js 中文文檔[9]
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 官網(wǎng): https://midwayjs.org/midway/
[5]MongoDB 官網(wǎng): 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/
