从JavaScript迁移到TypeScript,类型声明文件自动生成与中心化管理的实践

百家 作者:InfoQ 2021-09-06 14:48:29
作者 | 许京爽、许侃
编辑 | 蔡芳芳
为了解决从 JavaScript 逐步迁移到 TypeScript 过程中遇到的痛点,FreeWheel 核心业务团队评估并提出了一套由 Protobuf 文件自动化生成 TypeScript 类型声明文件的流程,支持 Protobuf 文件的变化触发类型声明文件的自动更新。所有的 TypeScript 类型声明文件以微服务为单位储存,集中维护在公司级别的 TypeScript 中心化仓库里。  
1 背景
FreeWheel 核心业务团队前后端开发现状

FreeWheel 核心业务系统采用微服务架构,并使用 Go 语言作为微服务的开发语言,基于 gRPC 进行服务的远程调用。微服务之间的数据接口采用 Protobuf 进行定义,使用 protoc 自动生成相应的 RPC 接口代码。

微服务需要对外提供 Restful 接口用于 Web 前端和 Open API,而基于 protoc 生成的服务一般用于集群内部通信。为了兼容 HTTP 调用,FreeWheel 使用 grpc-gateway 进行 Restful 接口的转化和代理转发。

目前 Web 前端基于 React 组件化开发,以 JavaScript 为官方语言。JavaScript 是一种弱类型语言,在运行时才明确变量的类型,由当前的值决定当前的类型。在前后端交互需要了解变量类型时,前端开发人员只能通过查看 Protobuf 文件的定义来得到当前变量的类型,开发体验不好且影响开发效率。

中心化 TypeScript 类型库的需求

基于该现状,FreeWheel 核心业务前端开发团队正在逐步将前端开发语言从 JavaScript 向 TypeScript 切换。这么做的原因主要在于,TypeScript 作为 JavaScript 的类型化超集,弥补了静态、弱类型的 JavaScript 的缺陷,具有静态类型声明,可以减少不必要的类型判断和人工查看类型的成本,开发过程中进行静态类型检查和类型提示,对提高开发效率有正向作用。

但在这个切换过程中,大量基础类型声明的构建成了一道必须跨越的鸿沟,主要体现在以下两点:

  • 缺少内部公共库的类型定义。目前线上一些比较老旧的 JavaScript 库,不太可能用 TypeScript 改写,对这部分文件如果能够提供一份公用的类型定义会更合适。

  • 缺失基于后端 Protobuf 定义对应的前端类型声明文件。目前整个微服务代码仓库已累积超过 700 个Protobuf接口定义文件、15k+ 个message定义。单纯靠开发人员手写实现转换并不现实。而且Protobuf接口仍在不断增加和修改,相应的类型声明文件也需要及时得到更新。

因此维护一个基于公司微服务层面的 TypeScript 类型中心化仓库的需求便呼之欲出。这个仓库既支持内部公共库的类型声明,还支持所有微服务的类型声明文件。通过发包共享给整个公司的同事使用,降低重复开发成本。

这一灵感来源于 TypeScript 社区最为热门的开源项目 DefinitelyTyped,它提供了很多 npm 上常用的包的类型声明文件,同时对于一些没有提供声明文件的包,也支持独立开发人员自行实现后上传到 DefinitelyTyped 里共享给大家使用,极大地促进了TypeScript的推广。但DefinitelyTyped 中并不包含 Protobuf 文件对应前端类型声明文件的解决方案。为了早日在团队内部完成 TypeScript 的使用推广,亟需解决这一痛点。

2 自动化 TypeScript 类型库生成方案的技术选型与设计

DefinitelyTyped 珠玉在前,我们参考其思路并结合 FreeWheel 开发现状,设计并实现了一套自动维护中心化类型库 @fw-types 的方案。

  • 一方面支持自动化地由  Protobuf 文件生成  TypeScript类型声明文件。当Protobuf 文件发生更改后触发生成 TypeScript类型文件的自动化流水线,将更新后的文件自动上传到@fw-types库里,然后触发 npm 发包流水线将新的类型包上传到内部的 Artifactory 仓库中,从而保证能够追踪由 Protobuf文件的更改而引起的类型声明文件的变化。

  • 另一方面支持前端开发人员可以给较老的前端库补充类型定义,提交 Pull Request 合并到中心化库里,共享给大家使用。

