首頁  >  文章  >  web前端  >  關於TypeScript在node專案中的實作分析

關於TypeScript在node專案中的實作分析

不言
不言原創
2018-07-23 11:32:451609瀏覽

這篇文章要跟大家分享的內容是關於TypeScript在node專案中的實作分析,有一定的參考價值,有需要的朋友可以參考一下。

TypeScript可以被理解為是JavaScript的一個超集,這意味著涵蓋了所有JavaScript的功能,並且在上面有著自己獨特的語法。 
最近的一個新專案開始了TS的踩坑之旅,現分享​​一些可以藉鏡的套路給大家。

為什麼選擇TS

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

而靜態強類型編譯能夠帶來許多的好處,其中最重要的一點就是可以幫助開發人員杜絕一些馬虎大意的問題:
關於TypeScript在node專案中的實作分析

圖為rollbar統計的數千個項目中數量最多的前十個異常  

也不難看出,因為類型不符、變數為空導致的異常比你敢承認的次數要多。
譬如 
關於TypeScript在node專案中的實作分析而這一點在TS中得到了很好的改善,任何一個變數的引用,都需要指定自己的類型,而你下邊在程式碼中可以用什麼,支援什麼方法,都需要在上邊定義:  
關於TypeScript在node專案中的實作分析這個提示會在開發、編譯期來提示給開發者,避免了上線以後發現有問題,再去修改。

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

關於TypeScript在node專案中的實作分析

而在TS中,對於一個函數,首先你需要定義所有參數的型別,以及傳回值的型別。
這樣在函數被呼叫時,我們就可以很清楚的看到這個函數的效果:
關於TypeScript在node專案中的實作分析

#這是最基礎的、能夠讓程式更穩定的兩個特性,當然,還有更多的功能在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

  5. ,全域安裝nodemon,在tsc編譯後自動刷新伺服器程式
  • #官方手冊
  • 官方Express範例

    #以及專案中使用的一些核心依賴:
  1. #reflect-metadata
  2. : 大量裝飾器的套件都會依賴的一個基礎包,用於注入資料
  3. routing-controllers
  4. : 使用裝飾器的方式來進行koa-router的開發
  5. sequelize
  6. : 抽象的資料庫操作
  7. #sequelize-typescript
  8. : 上述外掛程式的裝飾版本,定義實體時使用

專案結構

首先,放出目前專案的結構:<pre class="brush:php;toolbar:false">. ├── 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</pre>src為主要開發目錄,所有的TS程式碼都在這裡邊,經過編譯過後,會產生一個與src同級的dist資料夾,這個資料夾是
node引擎實際運行的程式碼。 

src###下,主要程式碼分為如下結構(依據自己專案的實際情況進行增刪):###

|folder|desc

##是用來處理介面請求,原文2#存放了各種中介軟體、全域or 自訂的中間件3各種設定項目的位置,包括連接埠、4這裡存放的是所有的實體定義(使用了sequelize進行資料庫操作)。 5#使用來自6#在儲存的各種日常開發中提煉出來的公共函數7存放了各種客製化的複合型別的定義,各種結構、屬性、方法傳回值的定義(目前包括常用的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
}</userinfo>

因为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中的一些问题

關於TypeScript在node專案中的實作分析当前GitHub仓库中,有2600+的开启状态的issues,筛选bug标签后,依然有900+的存在。  
所以很难保证在使用的过程中不会踩坑,但是一个项目拥有这么多活跃的issues,也能从侧面说明这个项目的受欢迎程度。

目前遇到的唯一一个比较尴尬的问题就是:
引用文件路径一定要写全。。

import module from '../../../../f**k-module'
#
1 #controllersappsroutes資料夾。
middleware
config log路徑、各種巴拉的常量定義。
entity
models entity中的實體進行sequelize來完成初始化的操作,並將sequelize物件拋出。
utils
types

以上是關於TypeScript在node專案中的實作分析的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn