四年多以前,一家名为 Heap 的创业公司开始使用TypeScript取代CoffeeScript。尽管公司内部的工程师普遍都很喜欢 TypeScript,但迁移的速度还是很慢,当时也没有一条明确的途径可以实现 100%的 TypeScript 代码库。本文是 Heap 公司在迁移过程总结出的经验教训,希望对广大开发者有所帮助。
实际上,如果说我们的目标是完全切换到 TypeScript 上,彼时我们却走在了错误的道路上。我们当时正在添加 TypeScript 代码,但我们添加 CoffeeScript 代码的速度却更快。TypeScript 和 CoffeeScript 的目标都是同一个 Node.js 运行时,我们希望这个运行时可以简化过渡工作,虽然大家都在期待转型,但我们却并没有积累那么多动力,我们也没有向着彻底抛弃 CoffeeScript 的未来前进。
在 2019 年初,我们又一次开始了从 CoffeeScript 切换为 TypeScript 的努力。这一次,我们决定让迁移工作恢复生机,这需要我们重新考虑转换现有代码的策略。在这样的理念引导下,我们有了一套新的指导原则。我们依照这些原则,将一个看似棘手的难题变成了一个易于管理、易于理解的过程,并设法显著改变下面这些曲线的形状:
那么我们是怎么做到的呢?本文就是我们的经验之谈。
人和技术同等重要
我们重新开始这项工作时最重要的体会就是,成功的迁移必须以人为本,要关注的不只是技术。作为工程师,我们很容易被技术潮流以及(尤其是)迁移的细节所吸引:我们都觉得迁移到 TypeScript 可以增强对代码的信心,并且都非常乐意花费大量时间来精心调整 TypeScript 配置。
事实证明,更重要的因素在于等式中人员的这一侧:我们如何让同事认同这种新范式?这个问题还引发了其他许多疑问。例如:在这个新模型中,我们如何让开发人员干劲十足?为了突出新范式的优势,有哪些障碍或摩擦是可以消除的?
沿着这些问题的思路,我们开发了一种新的迁移流程,并对其感到非常自豪。
开发体验必须明显改善
我们很快意识到,要让整个团队都参与进来,必须让开发人员体会到,他们在编写 TypeScript 时会更有效率。如果团队只是将迁移看作是从一种语法换成另一种差不多的语法,他们永远都不会产生认同感。如果迁移之后他们的日常工作效率并没有提升,那么就算工程师们往往更喜欢编写类型化代码,也敌不过旧习惯的巨大惯性。
我们首先找出代码库中有哪些区域在转换后将带来生产力的大幅提升,因为我们知道,有计划地转换文件比随机转换更具说服力。例如,我们的数据访问层(“ORM”)无处不在,并且大多数文件都会以某种方式来使用它。在数据访问层中引入类型会产生效率层面的乘数效应。今后转换的几乎所有文件都将从类型完善的数据库模型和实用工具中受益。
我们还将工具链和配置作为优先事项来对待。大多数开发人员使用的编辑器就是那么几种而已,因此我们创建了可以直接使用的编辑器配置,添加了调试配置,从而可以轻松设置断点和单步执行代码。
最后,我们整理了一套取得共识的 linting 规则,这些规则使我们能够在整个组织中以统一的样式编写代码,并让开发人员对迁移行动更加满意。
当团队开始看到这些转换工作的成果时,整个项目也就得到了认可,前进动力也会更足了。当我们的工程师开始将类型化数据访问视为必不可少的工具后,他们就能更好地意识到,代码库的其它部分也会平稳地转换完毕。
打破阻碍转换的技术障碍
当我们开始分析 TypeScript 的推广模式时,有一件事情很明显:对我们的工程师来说,使用 TypeScript 并不是一个无缝的过程,他们经常需要导入特殊的实用工具(ts-node/register)或创建 CoffeeScript 中间层文件,仅仅是为了导入这些 CoffeeScript 代码的 TypeScript 等效内容。简而言之,确实存在一个关于互操作的故事,但是它需要大量的样板,并且需要经历太多的尝试和错误才能走上正轨。
这些摩擦点是阻碍迁移的主要因素吗?很难说,但是我们知道,阻力最小的途径是一种强大的力量,如果我们想让人们支持转换,就需要使 TypeScript 成为简单而明显的选择。
为此我们优先做了一些工作,允许开发人员在任何服务或组件中编写 TypeScript。无论是后端、前端、脚本还是 devops 任务,我们都希望工程师能够使用 TypeScript 来编写代码,并使它正常工作。我们最后使用了 NODE_OPTIONS 环境变量与-r ts-node/register,以便现有的工作流(使用 coffee 命令运行 CoffeeScript 文件)能继续运作。
确保转换过程简单、安全和自动化
语言迁移可能会带来风险:CoffeeScript 和 ES6/TypeScript 之间一些看似等效的语法可能根本就不是一回事。开发人员可能会认为转换是重构(或更糟的是重写)的好机会,但这会让迁移工作本来就有的风险雪上加霜。
为了减轻这种风险,我们需要一个规范的流程来转换文件,其不会引入回归,也不会诱导工程师去做多余的事情。这个流程还要能快速执行。
我们确定了一个分为两部分的流程:首先自动转换 CoffeeScript 文件,然后立即手动添加基本类型注解和与 linter 相关的更改。关键在于抵制(不管是什么方式)重构代码的诱惑。这样一来,转换工作就成为了简单、遵循安全规则的机械活动,不会影响运行时行为。
对于初始转换而言,我们使用了将.coffee 文件转换为.ts 文件的脚本。在幕后,我们使用 decaffeinate(https://decaffeinate-project.org/)来从 CoffeeScript 转换为 ES6 JavaScript。由于所有 ES6 JavaScript 在语法上都是有效的 TypeScript,因此我们有了一个正常工作的文件。(我们发现 decaffeinate 是一种非常成熟且可靠的工具。)我们用 Git 历史中的一次独立提交来表示这一步骤。
不过这项工作尚未完成。我们在严格模式下使用 TypeScript,因此“隐式 any”等功能都已关闭。我们利用这个转换窗口为无法进行类型推断的事物创建了类型注解。我们避免在此阶段使用 any,而选择了更严格的 unknown。此阶段的目标是实施不会导致运行时行为变化的更改。我们没有做出任何形式的重构,而只是做了最小限度的工作,来让代码进入准备编译、lint 和通过测试的状态。
如果模块的依赖项已经转换为 TypeScript,则几乎不需要做任何工作:大多数类型是通过导入的模块来处理的。这样最后会产生滚雪球效应,转换的模块越多,转换工作就会变得越来越容易和安全。
第二个步骤也是一次独立提交;这大大简化了代码审核过程,因为在 decaffeinate 的步骤完成之后,审阅者可以轻松地看到所做的更改。
整个流程都记录在一份 TypeScript 转换指南中。Heap 的所有开发人员都可以在最短 5 分钟内,转换一个文件并打开拉取请求将其合并。
给团队成员提问和讨论的去处
应对这种迁移意味着要让你的同事们放弃一种舒适而有效的工作方式。这里的理念是,新方法将带来更多的生产力。但是要达到这一目标需要付出时间和精力。我们最不想看到的是一种失败的转换模式:在这种模式下,开发人员最后会陷入破损的工具、混乱的错误消息以及难以理解的编译器错误的烂摊子中。因此,我们的下一个优先事项是想办法引导整个团队提升专业水平。
为此,我们在 Slack 中创建了一个 #typescript 频道,让遇到困难的开发人员能够获得帮助。推动迁移工作的开发人员可以随时在频道里回答问题,监测常见的问题和障碍。如果同样的问题不断重现,他们就会知道哪些地方应该着重改进。
开发人员需要知道,他们所遇到的任何语言和工具问题都将得到及时解答。我们决定让“TypeScript 冠军”在完成他们自己的工作之前优先处理同事的问题。虽然这减慢了他们的工作进展,但同时也消除了迁移中面临的许多潜在的重大障碍。
追踪进度
从一开始,我们就知道不可能在一夜之间完成迁移任务,并且可能需要一年或更长时间才能完成此过程。这也是很好的:我们认为进步比完美更重要。
我们发现,长时间跟踪我们的工作进展是很有意义的。我们可以观察一段时间内的稳定进展,进而了解我们是否仍在进步。我们使用 Grafana 来可视化代码行数。下面是另一个可视化图像,显示了随时间推移的文件计数:
领导要尊重工程师
不幸的是,在这类项目中取得成功最重要的因素之一是领导是否愿意为你提供执行这些任务的空间。在我们的案例中,迁移项目是自下而上的工作:一开始,我向团队负责人和工程经理提出计划,一旦获得批准,我们就可以自由地找出实现此目标的最佳方法。虽然我们正在讨论的转换将涉及成千上万行代码,但迁移是 100%内部驱动的。
一般来说,这类项目的最佳拥护者往往是那些每天都在使用有问题工具的团队。在 Heap,我们很幸运地遇上了认同这种理念的领导层,他们也认同领导力在对工程师赋权并最终摆脱困境时是非常有意义的。在继续迁移的过程中,我们希望继续学习,并利用这些知识来简化下一个大型项目。
原文链接:https://heap.io/blog/engineering/migrating-to-typescript
评论