寫給前端的 Nest-js 教程——10 分鐘上手後端接口開發

框架簡介


Nest 是一個用於構建高效,可擴展的 Node.js 服務器端應用程序的框架。它使用漸進式 JavaScript,內置並完全支持 TypeScript(但仍然允許開發人員使用純 JavaScript 編寫代碼)並結合了 OOP(面向對象編程),FP(函數式編程)和 FRP(函數式響應編程)的元素。

在底層,Nest 使用強大的 HTTP Server 框架,如 Express(默認)和 FastifyNest 在這些框架之上提供了一定程度的抽象,同時也將其 API 直接暴露給開發人員。這樣可以輕鬆使用每個平臺的無數第三方模塊。

我猜肯定很多同學看不懂這段話,沒關係,我也暫時看不懂,但這不影響我們學會用它 CRUD

我們只需要知道它是一款 Node.js 的後端框架,規範化開箱即用的特性使其在國外開發者社區非常流行,社區也非常活躍,GitHub Repo 擁有 31.1k Star

相比於 ExpressKoa 的千奇百怪五花八門,Nest 確實是一股清流。

不過我們國內也有很棒的 Node.js 框架,比如說 Midway,和 Nest 一樣,採用的 IoC 的機制,也可以到 Midway 官網自行探索。

包括在 Nest 當中遇到的裝飾器相關的知識,大家也可以到上面林不渡同學的那篇文章中瞭解。

前置知識

項目環境

安裝 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 中,爲模塊提供相應的服務。

挺多文檔將 controllerprovider 翻譯爲控制器提供者,我感覺這種翻譯挺生硬的,讓人不知所云,所以我們姑且記憶他們的英文名吧。

controllerprovider 都創建完後,我們又會驚奇地發現,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