TypeScript在node項目中的實踐

TypeScript在node項目中的實踐

TypeScript能夠理解爲是JavaScript的一個超集,也就是說涵蓋了全部JavaScript的功能,並在之上有着本身獨特的語法。
最近的一個新項目開始了TS的踩坑之旅,現分享一些能夠借鑑的套路給你們。html

爲何選擇TS

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

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

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

而這一點在TS中獲得了很好的改善,任何一個變量的引用,都須要指定本身的類型,而你下邊在代碼中能夠用什麼,支持什麼方法,都須要在上邊進行定義:

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

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

而在TS中,對於一個函數,首先你須要定義全部參數的類型,以及返回值的類型。
這樣在函數被調用時,咱們就能夠很清晰的看到這個函數的效果:
github

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

TypeScript在node中的應用

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

環境依賴

在使用TS以前,須要先準備這些東西:typescript

  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下,主要代碼分爲了以下結構(依據本身項目的實際狀況進行增刪):

# folder desc
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項目中的實踐
相關文章
相關標籤/搜索