乐趣区

基于-Eggjs-的-Typescript-开发约定

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

从 JS 迁移到 TS

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

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

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

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

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

  • [‘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 之后类型就成了 string

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

基于 Egg.js 开发的约定

参考 该文章

编译开发

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

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

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

编译产物位置

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 镜像再发布

退出移动版