技术选型  

目前 GitHub 上由Protobuf文件生成 TypeScript 文件的工具有很多,我们分别调研并试用了这些工具,对比情况如下表所示。

  • 由于我们期望使用interface语法定义的类型,要求可以保留原始字段的蛇形命名,同时能够生成Protobuf 定义依赖的其他文件类型,最终选择proto-loader作为开发流程中的生成工具。对于变量名的转化,有三个工具是将Protobuf文件里的蛇形命名转化为驼峰命名。

  • AsObject 指的是有一类工具转化TypeScript包的语法中,以命名空间 namespace 的形式为主,对于空间本身定义成一个 AsObject 对象,命名空间可以有效的阻隔重名问题,但是每个类型在调用的过程中就需要添加  .AsObject 来使用。另一类转化以接口interface的形式转化,目前以interface形式的较少。

  • d.ts文件是集中管理的类型声明文件,但实际我们关心的是类型声明文件的内容,内容符合预期的话,.ts文件和d.ts文件对项目来说没有本质区别。

  • 对于import的文件,只有两个工具可以生成其对应的.ts文件。

  • 在社区活跃度上,这些工具均比较活跃,最近一个月内都有相关commit。

架构设计

整体解决方案的架构图如下图,从 @fw-types 代码仓库的入口来看可以划分为两个部分,一个是由于Protobuf文件的变化引发的自动由Protobuf文件生成TypeScript文件并上传到@fw-types库,另一个是和DefinitelyTyped一样,支持开发人员在本地实现类型声明文件并上传到共享库中,提供给大家使用。

整个流水线按照功能来说可以划分为三个阶段,分别是:

  • 捕获接口定义文件改动

  • 接口定义文件生成类型声明文件

  • 类型声明文件发包

这三个阶段的工作将会在下一章节中详细介绍。

3 持续集成流水线的实践详解
捕获接口定义文件改动

由Protobuf转向TypeScript化的关键点在于维护好每个版本Protobuf文件定义和类型声明文件的一一对应关系。因此从Protobuf 文件的生成开始,就需要持续集成流水线的介入。

捕获接口定义文件改动是整个流水线的第一阶段,如下图所示。后端开发人员提交Protobuf 文件改动,当对应微服务的持续集成测试通过之后,会被合并到主分支。我们在微服务代码仓库的合并事件里增加了钩子(webhook)。每当合并事件触发,该钩子会检测发生变化的文件里是否包含Protobuf文件,如果包含则触发下一阶段的任务。

接口定义文件生成类型声明文件

这一阶段的核心工作是由Protobuf文件生成TypeScript类型声明文件,将有变化的类型声明文件自动上传到@fw-types 里。考虑到 git 可以很直观地给出被改动文件的细节,因此这部分的重点只需要关注类型声明文件的生成和提交。

类型声明文件的生成

在技术选型时,我们对比了目前比较热门的一些开源项目,最终选择proto-loader作为开发流程中的生成工具。但工具本身只提供了初步的转化能力,我们还有一些额外的工作:

  • 工具最终生成的是以.ts后缀的文件,包含了我们所需要的变量类型声明。但在我们的使用场景中还需要对外暴露index.d.ts文件以方便前端开发人员使用,因此需要将.ts文件统一在index.d.ts文件中向外export。

  • 对于生成的.ts文件,我们还设计了相应的开头注释,体现当前文件是由工具自动生成的,并且显式地列出当前.ts 文件的Protobuf来源,方便溯源。

//DON'T EDIT. THIS IS GENERATED CODE!//package: 微服务名//source: / 主仓库 / 微服务 /proto/Protobuf 文件/***Generated by @fw-types.*Designed by @jsxu*@freewheel**/
提交生成文件到中心化仓库

在提交文件改动之前,我们需要先对@fw-types库的整体目录结构有所了解:

  • 以微服务为单位,每个微服务维护一个目录,包含当前服务所有的.d.ts文件,以及统一向外暴露的index.d.ts文件。

  • 除此以外每个微服务目录下还有一个package.json文件,这个文件是在接口定义文件生成类型步骤使用npm init生成得到的,该文件包含了当前服务的版本、依赖、名称等内容,提供给后续类型文件发包步骤使用。

  • commonTypes为一些基础的类型声明文件,例如 Protocol Buffers 本身定义的一些基础 Protobuf 文件和内部定义的一些公共 Protobuf 文件。鉴于这些 proto 依赖几乎每个微服务都会用到,我们对此做了特殊处理,单独发包管理。

