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

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

DELETE /user/:_id

現在我們刪除一下剛才添加的用戶信息。

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