前言
本系列是參考大佬圖雀社區的教程,采用的是ts+cloudbase開發的,用mysql的可以直接看圖雀大佬的,這也只能用於參考,很多要結合自己實際去更改。
本系列將以前端的視角進行書寫,分享自己的踩坑經歷。教程主要面向前端或者毫無后端經驗,但是又想嘗試 Node.js 的讀者,當然,也歡迎后端大佬斧正。
Nest 是一個用於構建高效,可擴展的 Node.js 服務器端應用程序的框架。它使用漸進式 JavaScript,內置並完全支持 TypeScript(但仍然允許開發人員使用純 JavaScript 編寫代碼)並結合了 OOP(面向對象編程),FP(函數式編程)和 FRP(函數式響應編程)的元素。
在底層,Nest 使用強大的 HTTP Server 框架,如 Express(默認)和 Fastify。Nest 在這些框架之上提供了一定程度的抽象,同時也將其 API 直接暴露給開發人員。這樣可以輕松使用每個平台的無數第三方模塊。
Nest 是我近半年接觸的一款后端框架,之前接觸的是 Koa2,但因為老項目被 “資深” 前端寫的亂七八糟,所以我就選擇了這款以 TypeScript 為主的、最近在國內興起的框架重構了。截止目前,Github 上的 nestjs 擁有 25.2k 個 Star,主要用戶在國外,所以側面可以證明其一定的穩定性。
Nest 采用 MVC 的設計模式,如果有 Angular 項目經驗的讀者,應該會覺得熟悉。我沒寫過 Angular,所以當初學的時候,走了一些彎路,主要是接受這種類 Spring 的設計理念。
好了,碎碎念到此為止,開始吧:
一、項目創建
項目環境:
- node.js: 11.13.0+
- npm: 6.13.4+
- nestjs: 7.0.3
- typescript: 3.8.3
先確操作系統上安裝了 Node.js(>= 10.13.0),然后安裝 Nest.js,然后新建項目,輸入如下指令:
$ npm i -g @nestjs/cli
$ nest new project-name
輸入完后,會初始化,此時,會問你使用哪一種方式來管理依賴包:
我選擇的是 yarn
,主要是國內的 npm
下載得比較慢。如果沒有 yarn
的,可以下載一個,也可以使用 npm
,不過本系列教程都使用 yarn
。
等雞啄完了米,等狗舔完了面,等火燒斷了鎖,就會得到下列信息:
按照提示,進入項目,不出意外,目錄應該是這個樣子的:
運行 yarn run start
或 yarn start
,會看到控制台輸出如下信息,表示服務已啟動:
二、Hello World!
1. 路由指向
打開 src
下的 main.ts
,不出意外,應該會看到下列代碼:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
await NestFactory.create(AppModule);
表示使用 Nest 的工廠函數創建了 AppModule,關於 Module 稍后會介紹。
await app.listen(3000)
表示監聽的是 3000 端口,這個可以自定義。若 3000 端口被占用導致項目啟動失敗,可以修改成其他端口。
然后我們通過 ApiPost 訪問本地的 3000 端口,會發現出現如下信息:
然后我們需要做的就是,找到為什么會出現 Hello World!
的原因。
打開 src
下的 app.service.ts
,會看到如下代碼:
// src/app.service.ts
import { Injectable } from '@nestjs/common';
()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}
發現這里有個方法 getHello()
,返回了 Hello World!
字符串,那么它在哪里被調用呢?
打開 src
下的 app.controller.ts
:
// src/app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
喔,原來如此,這里引入了 app.service.ts
中的 AppService
類,並實例化,然后通過 @Get()
修飾 AppController
里的 getHello()
方法,表示這個方法會被 GET
請求調用。
我們修改一下路由,就是在 @Get()
括號里面寫上字符串:
// src/app.controller.ts
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('hello-world')
getHello(): string {
return this.appService.getHello();
}
}
然后重啟項目(在控制台按下 Ctrl + C 終止項目,然后再輸入 yarn start
),此時我們再訪問 localhost:3000/
,就會發現 404
了:
此時,我們輸入 localhost:3000/hello-world
,熟悉的字符出現了:
這就是 Nest 的路由,是不是很簡單?
2. 局部路由前綴
路由還可以設置局部和全局的前綴,使用前綴可以避免在所有路由共享通用前綴時出現沖突的情況。
還是 app.controller.ts
,在 @Controller()
寫入 lesson-1
,這樣的話就表示當前文件中,所有的路由都有了前綴 lesson-1
:
// src/app.controller.ts
@Controller('lesson-1')
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('hello-world')
getHello(): string {
return this.appService.getHello();
}
}
然后重啟項目(在控制台按下 Ctrl + C 終止項目,然后再輸入 yarn start
),此時我們再訪問 localhost:3000/
,就會發現 404
了:
3. 全局路由前綴
這個更簡單了,只需要在 main.ts
中加上 app.setGlobalPrefix()
:
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('nest-zero-to-one'); // 全局路由前綴
await app.listen(3000);
}
bootstrap();
之后只要請求服務,所有的路由都要加上 nest-zero-to-one
前綴:
4. 使用 nodemon 模式啟動項目
如果不想頻繁重啟,可以使用 yarn start:dev
啟動項目,它會使用 nodemon 監聽文件的變化,並自動重啟服務。
若報錯,原因是可能之前裝過 typescript
或者 nestjs
腳手架,然后新建項目的時候,typescript
版本比較舊,只需在項目中更新到 3.7.0
以上:
yarn add typescript -D
出現這個截圖,但是沒有路由信息,表示 nodemon 的配置需要更改:
|
然后再運行 yarn start:dev
就可以了
或者干脆直接把 main.ts
扔到根目錄去(和 src 同級)
這樣再改動什么文件,都會自動重啟服務了。
三、新增模塊
通過上文,應該熟悉了 NestJS 的設計模式,主要就是 Controller
、Service
、Module
共同努力,形成了一個模塊。
Controller
:傳統意義上的控制器,提供 api 接口,負責處理路由、中轉、驗證等一些簡潔的業務;Service
:又稱為Provider
, 是一系列服務、repo、工廠方法、helper 的總稱,主要負責處理具體的業務,如數據庫的增刪改查、事務、並發等邏輯代碼;Module
:負責將Controller
和Service
連接起來,類似於namespace
的概念;
很直觀的傳統 MVC 結構,有 Spring 開發經驗的后端應該不會陌生。
下面我們通過新增一個 User 模塊來進行實戰:
1. Service
個人習慣先創建 Service,最后再創建 Module,因為 Controller 和 Module 都需要引入 Service,這樣引入的時候就可以有提示了(當然,也可以事先寫 import 語句,但 ESLint 的檢查會冒紅點,強迫症患者表示不接受)。
使用 nest-cli 提供的指令可以快速創建文件,語法如下:
$ nest g [文件類型] [文件名] [文件目錄(src目錄下)]
我們輸入:
$ nest g service user logical
就會發現 src 目錄下多了 logical/user/ 文件夾(個人喜歡將業務邏輯相關的文件放入 logical)
上圖中的 user.service.spec.ts 可以不用管…… 至少我寫了大半年,也沒動過這種文件。
然后我們看一下 user.service.ts
,用指令創建的文件,基本都長這樣:
// src/logical/user/user.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserService {}
於是,我們可以仿照 app.service.ts 來寫一個簡單的業務了:
// src/logical/user/user.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserService {
findOne(username: string): string {
if (username === 'Kid') {
return 'Kid is here';
}
return 'No one here';
}
}
2. Controller
現在,我們來寫控制器,輸入下列命令:
$ nest g controller user logical
初始化的 Controller 基本都長這個樣:
// src/logical/user/user.controller.ts
import { Controller } from '@nestjs/common';
('user')
export class UserController {}
接下來,我們把 Service 的業務邏輯引入進來:
import { Controller, Post, Body } from '@nestjs/common';
import { UserService } from './user.service';
('user')
export class UserController {
constructor(private readonly usersService: UserService) {}
('find-one')
findOne(() body: any) {
return this.usersService.findOne(body.username);
}
}
需要先用構造器實例化,然后才能調用方法,這里使用的是 POST
來接收請求,通過 @Body()
來獲取請求體(request.body)的參數。
我們用 Postman 來測試一下,先隨意傳入一個 username:
再傳入 ‘Kid’:
由此可知,我們成功匹配到了路由,並且編寫的業務生效了。
至此 70% 的流程已經走完,以后開發業務(搬磚),基本都是在 Service 和 Controller 里面折騰了。。。
注意:千萬不要往 Controller 里面添加亂七八糟的東西,尤其不要在里面寫業務邏輯,Controller 就應該保持簡潔、干凈。很多前端剛寫 Node 的時候,都喜歡在這里面寫邏輯,只為了省事,殊不知這對后期的維護是個災難。
3. Module
這個是連接 Service 和 Controller 的東東,很多人會奇怪,上文只是創建了 Service 和 Controller,怎么就可以訪問了呢?
打開 app.module.ts:
// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserService } from './logical/user/user.service';
import { UserController } from './logical/user/user.controller';
@Module({
imports: [],
controllers: [AppController, UserController],
providers: [AppService, UserService],
})
export class AppModule {}
發現使用指令創建文件的時候,已經自動幫我們引入 User 相關文件了,而 main.ts 文件里,又已經引入了 AppModule
,並使用 NestFactory
創建了實例。
因此,如果是新建無關痛癢的子模塊,即使不新建 Module 文件,也能通過路由訪問。
但是作為教程,還是大致說一下吧,先創建文件:
$ nest g module user logical
初始化的 Module 基本都長這個樣:
import { Module } from '@nestjs/common';
@Module({})
export class UserModule {}
我們把 Service 和 Controller 組裝起來:
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
@Module({
controllers: [UserController],
providers: [UserService],
exports: [UserService],
})
export class UserModule {}
這樣做有什么好處呢,就是其他 Module 想引入 User 的時候,就不用同時引入 Service 和 Controller 了,我們修改一下 app.module.ts
:
// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
// import { UserService } from './logical/user/user.service';
// import { UserController } from './logical/user/user.controller';
import { UserModule } from './logical/user/user.module';
@Module({
imports: [UserModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
保存運行,發現路由依然生效:
當然,Module 還有其他高級玩法,這個就不在這里展開了。
總結
本篇介紹了 Nest.js 項目的創建,路由的訪問,以及如何新增模塊。
每個模塊又可分為 Service、Controller、Module。在本篇中:Service 負責處理邏輯、Controller 負責路由、Module 負責整合。
通過實戰可以看出,Nest 還是相對簡單的,唯一的障礙可能就是 TypeScript 了。
寫慣了 JavaScript 的人,可能不是很能適應這種類型檢查,尤其是熱衷於使用各種騷操作的,不過既然涉及到了后端領域,還是嚴謹一點比較好,前期可以避免各種不規范導致的坑。
下一篇將介紹如何連接 MySQL 數據庫。