今天看啥  ›  专栏  ›  卡卡🦌

大规模应用TypeScript「2019 JSConf -Brie Bunge」(下)

卡卡🦌  · 掘金  ·  · 2019-08-16 06:42
阅读 31

大规模应用TypeScript「2019 JSConf -Brie Bunge」(下)

特别说明

这是一个由simviso团队进行的关于Airbnb大规模应用TypeScript分享的翻译文档,分享者是Airbnb的高级前端开发Brie Bunge

视频链接:大规模应用TypeScript「2019 JSConf -Brie Bunge」 或点击文章底部阅读原文观看视频

接上篇文章:大规模应用TypeScript「2019 JSConf -Brie Bunge」(上)

视频翻译文字版权归 simviso所有,未经授权,请勿转载!!!

本次参与翻译人员

如何解决问题

这听起来很棒! 但对于我们的初步提案,还有很多问题和疑虑。让我们来对TypeScript进行更深入的了解。

我们的主仓库依赖了一些我们内部的NPM包。为了获得自动完成和类型检查的能力,我们需要先将它们转换成TypeScript,这样做是否值得?

这也是我们目前面临的困境,我们的TypeScript 项目依赖于一个JS NPM包。那我们该如何获取这个包的类型?

这看起来像是需要首先将这个包转换为TypeScript。但这里有个问题,因为维护人员不允许我们对它做TS转换操作,可能他们也不情愿这么做。因为在我们提案的早期,我们并不确定是否要一直按照这个提案走下去。但是从另一个层面来讲,我们使用TypeScript是为了可以让开发人员可以有更好的体验。我们需要TypeScript提供的类型安全性。那么我们该如何解决看似鸡和蛋的问题呢?

TypeScript有一个叫做声明文件的功能。即一个以.d.ts为扩展名的文件,通过它我们可以为JavaScript文件定义类型。

让我们来看一个例子。

如图所示,一起来看我们之前看到的greeter方法。它上面是对应的.d.ts文件,方法里没有实现细节,它只描述了类型。TypeScript将它们组合到一起,这样,在编译是使用这个声明文件,在运行时使用这个原生的JS文件。

那么我们回到我们刚才提的问题(要不要一开始就转换),看看声明文件是如何提供帮助的。

当然,如果那个项目已转换为 TypeScript。我们就没有必要再生成一个.d.ts文件来作为TypeScript构建时的一部分(因为在使用TS编程的时候,要通过它对原生JS进行调用)。但我们认为这不止一种选择。相反,我们可以将声明文件放在我们的 TypeScript 项目中。

另一个选择则是我们可以创建一个单独的NPM包,并将声明所有声明文件放入其中。这很棒,因为现在可以现在跨多个仓库共享声明这些文件。通过这些你可以在使用类似React时,进行相应的类型检查。在安装React的同时你可以安装@types/react包。

在这个@types/react包中针对React的5000个常用包做了类型声明。@types/react与其他5000个其他包都在DefinitelyTyped 这个仓库中,它由社区在维护。在我们的主仓库中,绝大多数的公共依赖都已经由DefinitelyTyped 做到了类型声明。有活跃的社区氛围是TypeScript 的一个主要卖点。我们也回馈了一点力量,相信在这个房间里也有人做出了贡献。谢谢。