@fw-types|__serviceA|----index.d.ts|----type1.d.ts|----type2.d.ts|____package.json|__serviceB|__serviceC|__commonTypes

当类型声明文件生成之后,通过 git status命令可以获取到被改动文件列表,这里存在两种情况:

a. 对于新的微服务服务,对应的类型包还没发布,因此不存在 package.json 文件,我们通过 npm init 生成,并配置上相应的参数。

b. 对于已有的微服务,则需要对 package.json 文件中的 version 字段进行更新,详细内容将在后续包版本管理中介绍。

当全部改动都准备就绪,便可以调用 git commit 命令向远端仓库提交改动。我们对于 commit message 进行了特殊设计,将对应的 commit branch 也包含在其中,从而方便通过commit message里对类型声明文件和对应的Protobuf文件更改进行回溯。

类型声明文件发包

Freewheel 目前采用 Artifactory 进行制品内容(Artifacts)的管理与存储。Artifactory 是 JFrog 的一个产品,不但可以管理二进制包文件,还可以对市面上几乎所有语言的包依赖进行管理。这一阶段的类型声明文件的发包操作也有赖于 Artifactory 对 npm 包的支持。具体流程如下所示:

  • 当@fw-types仓库的 webhook 检测到 push 事件时,会触发向 Artifactory 发包的任务,包以微服务为单位进行管理。

  • 去每个服务下进行版本比较,拉取远端当前服务的最新版本与现在库里的版本比对,当不匹配时,说明当前代码仓库下的版本有所更新,需要调用 npm publish发新包。

这里需要注意,第一次和第 N 次发包是有区别的。第一次发包的时候 Artifactory 上并没有该服务的包,如果读取版本会直接报错中断流程,因此这里需要对是否是第一次发包进行判断,结合第二阶段生成类型声明文件的任务,对第一次发包的版本进行特殊处理。

最终在 Artifactory 上以微服务为单位的目录结构如下:

————————————————Artifactory——————————————————@fw-types  |————service A      |———— -          |————@fw-types              |—————— service A-0.0.0.tgz              |—————— service A-0.0.1.tgz                |—————— service A-0.1.0.tgz               |—————— service A-0.2.0.tgz  |————service B  |————service C  |————service D
使用情况

目前这套流程已经支持了 20+ 微服务,平均每天约有 5 个由于Protobuf文件变化自动触发的任务。平均每个 protobuf 改动合并之后能够在 30 分钟内从 Artifactory 下载到对应的包文件。

在 Web 前端的项目中也已经有 3 个项目开始逐步接入这些类型包,大大改善了团队前端工程师的开发体验。

下图为使用生成的 TypeScript 文件替换原先手写的类型。

4 落地应用的问题与解决方案
最终代码提取

我们从一开始生成.ts文件到最终可用的.ts文件提取流程如下图所示,包含工具生成和二次转化两部分。其中二次转化包含了冗余代码去除、命名变化和引用路径变化,下面逐个进行介绍。

冗余代码去除

proto-loader的设计会生成很多文件:

  • 对于每个message生成一个.ts文件

  • 对于 rpc 接口生成相应的 .Service.ts文件

  • 用于运行时 protobuf 类型获取的 ProtoGrpcType 文件

对于 FreeWheel 的业务场景,我们只关心与message相关的.ts文件 。此外,对于每一个message所生成的interface还会有一个额外的__Output类型,这个类型对于我们来说也是无用的。因此需要对这些冗余的代码进行删减,并根据情况对import里对引入进行调整。

命名变化

proto-loader以message名作为.ts文件名,有可能会出现文件名重名问题。例如当一个微服务下的两个protobuf文件里包含一个仅大小写存在差异的message,此时生成的.ts文件仅大小写存在差异,存储在同一路径下。一些不区分大小写的文件系统里会最终只保留其中一个文件。因此需要对于生成的文件名进行重复检测和重新命名,使用其所在的Protobuf文件名来区分。

