TypeScript在node項目中的實踐

TypeScript在node項目中的實踐

TypeScript可以理解爲是JavaScript的一個超集,也就是說涵蓋了所有JavaScript的功能,並在之上有着自己獨特的語法。
最近的一個新項目開始了TS的踩坑之旅,現分享一些可以借鑑的套路給大家。

爲什麼選擇TS

作爲巨硬公司出品的一個靜態強類型編譯型語言,該語言已經出現了幾年的時間了,相信在社區的維護下,已經是一門很穩定的語言。
我們知道,JavaScript是一門動態弱類型解釋型腳本語言,動態帶來了很多的便利,我們可以在代碼運行中隨意的修改變量類型以達到預期目的。
但同時,這是一把雙刃劍,當一個龐大的項目出現在你的面前,面對無比複雜的邏輯,你很難通過代碼看出某個變量是什麼類型,這個變量要做什麼,很可能一不小心就會踩到坑。

而靜態強類型編譯能夠帶來很多的好處,其中最重要的一點就是可以幫助開發人員杜絕一些馬虎大意的問題:
image
圖爲rollbar統計的數千個項目中數量最多的前十個異常

不難看出,因爲類型不匹配、變量爲空導致的異常比你敢承認的次數要多。
譬如

而這一點在TS中得到了很好的改善,任何一個變量的引用,都需要指定自己的類型,而你下邊在代碼中可以用什麼,支持什麼方法,都需要在上邊進行定義:

這個提示會在開發、編譯期來提示給開發者,避免了上線以後發現有問題,再去修改。

另外一個由靜態編譯類型帶來的好處,就是函數簽名。
還是就像上邊所說的,因爲是一個動態的腳本語言,所以很難有編輯器能夠在開發期間正確地告訴你所要調用的一個函數需要傳遞什麼參數,函數會返回什麼類型的返回值。

而在TS中,對於一個函數,首先你需要定義所有參數的類型,以及返回值的類型。
這樣在函數被調用時,我們就可以很清晰的看到這個函數的效果:

這是最基礎的、能夠讓程序更加穩定的兩個特性,當然,還有更多的功能在TS中的:TypeScript | Handbook

TypeScript在node中的應用

在TS的官網中,有着大量的示例,其中就找到了Express版本的例子,針對這個稍作修飾,應用在了一個 koa 項目中。

環境依賴

在使用TS之前,需要先準備這些東西:

  1. VS code,同爲巨硬公司出品,本身就是TS開發的,遂該編輯器是目前對TS支持度最高的一個
  2. Node.js 推薦8.11版本以上
  3. npm i -g typescript,全局安裝TS,編譯所使用的tsc命令在這裏
  4. npm i -g nodemon,全局安裝nodemon,在tsc編譯後自動刷新服務器程序

以項目中使用的一些核心依賴:

  1. reflect-metadata: 大量裝飾器的包都會依賴的一個基礎包,用於注入數據
  2. routing-controllers: 使用裝飾器的方式來進行koa-router的開發
  3. sequelize: 抽象化的數據庫操作
  4. sequelize-typescript: 上述插件的裝飾器版本,定義實體時使用

項目結構

首先,放出目前項目的結構:

.
├── README.md
├── copy-static-assets.ts
├── nodemon.json
├── package-lock.json
├── package.json
├── dist
├── src
│   ├── config
│   ├── controllers
│   ├── entity
│   ├── models
│   ├── middleware
│   ├── public
│   ├── app.ts
│   ├── server.ts
│   ├── types
│   └── utils
├── tsconfig.json
└── tslint.json

 

src爲主要開發目錄,所有的TS代碼都在這裏邊,在經過編譯過後,會生成一個與src同級的dist文件夾,這個文件夾是node引擎實際運行的代碼。
src下,主要代碼分爲了如下結構(依據自己項目的實際情況進行增刪):

