打开网易新闻 查看更多图片

作者 | Caleb Meredith,Andrew Wang

译者 | 弯月 责编 | 欧阳姝黎

出品 | CSDN(ID:CSDNnews)

以下为译文:

最近,为了紧跟时代发展的脚步,我们决定引入新技术和模式,并将我们的代码库迁移到TypeScript。

上帝说:要有光

这个故事要从最初我们创建代码库说起:

commit 681429d64232f44c6af1a1d838b91fe39d52edb0Author: HowieDate: Sun Apr 22 15:55:06 2012 -0700beginning state

2012 年,在服务器端使用 JavaScript 的想法仍然相对较新。记得那一年苹果发布了 iPhone 5,鸟叔的一首《江南 Style》红遍全世界,成为互联网上首个播放量超过 10 亿的视频。而我们的创始人则利用首代技术栈开始了 JavaScript以及 JavaScript 生态系统的冒险之旅。

在当初选择的技术中,有些已经相当成熟(比如 Node.js、Express和JavaScript 本身),而有些则仍然很年轻(比如 Backbone、Underscore.js、EJS 和 jQuery)。在过去的九年中,通过不断的重构工作,即使公司已经发展到一百多名工程师,代码量也超过了一百万行,但我们的代码库仍然是统一用相对现代的 JavaScript 语言编写而成的。

核心代码库的展示:扩展记录

下面,我来举个例子。我们的代码库有一个扩展记录的功能,是于 2012 年引入的。此后,核心功能几乎保持不变,但代码的结构已发生了很大变化。如下是过去 9 年中扩展记录功能的前 50 行代码:

我们可以通过这段代码大致了解一下我们的代码库:

  • 2012 年:首次提交到代码库;

  • 2015 年:更多人开始使用该代码库,我们建立了一些编程约定;

  • 2016 年:引入 Browserify,并添加了明确的 CommonJS 导入;

  • 2018 年:从 Backbone 样式类转换为 ES6 样式类;

  • 2019 年:自定义组件框架转换为 React 组件;

  • 2019 年:从 Flow 迁移到 TypeScript,CommonJS 导入替换为 ES6 导入;

  • 2021 年:使用类 React 组件替换了 createReactClass 和 mixins。

通过上述扩展记录的代码发展历史,相信你可以看出,我们愿意大规模地重构我们的代码库。我们认为无法维护的代码是不可避免的。我们认为保证代码质量是全体工程师的责任。比如,上述扩展记录的代码变化就是由不同团队的不同工程师在过去的几年中付出的心血。

接下来,我详细介绍一下,我们将代码库迁移至 TypeScript 的整个过程。

押错了宝

最初我们的代码库采用了原始的 JavaScript。静态类型的好处就不用我再重复了。2016 年,在我们研究 JavaScript 的增量类型时,Flow 和 TypeScript 这两匹黑马脱颖而出,一时之间难分上下。我们之所以选择 Flow,是因为在当时它对 React 的支持更好。

时至 2019 年,我们发现自己押错了宝。TypeScript 的发展速度远远超过了 Flow,而且 TypeScript 在功能、IDE 支持和社区资源等方面也展现出了明显的优势。于是,我们决定将代码库迁移至 TypeScript。

指导原则

如今,我们的代码库包含一百多万行 JavaScript 代码。考虑到规模以及复杂性,我们决定主要围绕以下三个原则开展迁移工作:

  • 不能破坏产品。保留现有代码的语义,以避免引入面向客户的问题。

  • 不能降低类型安全。与 Flow 相比,迁移中的每个更改都必须提高类型安全性。虽然可能仍然会有一些不安全的代码存留下来,但是每个更改都应保证或提高已有的类型安全。

  • 尽量保持简单。迁移需要大量代码改动。每个改动都应该尽量保持简单,而且应该以文件为单位推进。

大规模的迁移

一般情况下,TypeScript 的迁移都是增量逐步完成的,即逐个类型、逐个文件进行。由于我们的大部分代码库已经转换成了 Flow 类型,因此我们采用了另一种方法:一次性将整个代码库迁移至 TypeScript,这是一次大规模的迁移。

第一步,我们编写了一个小工具,完成了代码的纯机械转换。虽然有一些 Flow转换为 TypeScript 的工具,但我们还是编写了自己的工具,为的是满足我们的一些特定需求:

  • 现有的工具不会修改模块语法。我们首先需要将 CommonJS 模块语法(require()和 module.exports)转换为 ES 模块语法(import 和 export)。

  • 一些现有的工具对 Flow 的处理并不正确。例如,他们将 Flow 的类型强制转换表达式(x: T)(这个转换是协变的,covariant)转换成了 TypeScript 的类型强制转换表达式 x as T(这个转换是双变的,bivariant),这种做法是不安全的!而我们的工具则使用了自定义的工具函数,类似于 cast(x),实现为 function cast(x: T): T { return x }。

  • 此外,我们还希望进行一些内部特有的处理。例如,我们经常使用{[key: UserId]: string}等类型,但 TypeScript 并不支持自定义索引访问类型。因此,我们将这些转换成了 Record(而不是{[key: string]: string})。

