標籤:

精讀 《Nestjs 文檔》

本期精讀的文章是:Nestjs 文檔

體驗一下 nodejs mvc 框架的優雅設計。

1 引言

Nestjs 是我見過的,將 Typescript 與 Nodejs Framework 結合的最好的例子。

2 內容概要

Nestjs 不是一個新輪子,它是基於 Express、socket.io 封裝的 nodejs 後端開發框架,對 Typescript 開發者提供類型支持,也能優雅降級供 Js 使用,擁有諸多特性,像中間件等就不展開了,本文重點列舉其亮點特性。

2.1 Modules, Controllers, Components

Nestjs 開發圍繞著這三個單詞,Modules 是最大粒度的拆分,表示應用或者模塊。Controllers 是傳統意義的控制器,一個 Module 擁有多個 Controller。Components 一般用於做 Services,比如將資料庫 CRUD 封裝在 Services 中,每個 Service 就是一個 Component。

2.2 裝飾器路由

裝飾器路由是個好東西,路由直接標誌在函數頭上,做到了路由去中心化:

@Controller()nexport class UsersController {n @Get(users)n getAllUsers() {}nn @Get(users/:id)n getUser() {}nn @Post(users)n addUser() {}n}n

以前用過 Go 語言框架 Beego,就是採用了中心化路由管理方式,雖然引入了 namespace 概念,但當協作者多、模塊體量巨大時,路由管理成本直線上升。Nestjs 類似 namespace 的概念通過裝飾器實現:

@Controller(users)nexport class UsersController {n @Get()n getAllUsers(req: Request, res: Response, next: NextFunction) {}n}n

訪問 /users 時會進入 getAllUsers 函數。可以看到其 namespace 也是去中心化的。

2.3 模塊間依賴注入

Modules, Controllers, Components 之間通過依賴注入相互關聯,它們通過同名的 @Module @Controller @Component 裝飾器申明,如:

@Controller()nexport class UsersController {n @Get(users)n getAllUsers() {}n}n@Component()nexport class UsersService {n getAllUsers() {n return []n }n}n@Module({n controllers: [ UsersController ],n components: [ UsersService ],n})nexport class ApplicationModule {}n

在 ApplicationModule 申明其內部 Controllers 與 Components 後,就可以在 Controllers 中注入 Components 了:

@Controller()nexport class UsersController {ntconstructor(private usersService: UsersService) {}nn @Get(users)n getAllUsers() {n treturn this.usersService.getAllUsers()n }n}n

2.4 裝飾器參數

與大部分框架從 this.req 或 this.context 等取請求參數不同,Nestjs 通過裝飾器獲取請求參數:

@Get(/:id)npublic async getUser(nt@Response() res,nt@Param(id) id,n) {n const user = await this.usersService.getUser(id);n res.status(HttpStatus.OK).json(user);n}n

@Response 獲取 res,@Param 獲取路由參數,@Query 獲取 url query 參數,@Body 獲取 Http body 參數。

3 精讀

由於臨近雙十一,項目工期很緊張,本期精讀由我獨自完成 :p。

3.1 Typeorm

有了如此強大的後端框架,必須搭配上同等強大的 orm 才能發揮最大功力,Typeorm 就是最好的選擇之一。它也完全使用 Typescript 編寫,使用方式具有同樣的藝術氣息。

3.1.1 定義實體

每個實體對應資料庫的一張表,Typeorm 在每次啟動都會同步表結構到資料庫,我們完全不用使用資料庫查看錶結構,所有結構信息都定義在代碼中:

@Entity()nexport class Card {n @PrimaryGeneratedColumn({n comment: 主鍵,n })n id: number;nn @Column({n comment: 名稱,n length: 30,n unique: true,n })n name: string = nick;n}n

通過 @Entity 將類定義為實體,每個成員變數對應表中的每一列,如上定義了 id name 兩個列,同時列 id 通過 @PrimaryGeneratedColumn 定義為了主鍵列,列 name 通過參數定義了其最大長度、唯一的信息。

至於類型,Typeorm 通過反射,拿到了類型定義,自動識別 id 為數字類型、name 為字元串類型,當然也可以手動設置 type 參數。

對於初始值,使用 js 語法就好,比如將 name 初始值設置為 nick,在 new Card() 時已經帶上了初始值。

3.1.2 自動校驗

光判斷參數類型是不夠的,我們可以使用 class-validator 做任何形式的校驗:

@Column({ntcomment: 配置 JSON,ntlength: 5000,n})n@Validator.IsString({ message: 必須為字元串 })n@Validator.Length(0, 5000, { message: 長度在 0~5000 })ncontent: string;n

這裡遇到一個問題:新增實體時,需要校驗所有欄位,但更新實體時,由於性能需要,我們一般不會一次查詢所有欄位,就需要指定更新時,不校驗沒有賦值的欄位,我們通過 Typeorm 的 EventSubscriber 完成資料庫操作前的代碼校驗,並控制新增時全欄位校驗,更新時只校驗賦值的欄位,刪除時不做校驗:

@EventSubscriber()nexport class EverythingSubscriber implements EntitySubscriberInterface<any> {n // 插入前校驗n async beforeInsert(event: InsertEvent<any>) {n const validateErrors = await validate(event.entity);n if (validateErrors.length > 0) {n throw new HttpException(getErrorMessage(validateErrors), 404);n }n }nn // 更新前校驗n async beforeUpdate(event: UpdateEvent<any>) {n const validateErrors = await validate(event.entity, {n // 更新操作不會驗證沒有涉及的欄位n skipMissingProperties: true,n });n if (validateErrors.length > 0) {n throw new HttpException(getErrorMessage(validateErrors), 404);n }n }n}n

HttpException 會在校驗失敗後,終止執行,並立即返回錯誤給客戶端,這一步體現了 Nestjs 與 Typeorm 完美結合。這帶來的好處就是,我們放心執行任何 CRUD 語句,完全不需要做錯誤處理,當校驗失敗或者資料庫操作失敗時,會自動終止執行後續代碼,並返回給客戶端友好的提示:

@Post()nasync add(n @Res() res: Response,n @Body(name) name: string,n @Body(description) description: string,n) {n const card = await this.cardService.add(name, description);n // 如果傳入參數實體校驗失敗,會立刻返回失敗,並提示 `@Validator.IsString({ message: 必須為字元串 })` 註冊時的提示信息n // 如果插入失敗,也會立刻返回失敗n // 所以只需要處理正確情況n res.status(HttpStatus.OK).json(card);n}n

3.1.3 外鍵

外鍵也是 Typeorm 的特色之一,通過裝飾器語義化解釋實體之間的關係,常用的有 @OneToOne @OneToMany @ManyToOne@ManyToMany 四種,比如用戶表到評論表,是一對多的關係,可以這樣設置實體:

@Entity()nexport class User {n @PrimaryGeneratedColumn({n comment: 主鍵,n })n id: number;nn @OneToMany(type => Comment, comment => comment.user)n comments?: Comment[];n}n@Entity()nexport class Comment {n @PrimaryGeneratedColumn({n comment: 主鍵,n })n id: number;nn @ManyToOne(type => User, user => user.Comments)n @JoinColumn()n user: User;n}n

對 User 來說,一個 User 對應多個 Comment,就使用 OneToMany 裝飾器裝飾 Comments 欄位;對 Comment 來說,多個 Comment 對應一個 User,所以使用 ManyToOne 裝飾 User 欄位。

在使用 Typeorm 查詢 User 時,會自動外鍵查詢到其關聯的評論,保存在 user.comments 中。查詢 Comment 時,會自動查詢到其關聯的 User,保存在 comment.user 中。

3.2 部署

可以使用 Docker 部署 Mysql + Nodejs,通過 docker-compose 將資料庫與服務都跑在 docker 中,內部通信。

有一個問題,就是 nodejs 服務運行時,要等待資料庫服務啟動完畢,也就是有一個啟動等待的需求。可以通過 environment來拓展等待功能,以下是 docker-compose.yml:

version: "2"nservices:n app:n build: ./n restart: alwaysn ports:n - "5000:8000"n links:n - dbn - redisn depends_on:n - dbn - redisn environment:n WAIT_HOSTS: db:3306 redis:6379n

通過 WAIT_HOSTS 指定要等待哪些服務的埠服務 ready。在 nodejs Dockerfile 啟動的 CMD 加上一個 wait-for.sh 腳本,它會讀取 WAIT_HOSTS 環境變數,等待埠 ready 後,再執行後面的啟動腳本。

CMD ./scripts/docker/wait-for.sh && npm run deployn

以下是 wait.sh 腳本內容:

#!/bin/bashnnset -enntimeout=${WAIT_HOSTS_TIMEOUT:-30}nwaitAfterHosts=${WAIT_AFTER_HOSTS:-0}nwaitBeforeHosts=${WAIT_BEFORE_HOSTS:-0}nnecho "Waiting for ${waitBeforeHosts} seconds."nsleep $waitBeforeHostsnn# our target format is a comma separated list where each item is "host:ip"nif [ -n "$WAIT_HOSTS" ]; thenn uris=$(echo $WAIT_HOSTS | sed -e s/,/ /g -e s/s+/n/g | uniq)nfinn# wait for each targetnif [ -z "$uris" ];n then echo "No wait targets found." >&2;nn elsenn for uri in $urisn don host=$(echo $uri | cut -d: -f1)n port=$(echo $uri | cut -d: -f2)n [ -n "${host}" ]n [ -n "${port}" ]n echo "Waiting for ${uri}."n seconds=0n while [ "$seconds" -lt "$timeout" ] && ! nc -z -w1 $host $portn don echo -n .n seconds=$((seconds+1))n sleep 1n donenn if [ "$seconds" -lt "$timeout" ]; thenn echo "${uri} is up!"n elsen echo " ERROR: unable to connect to ${uri}" >&2n exit 1n fin donenecho "All hosts are up"nfinnecho "Waiting for ${waitAfterHosts} seconds."nsleep $waitAfterHostsnnexit 0n

4 總結

Nestjs 中間件實現也很精妙,與 Modules 完美結合起來,由於篇幅限制就不展開了。

後端框架已經很成熟了,相反前端發展的就眼花繚亂了,如果前端可以捨棄 ie11 瀏覽器,我推薦純 proxy 實現的 dob,配合 react 效率非常高。

討論地址是:精讀 《Nestjs 文檔》 · Issue #30 · dt-fe/weekly

如果你想參與討論,請點擊這裡,每周都有新的主題,每周五發布。

推薦閱讀:

怎麼才能成為一個nodejs大神?
nodejs 應該學習哪些框架?
參加第11屆D2前端技術論壇,你有什麼收穫?
手機遊戲伺服器端用node.js 還是用go,fibjs之類等比較好?

TAG:Nodejs | Framework |