歡迎關注我的公衆號睿Talk
,獲取我最新的文章:
一、前言
做過 Java EE 開發的朋友對 Spring 框架應該很熟悉了,它全面的功能和優秀的設計是得以廣泛流行的原因。它通過靈活使用控制反轉、依賴注入和麪向切面編程等設計理念,極大的規範了大型應用的架構,降低了模塊之間的耦合度,從而提升了應用的開發效率。在 NodeJS 的世界裏,也存在一個全面借鑑 Spring 設計思想的框架,它在 github 上有將近 2w 的 star,npm 的周下載量超過 11w,它就是本文要介紹的 NestJS。
二、與其它框架的對比
市面上 NodeJS 的服務端框架有很多,如Koa
、Express
、EggJS
、Midway
等,它們功能都很強大,也有很好的生態,插件非常豐富,爲什麼還需要Nest
呢?
如果是一個簡單的應用,其實用什麼框架都無所謂,一個框架用 100 行代碼實現,另一個用 80 行,區別不大。但涉及到企業級的應用,分分鐘有上萬行的代碼,代碼的組織結構就變得很重要了。如果代碼拆分不合理,一個 JS 文件就有上千行的代碼,後期的維護成本會非常的高。再考慮到複雜項目參與者衆多,沒有一個規範去約束的話每個人寫出來的代碼風格迥異,協作起來會很難受。上文提到的幾個框架對項目代碼的架構要麼是沒約束,要麼就是約束比較弱或者看起來很彆扭。相比之下Nest
的實現就很簡潔,用起來很順手。具體細節將在下文進行描述。
Nest
還通過依賴注入的形式實現了控制反轉,只要聲明模塊中的依賴,Nest
就會在啓動的時候去創建依賴,然後自動注入到相應的地方。依賴注入最大的作用是代碼解耦,依賴的對象根據不同的情況可以有多種實現,如單元測試的時候可以在不改業務代碼的情況下將依賴的對象換成 Mock 數據。
Nest
還踐行了面向切面編程的思想,除了Middleware
外,還有Exception Filter
、Pipes
、Guards
和Interceptors
幾個預定義的切面,可以集中進行異常處理、數據驗證、權限驗證和邏輯擴展等功能。Nest
自帶如數據驗證等一些常用的基於切面的功能,也可以通過繼承的方式來進行擴展。這些預定義的切面是代碼架構的組成部分,按照這些約定來組織代碼會大大降低日後的維護成本。
類型系統是後端開發很重要的一環,Nest
是使用TypeScript
實現的框架,因此原生就支持TypeScript
,而且還大量使用了註解,熟悉 Spring 的朋友會感到十分親切。
另外,Nest
是基於Express
實現的,需要的話可以取到底層的對象,如request
和response
。
三、實戰
下面的講解將會基於一個簡單的增刪改查 API 服務器,完整項目在這裏。
模塊化
項目是以模塊的形式組織起來的,模塊中可以聲明Controller
、Provider
、Import
和Export
。打開app.module.ts
,內容如下:
@Module({
imports: [CatsModule, MongooseModule.forRoot('mongodb://localhost/nest')],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
可以看到項目的根模塊AppModule
導入了項目中的另一個模塊CatsModule
和外部依賴MongooseModule
。另外也聲明瞭模塊內部的Controller
和Provider
。我們一般說的Service
是Provider
的一種。Module
、Controller
和Provider
的關係見下圖:
Controller
和Provider
都在Module
註冊,容器會將Provider
注入到Controller
中,Module
之間可以相互引用(Import)。像 ES6 的模塊化一樣,Import
後只能使用別人Export
出來的內容。
註解
再來看一下cats.controller.ts
。
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Get(':name')
async findOne(@Param('name') name: string): Promise<Cat> {
return this.catsService.findOne(name);
}
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
@Post()
@HttpCode(201)
@Header('Cache-Control', 'none')
async create(
@Body(new ValidationPipe()) createCatDto: CreateCatDto,
): Promise<Cat[]> {
return this.catsService.create(createCatDto);
}
}
這文件有大量的註解,這是Nest
有別於其它 NodeJS 框架的地方,像極了 Spring。很多註解的含義也與 Spring 的一致,像這裏的@Controller
、@Get
和@Post
都是用來聲明路由和 http 請求類型的。@Get(':name')
是獲取 url 的參數,而@Param('name')
是獲取請求體的參數。@Body(new ValidationPipe()) createCatDto: CreateCatDto
這行代碼做了很多事,首先將請求體取出,然後校驗數據類型是否合規,然後再將請求體轉換爲 DTO 對象供後續使用。DTO 的定義如下,也是通過註解定義校驗邏輯:
export class CreateCatDto {
@IsString()
readonly name: string;
@IsNumber()
readonly age: number;
@IsString()
readonly breed: string;
}
切面
上面提到的ValidationPipe
是內置的Pipe
切面,用於校驗參數類型。另外幾種切面和請求處理的順序見下圖:
這裏的Middleware
就是Express
原生的,其它幾個切面的用法見官方文檔,在此不多作介紹。
連接數據庫
例子中使用mongoose
連接和操作本地MongoDB
數據庫。爲了更方便使用,Nest
提供了@nestjs/mongoose
包,對mongoose
包裝了一層,使其更符合Nest
的使用風格。操作數據庫的步驟如下:
-
app.module
中定義連接的數據庫:MongooseModule.forRoot('mongodb://localhost/nest')
-
cat.schema
中定義 Schema -
cats.module
中聲明依賴 Model:MongooseModule.forFeature([{ name: 'Cat', schema: CatSchema }])
-
cats.service
中注入依賴 Model:constructor(@InjectModel('Cat') private readonly catModel: Model<Cat>) {}
-
cats.service
中使用 Model:this.catModel.findOne({ name }).exec()
四、總結
本文重點介紹了Nest
的設計思想,比較了它跟其它框架的異同,並結合實例詳細講解了具體的用法。文章的寫作目的是爲框架選型者提供一個快速的參考,也爲對Nest
感興趣的人提供感性的認識。如果想更詳細的瞭解Nest
用法,請看官方文檔。