精讀 《Nestjs 文檔》
本期精讀的文章是:Nestjs 文檔
體驗一下 nodejs mvc 框架的優雅設計。
1 引言
Nestjs 是我見過的,將 Typescript 與 Nodejs Framework 結合的最好的例子。
2 內容概要
Nestjs 不是一個新輪子,它是基於 Express、http://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之類等比較好?