这些公共的NPM包的类型声明已经有DefinitelyTyped 在做了,但那些内部的包该怎么办?我们自己安装了一个DefinitelyTyped镜像,在它通过创建一个单独的NPM域(@airbnb-types/*)。这样,你只需要安装@airbnb-types即可。这个仓库的设置与DefinitelyTyped类似,所以我们可以在里面添加并发布这些内部类型。

我们开源了一个starter 工具包,如果你有兴趣的话,可以来参与下。它里面没有类型定义,它只是在教你如何进行一些配置以便于进行测试或者发布自己的类型定义。

那么 TypeScript 究竟能帮忙避免多少 bugs 呢?近期,一个叫做“该不该做类型定义”的研究表明,在选择了TypeScript 的 github 仓库中,有 15% 的 bugs 得到了避免。

在我们内部,有一个记录生产环境事故的流程。这个流程的本意并不是为了责怪谁,而是要从错误中进行学习,这样我们之后就不会再犯类似的错误。所以我坐下来读了六个月的总结报告,阅读这些总结报告很有意思。我最喜欢的就是未捕获的异常以及危险的参数计算。

好吧,也许这些错误的名字并没有那么令人激动。无论如何,我将这些错误归类为与 JavaScript 相关或无关,以此确定哪些错误可以通过使用TypeScript 来避免。

让我们一起看个例子,使用TypeScript会带来哪些帮助。我们对所分享的这个 Input 组件进行修改,通过一些设置来还原bug。用户无法提交表单是因为它不再能通过验证,这是所分享的 Input 组件更改前的简化版本。

它接收一个叫onBlur的变量,并将其直接传递给input元素。所做的改变就是添加一个新的onBlur 事件处理。但这里有一个不明显的bug,你能发现它吗?

就是事件参数不再传递给onBlur prop。这就导致在好几个不同仓库中都出现了这同一个问题。

这里 Input 组件作为Redux Form的一部分进行使用,期望得到一个事件或值,以便验证正常工作。如果没有该事件,表单将不再通过验证,这就意味着提交按钮始终处于禁用状态。

TypeScript在这里是如何帮到我们的?从文档中我们可以看到Redux Form有类型捕获约束。

onBlur 事件属性必须传递一个事件或值。因此,如果我们使用了TypeScript下的Redux Form,那么在函数调用那里就可以看到一个当没有传递事件参数时所产生的错误。

另一类常见的问题就是涉及严格的空值检查。即对使用属性来构造或尝试调用可能为null或undefined的内容进行检查。你可能以前有见过这个错误。

另一种是类型不匹配。当我们尝试使用彼此不匹配的类型时,TypeScript就会提示我们。

所以现在我们对常见的检查出来的问题有了更好的理解,TypeScript可以帮助预防这种bug。

那总体百分率是多少?(那个事故日志所表现的)38%!

我们发现有38%的事故导致了生产阶段的bug。这些对我们用户产生实际影响的bug,可以使用TypeScript来阻止。这对我们来说是个巨大的发现。它有助于将这种(积极)效果转换到现实中。我们复制了一些BUG事件,并向大家展示了TypeScript所给出的Error提示,然后对bug进行修复(也就是我们看到的bug提示灯泡灭掉了)。的确,我们也可以通过写测试代码来捕捉这些,但是通过静态类型检查可以额外增加一层保护层。因此,如果你所在公司有类似的历史,那么你可能就有必要和懂TypeScript小伙伴一起来看一下这些问题在你们的代码里所占的比例。

那么团队是否希望切换到TypeScript呢?我们在几个团队试用了TypeScript,专门针对之前没有使用过TypeScript的团队来获取更多的使用反馈经验。我们帮他们设置好 TypeScript 环境,然后收集他们的反馈。在用了一段时间后,我们向他们发送了一份调查问卷,询问他们是否应继续使用TypeScript。反馈结果是非常肯定的。

我们建议使用这种试用期(的形式,其实就是金丝雀模式)来测试新技术或模式。前端工作组的开发也是基于这个形式来进行的,因为它是独立的,它可以很容易回滚到之前的状态。这也对提案很有帮助,因为我们可以判断团队是否真的喜欢使用TypeScript。

这里可能会有一些关于构建时间上的担忧。我们测量了,发现并没有明显的影响。我们在主仓库启用超过了500条eslint规则,也使用TypeScript eslint解析器。我们很高兴地发现它们中的大多数都可以工作。如果我们在将来要弃用TypeScript的话,我们可以剥离类型,并最终得到大致相同的JavaScript。所以我们逐一记录、思考、跟进,并且针对提出的问题和担忧找到解决办法。

与批评者合作并听取他们的担忧对我们来说非常重要,最后这些批评者中的大部分转而会支持我们,我们的提案也从他们的反馈中变得更为健壮。

在充分解决了这些问题之后,针对所有前端工程师进行了我们是否应该采用TypeScript的调查。我们收到了肯定的回答之后,我们有足够的证据向前推进,并通过这项提案。

逐步采用

在此基础上,我们逐步扩大了采用范围。

此时,我们已经度过了试验阶段,这对于验证 TypeScript 和打好基础是很有用的。我们已经解决了早期的矛盾,并改进了工具和文档,所以之后团队成员会更容易入门。

我们一直有与 TypeScript 团队进行着联系。并帮忙解决一些问题,比如,更好的默认属性优先级处理。在这个阶段,我们自己内部的 TypeScript 社区也得到了发展。但是大部分 Airbnb 的员工还不知道 TypeScript。这也意味着更多的人可以去帮助和回答他们的问题。接下来我们将会进入测试状态,团队可以选择使用它。为了帮助团队,我们创建了内部文档和风格指南,并举办了一些学习课程。我们建立了一个聊天组,一个内部的类Stack Overflow,谷歌Email主题,github组,来供组内成员交流。我们想确保人们能得到他们需要的帮助。最后一步是将 TypeScript 完全普及化。此时就意味着它是稳定状态,每个人都应该开始使用它。

我们目前正在努力地去接近这个目标。剩下的步骤就是巩固风格指南、文档、加强内部培训和迁移更多代码。到目前为止,我们大约有50%的团队使用 TypeScript,在主仓库中,有10%的文件已经被转换成 TypeScript。通过这种渐进的方法,使团队迁移至 TypeScript 的过程更加顺畅。

如果从第一天开始就要求每个人应该使用TypeScript,那么一个接一个的人就会遇到同样的问题。相反,我们先在小范围内使用 TypeScript ,然后总结一些经验技巧。当我们准备把它大规模推广时,这些经验技巧也会用得上。

迁移策略

我们已经探索出了几种将代码迁移至 TypeScript 的方式。我们最初的迁移策略的是混合使用 JavaScript 、TypeScript。

让我们看看,在主仓库中这个策略是如何进行的。这是我在 airbnb.com 上找到的一个简化版本,并且给它们起了一个比较合理的名字。所以这里不存在公司的隐私信息。

让我们放大homes 项目,看看使用混合策略转换它会是什么样子。

我们添加了一个TypeScript配置文件,并将各个文件从js重命名为ts或jsx重命名为tsx。如果TypeScript 报错了,那我们动手去修复他们吧。TypeScript 的一个很棒的特性是,在编译和运行之前,并不需要转换所有代码。这个配置选项(allowJS)允许 javascript 和 TypeScript 文件共存。在这一点上,我们可以看到网站仍能继续运行。我们不需要暂停开发而去迁移整个项目,我们可以挨个迁移文件。我们会重复这个过程,直到整个项目被迁移。

在关于迁移的话题上,我想花些时间和大家分享一些我们认为有用的技巧。第一个是$TSFixMe。

我们通过TypeScript的any类型添加了一个全局类型别名,这意味着它可以为任何类型。我们将它称之为$TSFixedMe,表名我们在代码向TypeScript迁移完成后,再来将类型修正。平时最佳实践是避免使用any,因为它会造成类型安全丢失,但它在迁移过程中会很有帮助。

使用@ts-ignore注解可以做到忽略下一行错误。正确地输入一个文件可能涉及一些深层依赖链解析(类似于复杂对象)。我们可以尝试通过首先转换子文件来避免这种情况,但有时这是不可避免的。因此,$TSFixedMe和@ts-ignore注解能够帮助拆分这些内容,同时则会增加这些检查工作。这些都是暂时的,我们计划添加类型覆盖工具,并在后面我们改进类型时提供帮助。

在JSX中,我们在React组件上使用propTypes 进行运行时类型检查。在将jsx转换为tsx的时候,我们可以删除proptypes直接用TypeScript,也可以在proptypes基础上添加TypeScript。在我们所分享的react项目中,我们想保留传参类型,以便别人使用的时候仍然可以获得运行时检查。为了避免重复两次类型声明,那就需要与这些类型保持同步。我们创建了一个Props类型,通过它将给定的propTypes和defaultProps来派生出一个TypeScript类型。这样,propTypes和defaultProps组合并得到这个最终类型。如果你好奇它是如何工作的,你可以查看我在gist上分享的代码片段。

最近我们已经在使用修订迁移策略All-in TS进行实验。让我们回过来在看看这个Homes项目,然后对它们进行使用all in策略,然后在看它工作怎么样。

我们从js形式的文件开始,我们把所有的文件都改成TS形式的,然后让项目编译。可能我们使用一些比我们想要的更宽松的类型,但其实我们已经开启了TS最严格的检查配置。

然后我们接下来再继续改进类型,移除ts fix语句,比如@ts-ignore(@ts-ignore 注释隐藏 .ts 文件中的错误)。这与js和ts混合策略相比起来有一些优势。通过类型逐步改进比通过文件逐步改进更为简单(两种策略的对比)。如果你正在开发一个新功能,你只需要关注新添加的类型,然后简单的修复这个它即可,而不是先转换整个文件来修复所有错误,然后再添加你所需要类型。

不用重命名文件也意味着更方便查看。有时候,如果一个文件在一次提交被重命名,然后在别的提交中修改。他们会在code review中单独出现,程序员必须要合在一起看才能知道变化了什么。后一种策略还能清楚地知道缺少哪些类型。

TypeScript类型推导能力十分强大,我们可以在编写代码的时候大量使用它。为了通过编译,有些文件需要进行一些TS Fixed。TS就可以推断出剩余部分。

还有就是开发者们可能有一个固定的思维模式,他们并不会根据文件扩展名来切换思维,于是就出现了比如为什么我不能在这里添加类型?为什么我不能在那里得到编译错误的疑问(.js和.ts混用)?那些类型在所有文件中都可以添加、使用、检查。

这听起来很不错,但是我们该如何迁移我们整个代码呢?对于大规模代码修改而言,Codemod是一种十分强大的工具。拿最简单的形式来说,就好比是我们在我们的项目中所使用的全局搜索和替换。你也许在你之前的IDE里面干过这件事(全局查找和替换),这些Codemod库可以通过正则来替换,但它们很不稳定,可能会因细微的代码风格变化而终止。

或者我们可以使用之前某人已经讲过的抽象语法树。so,这就是这段代码用AST(抽象语法树)来表达的形式。如图所示,左侧的代码都一 一对应着右侧抽象语法树上的节点。所以为了好玩,我们想写一个Codemod来反转代码中的所有标识符。我们将我们的代码作为输入参数,根据这个创建出AST(抽象语法树),修改AST树然后产生新的代码。这里的关键是我们以编程方式进行此更改。如果你手上需要修改的文件数并不多的话,我们可以一个个的去修改。但如果一旦文件数量达到数千个以上,这种手动去修改的想法可能会令人感到十分心累。

因此我们Airbnb采用了Facebook的Jscodeshift来进行这种大量的代码重构。这个转换库可以捕获我们刚刚对该ast进行的修改并且反转标识符。我们找到与标识符对应的所有节点,用名字反转,用新节点去替换这些节点,然后得到新的代码。Missy Elliott(歌手)也将会我们感到自豪,所以我们反转了它。

我们拿到了代码并且重新改装,找到了成员的标识符然后翻转它。yeah!

astexplorer.net这个网站无法帮你掌握好说唱技巧,但可以帮助你查看你的Codemods。在这个网页下,它有一个可以通过源代码输出对应的AST树的功能,以及在你对代码的改变同时反映到AST树上。

我也在DefinitelyTyped 这个库提交了关于Jscodeshift的PR,这样的话可以来降低大家在使用TypeScript与Codemod的交互门槛。

在将JavaScript代码迁移到TypeScript时,有这几种模式。对于react组件,我们一次次的将静态类属性移动到class body里面。创建一个PropsType表示react生命周期方法。我们将它们编码为Codemod,以便我们可以在更多代码上重复运行它们。我们通过使用一个叫作TS Migrate的工具来将它们进行打包。这个工具的功能是当如一个JS项目,然后得到一个编译好的TS项目。随着时间的推移,你仍然需要慢慢找到类型,但它为你提供了一个工作前提。我们将此工具应用于我们的内部分享的React组件库,现在在我们的网站上已经频繁地使用。我们有内部的类型定义库(DefinitelyTyped),但是因为react分享组件库的快速发展,所以做到与时俱进地更新太难了。所以,我们想直接从源码类型出发,这也是我们迁移TS的第一个目标。我们已经将超过3万行以上的代码都进行了TypeScript化,你们可能认为我们整个团队花了四周的时间才能完成这个。事实上,我们用了一套我们自己的Codemod工具,仅需数分钟就完成了。

我们使用来自proptypes的类型信息,同时使用$TSFixMe,并基于此来继续进行优化。但即便如此,我们也生成了有意义的TypeScript声明文件,这样我们可以在其他仓库中进行使用。在这个例子中,我们可以看到需要合并的代码行数多的有点可怕。通过使用TypeScript编译器以及在可视化回归测试的帮助下,我们将在CI上运行测试。通过这些测试我可以很自信的说,我的这些改变不会对原来的系统产生任何不利的影响。当然我们还能确保我们的站点仍旧在正常工作,并不需要回滚代码。不可思议!

我们现在已经将TS Migrate运用在其它的一些地方,同时也在不断优化和迭代它。我们计划在以后会将它运用于更多的代码上(JS代码)。我们打算之后将它开源,这样你们也能将它运用在你们的自己的代码迁移上。

我想给你一些我们可以从TypeScript迁移中得出关键点,并且是可以广泛应用的。在大型组织中实施变革可能是一项挑战,但强有力的事实依据和相关问题和担忧的解决,可以使我们信服。采用逐步变化的方式有助于减少摩擦并证明其价值。一条明确的迁移路线能帮助团队更好的转向新的模式,同时好的工具也能促进这个过渡的过程。

我之所以开始这个工作,是因为之前有个产品组对我的工具感到失望。当我得知公司内部其他人也有这种改变的想法的时候,我便与他们合作并将之进行下去。与其怨天尤人接受现状,只有通过行动才能发生积极的改变。所以我鼓励你去追求那些可以让你对组织充满激情的事情,让你和你周围的人的生活变得更好。

感谢大家的倾听,同时感谢AirBnb为这个项目作出贡献的每一个人,尤其是台下的Joe和Mohsen。还有对其他一些优秀的Airbnb工程师表示感谢。我手上也有些TypeScript主题的小便签和一些钥匙链,先到先得,只限前30人。

感谢大家的倾听




原文地址:访问原文地址
快照地址: 访问文章快照