寫給前端的 Nest-js 教程——10 分鐘上手後端接口開發
框架簡介
Nest
是一個用於構建高效,可擴展的Node.js
服務器端應用程序的框架。它使用漸進式JavaScript
,內置並完全支持TypeScript
(但仍然允許開發人員使用純JavaScript
編寫代碼)並結合了OOP
(面向對象編程),FP
(函數式編程)和FRP
(函數式響應編程)的元素。在底層,
Nest
使用強大的HTTP Server
框架,如Express
(默認)和Fastify
。Nest
在這些框架之上提供了一定程度的抽象,同時也將其API
直接暴露給開發人員。這樣可以輕鬆使用每個平臺的無數第三方模塊。
我猜肯定很多同學看不懂這段話,沒關係,我也暫時看不懂,但這不影響我們學會用它 CRUD
。
我們只需要知道它是一款 Node.js
的後端框架,規範化和開箱即用的特性使其在國外開發者社區非常流行,社區也非常活躍,GitHub Repo 擁有 31.1k Star
。
相比於 Express
和 Koa
的千奇百怪五花八門,Nest
確實是一股清流。
不過我們國內也有很棒的 Node.js
框架,比如說 Midway
,和 Nest
一樣,採用的 IoC
的機制,也可以到 Midway 官網自行探索。
包括在 Nest
當中遇到的裝飾器相關的知識,大家也可以到上面林不渡同學的那篇文章中瞭解。
前置知識
-
HTTP
-
TypeScript/JavaScript
項目環境
-
mongodb
-
node.js >= 10.13.0
安裝 MongoDB
這個章節的教程我就只寫 Mac OS
上的安裝了,畢竟上了大學就很少用 Windows
了,用 Windows
的同學可以到 MongoDB
官網選擇對應的系統版本去下載 msi
的安裝包,或者搜索引擎裏搜索一下,記得限定一下結果的時間,保證能夠搜索到最新的教程。
強烈建議使用 Homebrew
來對 Mac OS
的軟件包環境進行管理,沒有安裝的同學可以到這裏下載。
https://brew.sh/
由於目前 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<User>) {}
// 查找所有用戶
async findAll(): Promise<User[]> {
const users = await this.userModel.find();
return users;
}
// 查找單個用戶
async findOne(_id: string): Promise<User> {
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<T = unknown> {
code: number;
data?: T;
message: string;
}
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
// GET /user/users
@Get('users')
async findAll(): Promise<UserResponse<User[]>> {
return {
code: 200,
data: await this.userService.findAll(),
message: 'Success.'
};
}
// GET /user/:_id
@Get(':_id')
async findOne(@Param('_id') _id: string): Promise<UserResponse<User>> {
return {
code: 200,
data: await this.userService.findOne(_id),
message: 'Success.'
};
}
// POST /user
@Post()
async addOne(@Body() body: CreateUserDTO): Promise<UserResponse> {
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<UserResponse> {
await this.userService.editOne(_id, body);
return {
code: 200,
message: 'Success.'
};
}
// DELETE /user/:_id
@Delete(':_id')
async deleteOne(@Param('_id') _id: string): Promise<UserResponse> {
await this.userService.deleteOne(_id);
return {
code: 200,
message: 'Success.'
};
}
}
至此,我們就完成了一個完整的 CRUD
操作,接下來我們來測試一下~
接口測試
接口測試我們用的是 Postman
,大家可以去下載一個,非常好用的接口自測工具。
數據庫可視化工具我們用的是 MongoDB
官方的 MongoDB Compass
,也很不錯。
GET /user/users
GET /user/users
一開始我們的數據庫中什麼都沒有,所以返回了一個空數組,沒用用戶信息。
POST /user
POST /user
現在我們添加一條用戶信息,服務器返回添加成功。
Added
GET /user/:_id
GET /user/:_id
添加完一條用戶信息之後再查詢,可算是能查詢到我的信息了。
PUT /user/:_id
PUT /user/:_id
現在假如我想修改密碼,發送一個 PUT
請求。
Edited
DELETE /user/:_id
DELETE /user/:_id
Deleted
完結撒花
大功告成,CRUD
就這麼簡單,用這個項目去參加一些學校舉行的比賽,拿個獎肯定沒什麼問題,開箱即用(學校老師們別打我)。
總結
教程還算是用了比較通俗易懂的方式爲大家講解了如何寫一個帶有 CRUD
功能的後端 Node.js
應用,框架採用的是 Nest.js
。
相信大家在上面的教程中肯定有非常多不懂的部分,比如說 @Get()
、@Post()
、@Param()
、@Body()
等等的裝飾器,再比如說一些 Nest.js
相關的概念。
沒關係,我的建議是:**學編程先模仿,遇到不懂的地方先記住,等到自己的積累夠多了,總有一天你會回過頭髮現自己茅塞頓開,突然懂了。**這也是我個人學習的一個小技巧。
在學習的過程中,也一定會遇到一些問題,學習編程的過程中遇到問題不能自己憋着,**一定要學會請教大佬!一定要學會請教大佬!一定要學會請教大佬!**重要的事情說三遍。
不過也別很簡單的問題就去請教大佬,而且最好給一點小小的報酬,畢竟誰也沒有義務幫你解決問題。
我在學習的過程中也請教了一些社區裏面的大佬,同時還進入了 Nest.js
的社區答疑羣,向國外友人請教學到了不少知識。
當然,這個 Demo
中也有很多可以完善的地方,比如說錯誤處理。
數據庫的操作肯定是有可能出現錯誤的,比如說我們漏傳了 required: true
的參數,數據庫就會報錯。
這個時候我們就要寫一個 try/catch
捕獲這個異常,或者乾脆寫一個異常的過濾器,將所有的異常統一處理(Nest.js
支持過濾器)
除此之外,既然有可能出現異常,那麼我們就需要一個日誌系統去捕獲這個異常,方便查錯糾錯。
如果涉及到登錄註冊的部分,還有密碼加解密的過程,同時還可能有權限校驗問題需要進行處理。
所以後端的同學肯定不止 CRUD
啦(可算圓回來了)。
這個教程的所有代碼我都放在了我的 GitHub 倉庫:
https://github.com/wjq990112/Nest-CRUD-Demo
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Wng7s2qKXwVIBytI8tkoqA