#folderdesc
1 controllers 用於處理接口請求,原appsroutes文件夾。
2 middleware 存放了各種中間件、全局 or 自定義的中間件
3 config 各種配置項的位置,包括端口、log路徑、各種巴拉巴拉的常量定義。
4 entity 這裏存放的是所有的實體定義(使用了sequelize進行數據庫操作)。
5 models 使用來自entity中的實體進行sequelize來完成初始化的操作,並將sequelize對象拋出。
6 utils 存放的各種日常開發中提煉出來的公共函數
7 types 存放了各種客製化的複合類型的定義,各種結構、屬性、方法返回值的定義(目前包括常用的Promise版redis與qconf)

controllers

controllers只負責處理邏輯,通過操作model對象,而不是數據庫來進行數據的增刪改查

鑑於公司絕大部分的Node項目版本都已經升級到了Node 8.11,理所應當的,我們會嘗試新的語法。
也就是說我們會拋棄Generator,擁抱async/await 。

使用KoaExpress寫過接口的童鞋應該都知道,當一個項目變得龐大,實際上會產生很多重複的非邏輯代碼:

router.get('/', ctx => {})
router.get('/page1', ctx => {})
router.get('/page2', ctx => {})
router.get('/page3', ctx => {})
router.get('/pageN', ctx => {})

 

而在每個路由監聽中,又做着大量重複的工作:

router.get('/', ctx => {
  let uid = Number(ctx.cookies.get('uid'))
  let device = ctx.headers['device'] || 'ios'
  let { tel, name } = ctx.query
})

 

幾乎每一個路由的頭部都是在做着獲取參數的工作,而參數很可能來自headerbody甚至是cookiequery

所以,我們對原來koa的使用方法進行了一個較大的改動,並使用routing-controllers大量的應用裝飾器來幫助我們處理大部分的非邏輯代碼。

原有router的定義:

module.exports = function (router) {
  router.get('/', function* (next) {
    let uid = Number(this.cookies.get('uid'))
    let device = this.headers['device']

    this.body = {
      code: 200
    }
  })
}

 

使用了TypeScript與裝飾器的定義:

@Controller
export default class {
  @Get('/')
  async index (
    @CookieParam('uid') uid: number,
    @HeaderParam('device') device: string
  ) {
    return {
      code: 200
    }
  }
}

 

爲了使接口更易於檢索、更清晰,所以我們拋棄了原有的bd-router的功能(依據文件路徑作爲接口路徑、TS中的文件路徑僅用於文件分層)。
直接在controllers下的文件中聲明對應的接口進行監聽。

middleware

如果是全局的中間件,則直接在class上添加@Middleware裝飾器,並設置type: 'after|before'即可。
如果是特定的一些中間件,則創建一個普通的class即可,然後在需要使用的controller對象上指定@UseBefore/@UseAfter(可以寫在class上,也可以寫在method上)。

所有的中間件都需要繼承對應的MiddlewareInterface接口,並需要實現use方法

// middleware/xxx.ts
import {ExpressMiddlewareInterface} from "../../src/driver/express/ExpressMiddlewareInterface"

export class CompressionMiddleware implements KoaMiddlewareInterface {
  use(request: any, response: any, next?: Function): any {
    console.log("hello compression ...")
    next()
  }
}

// controllers/xxx.ts
@UseBefore(CompressionMiddleware)
export default class { }

 

entity

文件只負責定義數據模型,不做任何邏輯操作

同樣的使用了sequelize+裝飾器的方式,entity只是用來建立與數據庫之間通訊的數據模型。

import { Model, Table, Column } from 'sequelize-typescript'

@Table({
  tableName: 'user_info_test'
})
export default class UserInfo extends Model<UserInfo> {
  @Column({
    comment: '自增ID',
    autoIncrement: true,
    primaryKey: true
  })
  uid: number

  @Column({
    comment: '姓名'
  })
  name: string

  @Column({
    comment: '年齡',
    defaultValue: 0
  })
  age: number

  @Column({
    comment: '性別'
  })
  gender: number
}

 