我们的工具最有技术含量的一个功能是,它处理未声明类型的函数参数的方式。例如,请看以下示例,参数x没有指定类型:

function f(x) {return x * 2;

在这种情况下,Flow 可以根据上下文推断x是一个数字。但是,TypeScript 不会,而且在严格模式下还会报错。

我们的某些代码利用了 Flow 的这一功能。由于我们的指导原则之一是“不能降低类型安全”,因此所有带有 any 参数的函数都需要改。我们的工具通过执行 flow type-at-pos,使用 Flow 推断的类型来标注每个未声明类型的函数参数。事实证明,大多数情况下,Flow 可以推断出 any 的类型。

撸起袖子开干

我们的工具完成了大部分必要的改动,总共修改了 3300 个文件。但是,仍然还有很多无法自动处理的地方。tsc 运行的结果显示,1600 多个文件引发了 15000 多个 TypeScript 错误,我们必须手动修改了。

幸运的是,我们找到了一位类型系统专家,他花了大约一个星期的时间,专门坐下来修复 TypeScript 的错误。整个过程枯燥又乏味,但是总好过使用 // @ts-ignore 搞乱代码。看来,我们树立指导原则还是很有必要的,我们不能让现有 Flow 代码的类型安全性倒退(原则 2:不能降低类型安全),但是为了提高类型安全性而积极地重构实际上也很危险(原则 1:不能破坏产品)。由于首要原则是不能破坏产品,因此有时添加 // @ts-ignore 是最好的解决方案。

所有这些工作都是在单独的分支上完成的。在分支通过类型检查和自动化测试之后,修改就可以反映到主分支上了。

由于手动检查 1600 多个文件(以及 4.8 万行代码)的改动不太现实,因此我们结合使用了很多工具:

  • 将 14 个自动转换,以及 17 个类别的手动转换写成文档,然后要求公司工程师审阅该文档。

  • 每个领域的专家最多会被分配 10 个文件,然后进行代码审核。

  • 对修改前后的代码编译成的 JavaScript 包的差异进行代码审核。我们的编译技术栈没有变化(Webpack 和 Babel),因此编译后的软件包只有一些无足轻重的变动。

2019 年 10 月底,我们冻结了主分支,重新运行了代码自动转换,并合并了 TypeScript 分支。从那以后,我们就正式迈入了 TypeScript。

尘埃落定

由于我们的第三项原则是:尽量保持简单,因此其他的改进想法都被推迟了。我们的一位工程师撰写了一份大约包含 20 种修改思路的文档。令我们十分自豪的是,在过去的两年中,我们的工程师们靠自己的努力实现了一半以上的修改方案。下面,仅举几个例子:

  • 你可以看到在上述代码片段中,我们于 2021 年初将 createReactClass 组件全部转换成了 React ES6 类。这是一项艰巨的任务,因为代码库的很多地方都使用了 createReactClass。我们的开发团队发现自定义类型 createReactClass 严重影响到了 TypeScript 的构建时间,于是,他们非常高效地完成了全部修改。

  • 我们的自动化团队编写了一个辅助方法,可以根据给定的定义,为我们的内部对象定义验证框架生成 TypeScript 类型。在这之前,我们需要同时维护定义和 TypeScript 类型,这可能会导致两者的不一致。

  • 我们的企业团队将所有文件扩展名都转换成了.tsx。作为一个团队,我们认为.ts和.tsx代表 TypeScript 语言的两种形式,所以我们决定只采用一种形式。

  • 我们的一位创始人增强了我们的 MySQL 数据库访问层,可以返回带有类型的查询结果。在 TypeScript 3.9 发布之后,他们还将所有// @ts-ignore注释升级为 @ts-expect-error。

  • 我们的生态系统团队正在努力通过工具来激活--noUncheckedIndexAccess,这是 TypeScript 4.1 中的一项新功能。

总结

从长远来看,低代码/无代码应用程序开发平台是一种趋势,我们相信在未来几十年中我们还将在该领域继续创新。我们非常重视代码的质量,并希望不断发展我们的代码库。

TypeScript 迁移是我们的代码库所经历的一次最大的重构,但肯定不是最后一次。

链接:https://medium.com/airtable-eng/the-continual-evolution-of-airtables-codebase-migrating-a-million-lines-of-code-to-typescript-612c008baf5c

声明:本文由CSDN翻译,转载请注明来源。

60+专家,13个技术领域,CSDN 《IT 人才成长路线图》重磅来袭!

直接扫码或微信搜索「CSDN」公众号,后台回复关键词「路线图」,即可获取完整路线图!

☞“时隔 10 年,重新开始写代码的我要崩溃了!”☞Linux 30 年专访:Linus Torvalds 谈 Linux 内核开发与 Git☞Google 宣布 Kotlin-first 已四年,为什么 Java 开发者仍不买账?