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项目中的实践
相关文章
相关标签/搜索