因爲sequelize建立連接也是需要對應的數據庫地址、賬戶、密碼、database等信息、所以推薦將同一個數據庫的所有實體放在一個目錄下,方便sequelize加載對應的模型
同步的推薦在config下創建對應的配置信息,並添加一列用於存放實體的key。
這樣在建立數據庫鏈接,加載數據模型時就可以動態的導入該路徑下的所有實體:

// config.ts
export const config = {
  // ...
  mysql1: {
    // ... config
+   entity: 'entity1' // 添加一列用來標識是什麼實體的key
  },
  mysql2: {
    // ... config
+   entity: 'entity2' // 添加一列用來標識是什麼實體的key
  }
  // ...
}

// utils/mysql.ts
new Sequelize({
  // ...
  modelPath: [path.reolve(__dirname, `../entity/${config.mysql1.entity}`)]
  // ...
})

 

model

model的定位在於根據對應的實體創建抽象化的數據庫對象,因爲使用了sequelize,所以該目錄下的文件會變得非常簡潔。
基本就是初始化sequelize對象,並在加載模型後將其拋出。

export default new Sequelize({
  host: '127.0.0.1',
  database: 'database',
  username: 'user',
  password: 'password',
  dialect: 'mysql', // 或者一些其他的數據庫
  modelPaths: [path.resolve(__dirname, `../entity/${configs.mysql1.entity}`)], // 加載我們的實體
  pool: { // 連接池的一些相關配置
    max: 5,
    min: 0,
    acquire: 30000,
    idle: 10000
  },
  operatorsAliases: false,
  logging: true // true會在控制檯打印每次sequelize操作時對應的SQL命令
})

 

utils

所有的公共函數,都放在這裏。
同時推薦編寫對應的索引文件(index.ts),大致的格式如下:

// utils/get-uid.ts
export default function (): number {
  return 123
}

// utils/number-comma.ts
export default function(): string {
  return '1,234'
}

// utils/index.ts
export {default as getUid} from './get-uid'
export {default as numberComma} from './number-comma'

 

每添加一個新的util,就去index中添加對應的索引,這樣帶來的好處就是可以通過一行來引入所有想引入的utils

import {getUid, numberComma} from './utils'

 

configs

configs下邊存儲的就是各種配置信息了,包括一些第三方接口URL、數據庫配置、日誌路徑。
各種balabala的靜態數據。
如果配置文件多的話,建議拆分爲多個文件,然後按照utils的方式編寫索引文件。

types

這裏存放的是所有的自定義的類型定義,一些開源社區沒有提供的,但是我們用到的第三方插件,需要在這裏進行定義,一般來說常用的都會有,但是一些小衆的包可能確實沒有TS的支持,例如我們有使用的一個node-qconf

// types/node-qconf.d.ts
export function getConf(path: string): string | null
export function getBatchKeys(path: string): string[] | null
export function getBatchConf(path: string): string | null
export function getAllHost(path: string): string[] | null
export function getHost(path: string): string | null

 

類型定義的文件規定後綴爲 .d.ts
types下邊的所有文件可以直接引用,而不用關心相對路徑的問題(其他普通的model則需要寫相對路徑,這是一個很尷尬的問題)。

目前使用TS中的一些問題


當前GitHub倉庫中,有2600+的開啓狀態的issues,篩選bug標籤後,依然有900+的存在。
所以很難保證在使用的過程中不會踩坑,但是一個項目擁有這麼多活躍的issues,也能從側面說明這個項目的受歡迎程度。

目前遇到的唯一一個比較尷尬的問題就是:
引用文件路徑一定要寫全。。

import module from '../../../../f**k-module'

 

小結

初次嘗試TypeScript,深深的喜歡上了這個語言,雖說也會有一些小小的問題,但還是能克服的:)。
使用一門靜態強類型編譯語言,能夠將很多bug都消滅在開發期間。

基於上述描述的一個簡單示例:代碼倉庫

希望大家玩得開心,如有任何TS相關的問題,歡迎來騷擾。NPM loves U.

TypeScript在node項目中的實踐
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章