刷新开发者工具架构:将开发者工具迁移到 TypeScript

Tim van der Lippe
Tim van der Lippe

本博文是系列博文的一部分,其中介绍了我们正在对开发者工具的架构所做的更改及其构建方式。

迁移到 JavaScript 模块迁移到 Web 组件之后,我们今天继续发布这篇博文系列,了解 Devtools 架构的变更及其构建方式。 (如果您还没有看过此内容,不妨观看我们发布的一个视频,了解将开发者工具的架构升级为现代网络的工作内容,其中包含有关如何改进 Web 项目的 14 条提示。)

在这篇博文中,我们将介绍从 Closure 编译器类型检查工具迁移到 TypeScript 的 13 个月历程。

简介

鉴于开发者工具代码库的大小,并且需要为处理它的工程师提供信心,有必要使用类型检查工具。 为此,DevTools 早在 2013 年就采用了 Closure 编译器。采用 Closure 后,开发者工具工程师可以放心地做出更改;Closure 编译器将会执行类型检查,以确保所有系统集成都属于类型正确的集成。

不过,随着时间的推移,替代类型检查工具在现代 Web 开发中逐渐流行。TypeScriptFlow 是两个重要的示例。此外,TypeScript 也成为 Google 的官方编程语言。虽然这些新的类型检查工具越来越受欢迎,但我们也注意到,我们发布的回归本应被类型检查工具捕获。因此,我们决定重新评估对类型检查工具的选择,并确定在开发者工具上进行开发的后续步骤。

评估类型检查工具

由于开发者工具已经在使用类型检查工具,因此我们要回答的问题是:

我们是继续使用 Closure 编译器,还是迁移到新的类型检查工具?

为了回答这个问题,我们必须根据多个特征评估类型检查工具。由于我们使用类型检查工具时侧重于工程师信心,因此对我们来说最重要的方面是类型正确性。换句话说:类型检查工具在发现实际问题方面的可靠性如何?

我们的评估重点关注我们发布的回归问题,并确定这些问题的根本原因是什么。 这里的假设是,由于我们已经在使用 Closure 编译器,Closure 不会捕获这些问题。因此,我们必须确定是否有任何其他类型检查工具能够做到这一点。

TypeScript 类型正确性

由于 TypeScript 是 Google 官方支持的编程语言,并且越来越受欢迎,因此我们决定先评估 TypeScript。 TypeScript 是一个很有趣的选择,因为 TypeScript 团队本身就使用开发者工具作为他们的测试项目之一来跟踪其与 JavaScript 类型检查的兼容性。 他们的基准引用测试输出表明,TypeScript 捕获了大量的类型问题,而 Closure 编译器未必能检测到这些问题。其中许多问题可能是我们推出回归问题的根本原因;这反过来又使我们相信 TypeScript 是开发者工具的一个可行方案。

在向 JavaScript 模块迁移的过程中,我们发现 Closure Compiler 发现了比以往更多的问题。迁移到标准模块格式提高了 Closure 理解代码库的能力,并因此提高了类型检查工具的效率。不过,TypeScript 团队使用的是开发者工具的基准版本,而此版本在 JavaScript 模块迁移之前已经推出。 因此,我们必须弄清楚迁移到 JavaScript 模块是否还减少了 TypeScript 编译器能捕获的错误数。

评估 TypeScript

开发者工具已存在十多年,如今已发展成为一款规模相当且功能丰富的 Web 应用。在撰写本博文时,开发者工具包含大约 150,000 行第一方 JavaScript 代码。 当我们在源代码中运行 TypeScript 编译器时,大量错误让人感到无所适从。我们能够发现,虽然 TypeScript 编译器发出的与代码解析相关的错误越来越少(大约 2,000 个错误),但我们的代码库中仍有 6,000 个与类型兼容性相关的错误。

这表明,虽然 TypeScript 能够了解如何解析类型,但它在我们的代码库中发现了大量类型不兼容性。通过对这些错误的人工分析结果,我们发现 TypeScript(大多数情况下)是正确的。 TypeScript 能够检测到这些信号,而 Closure 却没有,这是因为 Closure 编译器通常会将类型推断为 Any,而 TypeScript 会根据赋值推断类型并推断更准确的类型。因此,TypeScript 确实更能理解对象的结构,并发现了有问题的用法

一个重要的发现是,在开发者工具中使用 Closure 编译器时,频繁使用 @unrestricted。使用 @unrestricted 为某个类添加注解可有效关闭 Closure 编译器对该特定类的严格属性检查,这意味着开发者可以随意增强类定义,而无需考虑类型安全。 我们无法找到任何与为什么在开发者工具代码库中广泛使用 @unrestricted 有关的历史上下文,但它导致大部分代码库以一种安全性较低的操作模式运行 Closure 编译器。

在对我们的回归问题与 TypeScript 发现的类型错误进行交叉分析后,我们也发现存在重叠,这使我们相信 TypeScript 本可以防止这些问题(前提是类型本身是正确的)。

正在拨打 any

此时,我们不得不做出决定:是提高 Closure 编译器的使用还是迁移到 TypeScript。(由于 Google 和 Chromium 均不支持 Flow,因此我们不得不放弃此选项。) 根据与从事 JavaScript/TypeScript 工具的 Google 工程师的讨论和他们的建议,我们选择选用 TypeScript 编译器。 (我们最近还发布了一篇介绍如何将 Puppeteer 迁移到 TypeScript 的博文。)

TypeScript 编译器的主要原因在于类型正确性得到提升,而其他优势还包括获得 Google 内部 TypeScript 团队的支持,以及 TypeScript 语言的功能,例如 interfaces(而不是 JSDoc 中的 typedefs)。

选择 TypeScript 编译器意味着我们对开发者工具代码库及其内部架构进行了大量投资。因此,我们预计迁移到 TypeScript 至少需要一年时间(预计于 2020 年第 3 季度实现)。

执行迁移

还有一个最大的问题:我们如何迁移到 TypeScript? 我们有 15 万行代码,无法一次性迁移这些代码。我们还知道,在代码库上运行 TypeScript 会发现数以千计的错误。

我们评估了多个选项:

  1. 获取所有 TypeScript 错误并将其与“黄金”输出进行比较。 这种方法与 TypeScript 团队采用的方法类似。这种方法的最大缺点是出现合并冲突,因为有数十名工程师在同一个代码库中工作。
  2. 将所有有问题的类型设置为 any这本质上会使 TypeScript 抑制错误。我们没有选择此选项,因为我们的迁移目标是类型正确,抑制操作会破坏类型。
  3. 手动修正所有 TypeScript 错误。这将需要修正数千个错误,非常耗时。

尽管预计需要付出大量努力,我们还是选择了方案 3。 我们之所以选择这种方法,还有其他一些原因:例如,它可以让我们审核所有代码,并对所有功能(包括其实现)进行十年一次的审核。 从业务角度来看,我们不是提供新价值,而是保持现状。这导致难以证明选项 3 的正确性。

不过,通过采用 TypeScript,我们坚信可以防止未来发生问题,尤其是回归问题。因此,人们的论点较少是“我们将增加新的业务价值”,而是更多地提到“我们致力于确保不会失去获得的商业价值”。

TypeScript 编译器的 JavaScript 支持

在获得各方认可并制定计划在同一 JavaScript 代码上运行 Closure 和 TypeScript 编译器后,我们开始着手制作一些小文件。 我们大多采用自下而上的方法:从核心代码开始,沿着架构向上移动,直到完成高级面板为止。

我们可以通过预先向开发者工具中的每个文件添加 @ts-nocheck 来并行处理相关工作。“修复 TypeScript”的流程将移除 @ts-nocheck 注解并解决 TypeScript 发现的任何错误。这意味着我们确信,每个文件都已检查完毕,并且尽可能多的类型问题已得到解决。

一般来说,此方法几乎没有问题。我们在 TypeScript 编译器中遇到了多个 bug,但大多数 bug 都鲜为人知:

  1. 系统会将函数类型会返回 any 的可选参数视为必需参数:#38551
  2. 为某个类的静态方法分配的属性会破坏声明:#38553
  3. 对于具有 no-args 构造函数的子类和具有 args 构造函数的父类,在声明时会忽略子构造函数:#41397

这些 bug 表明,在 99% 的情况下,TypeScript 编译器是构建坚实的基础。 是的,这些隐蔽的 bug 有时会导致开发者工具出现问题,但大多数情况下,它们非常模糊,我们可以轻松解决这些问题。

导致一些混淆的唯一问题是 .tsbuildinfo 文件的不确定性输出:#37156。 在 Chromium 中,我们要求同一 Chromium 提交内容的任意两个 build 会产生完全相同的输出。遗憾的是,我们的 Chromium 构建工程师发现 .tsbuildinfo 输出具有不确定性:crbug.com/1054494。为了解决此问题,我们必须对 .tsbuildinfo 文件(基本包含 JSON)进行 monkey 修补,然后对其进行后处理,以返回确定性输出:https://crrev.com/c/2091448。幸运的是,TypeScript 团队解决了上游问题,我们很快就移除了我们的权宜解决方法。感谢 TypeScript 团队积极接收 bug 报告并及时解决这些问题!

总体而言,我们对 TypeScript 编译器的(类型)正确性感到满意。我们希望作为一个大型开源 JavaScript 项目的 Devtools 帮助巩固了 TypeScript 对 JavaScript 的支持。

分析成效

在解决这些类型错误以及慢慢增加 TypeScript 检查代码量方面,我们取得了不错的进展。 然而,在 2020 年 8 月(此次迁移 9 个月后),我们进行了核实,发现按照目前的进度,我们无法按时完成截止日期。我们的一位工程师构建了一个分析图,以显示“TypeScriptification”(我们为此迁移指定的名称)的进度。

TypeScript 迁移进度

TypeScript 迁移进度 - 跟踪需要迁移的剩余代码行

预计在 2021 年 7 月至 2021 年 12 月期间,项目余额可以达到零,大约一年后。 与管理层和其他工程师讨论后,我们同意增加致力于迁移到 TypeScript 编译器支持的工程师人数。 之所以能够这么做,是因为我们将迁移设计为可并行执行,使得处理多个不同文件的多个工程师不会相互冲突。

这时,TypeScriptification 过程变成了整个团队共同努力的结果。有了额外的帮助,我们才在 2020 年 11 月底、开始实施后的 13 个月以及最初预计的一年多前完成了迁移。

18 名工程师总共提交了 771 份更改列表(类似于拉取请求)。 我们的跟踪错误 (https://crbug.com/1011811) 收到了超过 1200 条评论(几乎所有评论都是由变更列表自动生成的帖子)。 我们的跟踪表格有超过 500 行,其中列出了所有要类型化处理的文件、相应的分配对象,以及“类型脚本化”的变更列表。

减少 TypeScript 编译器性能影响

我们目前遇到的最大问题是 TypeScript 编译器性能不佳。鉴于构建 Chromium 和开发者工具的工程师人数,此瓶颈的成本很高。遗憾的是,我们在迁移之前未能发现这种风险,并且直到我们将大多数文件迁移到 TypeScript 时,我们发现各个 Chromium 版本所花费的时间明显增加:https://crbug.com/1139220

我们已向 Microsoft TypeScript 编译器团队向上游报告了此问题,但遗憾的是,他们判定这是有意为之。 我们希望他们会重新考虑此问题,但与此同时,我们正在努力尽可能减少对 Chromium 端性能缓慢的影响。

很抱歉,我们目前可用的解决方案并不一定适合非 Google 员工。由于对 Chromium 的开源贡献非常重要(尤其是 Microsoft Edge 团队的贡献),因此我们正在积极寻找适合所有贡献者的替代方案。但目前我们尚未找到合适的替代解决方案。

开发者工具中 TypeScript 的当前状态

目前,我们已从代码库中移除 Closure 编译器类型检查工具,仅依赖于 TypeScript 编译器。我们能够编写由 TypeScript 编写的文件,并利用 TypeScript 特有的功能(例如接口、泛型等),这对我们每天都很有帮助。我们对于 TypeScript 编译器能捕获类型错误和回归更有信心,而我们希望在首次开始进行此次迁移时能够达到这种效果。与许多迁移一样,这种迁移速度缓慢、细节问题,而且通常具有挑战性,但随着我们不断收获到好处,我们认为它是值得的。

下载预览渠道

您可以考虑将 Chrome Canary 版Dev 版Beta 版用作默认开发浏览器。通过这些预览渠道,您可以使用最新的开发者工具功能,测试先进的网络平台 API,并在用户采取行动之前发现网站上的问题!

与 Chrome 开发者工具团队联系

使用以下选项讨论博文中的新功能和变化,或讨论与开发者工具有关的任何其他内容。

  • 通过 crbug.com 提交建议或反馈。
  • 使用开发者工具中的更多选项   了解详情   > Help > Report a DevTools issues来报告开发者工具问题。
  • 发推文:@ChromeDevTools
  • 请在 YouTube 视频或“开发者工具提示”YouTube 视频中留言说明“开发者工具的新变化”。