基于 Egg.js 的 Typescript 开发约定

有一种说法是 9102 年作 Node 后台开发还不上 Typescript 不是太懒惰就是太菜了,其实假如你使用 Egg.js, 他们已经对基于 ts 开发作了许多支持,在项目里引入 ts 并无那么困难,且对减小开发过程当中的低级错误很是有帮助。
在这里记录一下我的在 Egg.js 项目里引入 ts 的一些约定,其实都在官方文档、文章和 Github issue 里了。javascript

从 JS 迁移到 TS

web 开发不是瀑布流,定了需求闭门开发几个月上线,每每是基于线上运行的 js 项目,要改形成基于 ts,前提是不能中断服务,初步设想有如下三种方案:html

  1. 逐步替换文件,.js 和 .ts 后缀文件并存,经过 .d.ts 文件来标明类型获取提示;
  2. 分红 js base 和 ts base 两个项目,逐步替换接口,新接口完成并经过测试后,流量切换到新项目;
  3. 直接作项目全量文件的 ts 化,利用 any 类型先完成迁移,后期再逐步以【用到了就重构】的原则重构全部代码。

第一种方式在定义 .d.ts 文件时很是繁琐且会有一些坑,如很难推断函数返回类型,且在使用一些 es 原生方法时覆盖类型会有坑;前端

第二种方式须要实现一种流量切换的方式,如加入 haproxy 中间层,经过 header 或是 path 等方式来识别新老接口流量。碰到的主要问题是在新老项目一个服务每每要改动多个公共文件,很难保持同步,会有矛盾发生;java

第三种方式是最后采用的方式,从 js 代码中切出一个新分支,写了脚本批量替换文件后缀,花了大约 2-3 天完成迁移,期间在旧 js 代码中添加的新功能,须要确认在新 js 代码中也实现一遍。
过程当中也是发现了一些坑,举例:node

  • ['dev', 'stage'].includes(item), 若 item 有可能为 undefined,则代码会报错,这里就不像 js 那么灵活
  • parseInt 只接受 string 参数,而以前有时为了确保获得的必定是数字,会往这个函数里传入 number 类型参数,ts 对这种状况会报错。值得注意的是,因为 any 类型可赋值给全部类型,因此若是 ts 推断传入参数是 any 类型,parseInt(any) 是不会报错的。这也是为何有人推荐在 tsconfig.json 中打开 noImplicitAny 选项的缘由。保证 parseInt(item) !== NaN 这种逻辑,仍是应该手动实现,不能彻底依赖于 ts 的判断;
  • Object.entries, array.map 等 es 内置方法有本身的类型定义,有可能会致使你传入的参数类型丢失(降级),如 Object.entries 函数原型定义里,`entries<T>(o: { [s: string]: T } | ArrayLike<T>): [string, T][];

entries(o: {}): [string, any][];` 假如传入的参数类型是某种字符串字面量,这里变到 s 以后类型就成了 stringgit

  • 在改造过程当中不免处于简化需求把 array 类型设为 any[], 这时再 map,里面的参数也会跟着变成 any 类型,想从里面的参数中取值赋值,有时就会发生报错。从快速迁移的目的上来说,能够灵活使用 any + // 来临时忽略报错。

基于 Egg.js 开发的约定

参考 该文章github

编译开发

在本地开发时,使用 egg-bin dev 命令启动应用,egg-bin 自带 ts-node,能够直接运行 ts 代码,无需编译过程;web

发布到线上时,egg 官方建议是仍是使用 Node + js 原生代码,使用 egg-script start 启动,因此须要将 ts 代码编译成 js 的过程。docker

在后端开发时能够主动升级 Node 版本,使用一些先进的 es 语法新特性,不像前端为了编译成浏览器广泛能兼容的 es5 代码会使用不少 polyfill 的技巧,因此 ts 代码编译成 js 后其实并无太难懂(tsconfig.json 里 compilerOptions => target -> es2017)。尽管如此使用编译后的 js 代码仍是须要映射回 ts 代码,以便线上报错时能有比较清晰的提示,ts 采用的方案是 sourceMap (tsconfig.json => compilerOptions =>inlineSourceMap -> true)。
关于 sourceMap 的原理能够参考阮一峰的这篇文章typescript

编译产物位置

egg-bin 在加载文件时,若是识别到同目录下的 .ts 文件和 .js 文件,会优先加载 .js 文件。

真实环境下,为了不把 .ts .js 文件同时托管在 git 上,形成混乱,最好是引入 ci 层完成编译的工做,这时就可使用单独的 dist 目录来托管编译后的 js 代码,无需上述 egg-ts-helper 操做,设置 tsconfig.json => compilerOptions => outDirs -> ./dist 便可。

内置对象类型推断

egg 扩展了 koa 的application、context 对象,挂载了 controller、service、model 等对象在上面,然而你本身定义的 controller 文件 ts 没法感知他们和 egg 的关系,因此 egg 提供了 egg-ts-helper 工具来完成来自动生成 .d.ts 文件,放在 typings 目录。参考文章

因为 model 目录并非 egg 默认约定会使用的目录,只是大多数开发者的一个通识,因此针对 model 目录 egg-ts-helper 并不会主动生成 .d.ts 文件,你须要主动提供 tshelper.js 文件,利用 egg-ts-helper 提供的 api 来 watch model 目录。

启用 egg-ts-helper 的方式很简单,老的方式是 egg-bin dev -r egg-ts-helper/register 新的方式是设置 package.json => egg => declarations -> true

tsconfig.json
  • resolveJsonModule => ts 默认没法加载 json 模块,须要开启此选项
  • paths => 该选项是为了简化在代码中引用一些模块时,须要注意相对路径的问题,配合 baseUrl 选项便可比较方便的使用一些模块,详情参考文档。实际使用有一些注意事项,参考 egg 官方的提示

发布流程

线上使用 ci 负责编译工做,ci 服务器须要下载所有依赖(包括 dependencies 和 devDependencies),获得编译产物后,再发布到 staging 服务器,staging 服务器只运行 npm install --production, 打包成 docker 镜像再发布

相关文章
相关标签/搜索