生成文件import路径的变化

使用proto-loader生成的类型声明文件里,存在对其他类型声明文件的引用。直接生成的结果里import的路径采用原先各个服务Protobuf文件的路径关系,存放在proto子路径下,例如下图所示import ../proto/。

————————————————proto——————————————————micro_services_repo  |————service A      |—————— proto          |—————— a.proto          |—————— b.proto           |—————— c.proto  |————service B  |————service C  |————service D

而我们维护的@fw-types路径如下,没有 proto子目录,因此import的 .ts 文件路径如果和原先proto的路径一致的话,会无法正确读取,需要对其生成的文件import的路径进行更改,以我们@fw-types的文件管理形式读取,import ./。

————————————————typescript——————————————————@fw-types  |————service A      |—————— a.ts      |—————— b.ts       |—————— c.ts      |—————— index.d.ts  |————service B  |————service C  |————service D

除此之外,由于部分文件命名的变化,也需要更改对应文件的import的路径,才能最终实现正确类型的引入。

包版本管理

对于每一个微服务服务的类型声明文件包,其版本在每次d.ts文件存在更新后,都需要进行版本号的更新,并将更新后的版本信息一起作为 commit message 传到@fw-types里,我们采用语义化版本(SemVer)规范。其命名规则是以 x.y.z 的形式:

  • X 表示主版本号,当 API 兼容性变化时,X 递增

  • Y 表示次版本号,当存在不影响兼容性的功能增加时,Y 递增

  • Z 表示修订号,当存在不影响兼容性的 Bug 修复时,Z 递增

目前 FreeWheel 主要使用 proto3 版本,基于默认前向兼容的情况下,我们暂时只对 x 和 y 位进行更新。因为对于Protobuf很难界定 bug 修复的行为,所以只存在兼容性变化和新特征的添加,具体结合Protobuf的改动来确定最终得到的版本,x 位表示无法兼容的变更,y 位表示新字段新功能的增加。

前端库的类型支持

本解决方案旨在维护一个公司级别的TypeScript类型中心化仓库,除了对于Protobuf文件生成TypeScript类型声明文件以外, 还期望覆盖一些前端库的类型声明。因此,我们也支持前端开发人员在 @fw-types仓库里以 Pull Request 的形式提交对目前公司内部使用的JavaScript库手写的类型声明文件,共享给全公司的同事使用,期望在公司层面维护一个活跃的TypeScript 生态。

5 未来计划与展望

基于前文其实可以看出,虽然proto-loader 解决了大多数问题,但也引入了不少额外工作。我们计划基于proto-loader定制一版专门应对 FreeWheel 需求的生成工具,降低二次转化部分代码的维护成本。这一部分工作已经在进行之中。

此外,目前生成的代码尚未被 lint 和格式化,为了保证统一的生成文件样式,我们还需要加入对 lint 和格式化的支持。

最后,@fw-types 仓库的推广使用还需要提供更加精简的接入步骤,继续增加对更多微服务和前端库的支持,使 JavaScript 往 TypeScript 的迁移更为简单和顺利。

作者介绍:

许京爽,Freewheel 软件开发实习生,就读于北京航空航天大学计算机学院。

许侃,实习指导老师,FreeWheel 高级研发工程师,研究方向为云原生、数据可视化等领域,热衷于新技术的探索与分享。

今日好文推荐
20 年起义,敏捷已死,敏捷万岁
阿里将投1000亿助力共同富裕;字节跳动员工取消大小周后收入普降17%;《数据安全法》正式实施 | Q资讯
估值高达380亿美元!大数据独角兽Databricks官宣16亿美元新融资
Azure 曝史上“最严重”漏洞?微软也难以回应云安全质疑


 活动推荐

编程语言热门榜评选来了!编程语言作为技术人的入圈工具,与每个技术人的职业生涯息息相关~

近期,InfoQ 特别推出了【InfoQ 编程语言排行榜】,精心挑选了当前最流行、最受欢迎的编程语言,同时设置了八个有代表性的问题。扫描图中二维码,为你看好的编程语言投上一票!

点个在看少个 bug 

关注公众号:拾黑(shiheibook)了解更多

[广告]赞助链接:

四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

公众号 关注网络尖刀微信公众号
随时掌握互联网精彩
赞助链接