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

Tim van der Lippe
Tim van der Lippe

本文属于一系列博文的一部分,介绍了我们对 DevTools 架构所做的更改以及其构建方式。

迁移到 JavaScript 模块迁移到 Web 组件之后,我们今天继续推出有关我们对开发者工具架构所做的更改及其构建方式的博文系列。 (如果您尚未观看,我们发布了一个视频,介绍了我们如何将开发者工具的架构升级到新式 Web,其中包含 14 条有关如何改进 Web 项目的技巧。)

在本文中,我们将介绍从 Closure Compiler 类型检查器改用 TypeScript 的 13 个月历程。

简介

鉴于 DevTools 代码库的大小,以及需要向负责维护该代码库的工程师提供信心,使用类型检查器势在必行。为此,DevTools 早在 2013 年就采用了 Closure Compiler。采用 Closure 后,DevTools 工程师可以放心地进行更改;Closure 编译器会执行类型检查,以确保所有系统集成均采用正确的类型。

不过,随着时间的推移,替代类型检查器在现代 Web 开发中越来越受欢迎。两个值得注意的例子是 TypeScriptFlow。此外,TypeScript 还成为了 Google 的官方编程语言。虽然这些新的类型检查器越来越受欢迎,但我们也注意到,我们发布的代码存在应该由类型检查器捕获的回归问题。 因此,我们决定重新评估所选类型检查器,并确定在 DevTools 上进行开发的后续步骤。

评估类型检查器

由于 DevTools 已经在使用类型检查器,因此我们需要回答的问题是:

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

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

我们的评估重点是已发布的回归问题,以及确定其根本原因。这里的假设是,由于我们已经在使用 Closure Compiler,Closure 不会发现这些问题。因此,我们必须确定是否有任何其他类型检查器能够做到这一点。

TypeScript 中的类型正确性

由于 TypeScript 是 Google 官方支持的编程语言,并且普及率正在快速提高,因此我们决定先评估 TypeScript。TypeScript 是一个有趣的选择,因为 TypeScript 团队本身就使用开发者工具作为其测试项目之一,以跟踪其与启用 JavaScript 类型检查的兼容性。他们的基准参考测试输出表明,TypeScript 捕获了大量类型问题,而 Closure 编译器未必能检测到这些问题。其中许多问题可能是导致我们发布的代码出现回归问题的根本原因;这反过来又让我们相信 TypeScript 可能是开发者工具的可行选择。

在迁移到 JavaScript 模块的过程中,我们发现 Closure Compiler 发现的问题比以前多。改用标准模块格式后,Closure 能够更好地理解我们的代码库,因此类型检查器的效率也得到了提升。 不过,TypeScript 团队使用的 DevTools 基准版本早于 JavaScript 模块迁移。因此,我们必须确定向 JavaScript 模块迁移是否也减少了 TypeScript 编译器会捕获的错误数量。

评估 TypeScript

开发者工具已经存在十多年了,在此期间,它已发展成为一个功能丰富且体量庞大的 Web 应用。在撰写这篇博文时,开发者工具包含大约 15 万行第一方 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 Compiler 使用方式或迁移到 TypeScript 之间做出选择。(由于 Google 和 Chromium 都不支持 Flow,因此我们不得不放弃该选项。) 在与负责 JavaScript/TypeScript 工具的 Google 工程师讨论并听取他们的建议后,我们选择了 TypeScript 编译器。 (我们最近还发布了一篇关于将 Puppeteer 迁移到 TypeScript 的博文。)

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

选择 TypeScript 编译器意味着我们必须在 DevTools 代码库及其内部架构上进行大量投资。因此,我们估计至少需要一年的时间才能迁移到 TypeScript(目标时间为 2020 年第 3 季度)。

执行迁移

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

我们评估了多种方案:

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

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

不过,我们坚信,通过采用 TypeScript,我们可以防范未来出现的问题,尤其是回归问题。因此,我们提出的论点不是“我们将创造新的业务价值”,而是“我们将确保不丢失已获得的业务价值”。

TypeScript 编译器的 JavaScript 支持

在获得团队支持并制定在同一 JavaScript 代码上同时运行 Closure 和 TypeScript 编译器的计划后,我们开始从一些小文件着手。我们的方法主要是自下而上:从核心代码开始,向上沿着架构移动,直到到达高级面板。

我们通过向 DevTools 中的每个文件预先添加 @ts-nocheck,成功实现了工作并行处理。“修复 TypeScript”的过程是移除 @ts-nocheck 注解并解决 TypeScript 会发现的任何错误。这意味着,我们有信心地说,每个文件都已检查过,并且已解决尽可能多的类型问题。

一般来说,这种方法很有效,并且很少出现问题。我们在 TypeScript 编译器中遇到了几个 bug,但其中大多数 bug 都很难发现:

  1. 返回 any 的函数类型的可选参数被视为必需参数:#38551
  2. 将属性分配给类的静态方法会破坏声明:#38553
  3. 声明包含 no-arg 构造函数的子类和包含 args 构造函数的父类时,会省略子构造函数:#41397

这些 bug 表明,在 99% 的情况下,TypeScript 编译器都是一个可靠的基础。是的,这些不易发现的 bug 有时会导致 DevTools 出现问题,但大多数情况下,它们不易发现,因此我们可以轻松规避。

唯一令人困惑的问题是 .tsbuildinfo 文件的非确定性输出:#37156。在 Chromium 中,我们要求同一 Chromium 提交的任何两个 build 都应产生完全相同的输出。很遗憾,我们的 Chromium build 工程师发现 .tsbuildinfo 输出是非确定性的:crbug.com/1054494。为了解决此问题,我们不得不对 .tsbuildinfo 文件(本质上包含 JSON)进行猴子补丁处理,并对其进行后处理以返回确定性输出: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 编译器支持的工程师数量。 之所以能够实现这一点,是因为我们将迁移设计为可并行执行,这样多个工程师处理多个不同文件时就不会相互冲突。

此时,TypeScript 化流程已成为全团队的工作。在获得额外帮助后,我们于 2020 年 11 月底完成了迁移,比开始迁移后的 13 个月后完成,比我们最初预计的时间提前了一年多。

共有 18 位工程师提交了 771 个更改列表(类似于拉取请求)。我们的跟踪 bug (https://crbug.com/1011811) 有超过 1200 条评论(几乎全部是来自更改列表的自动发布内容)。 我们的跟踪表格包含超过 500 行,其中列出了所有要转换为 TypeScript 的文件、其分配者以及“TypeScript 化”的更改列表。

减轻 TypeScript 编译器性能的影响

我们目前遇到的最大问题是 TypeScript 编译器的运行速度缓慢。鉴于构建 Chromium 和 DevTools 的工程师数量,这个瓶颈会带来巨大开销。很遗憾,我们在迁移之前无法发现这一风险,直到我们将大多数文件迁移到 TypeScript 后,才发现 Chromium build 的构建时间明显增加:https://crbug.com/1139220

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

很遗憾,我们目前提供的解决方案并不总适合非 Google 贡献者。由于对 Chromium 的开源贡献非常重要(尤其是来自 Microsoft Edge 团队的贡献),我们正在积极寻找适合所有贡献者的替代方案。不过,目前我们还没有找到合适的替代方案。

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

目前,我们已从代码库中移除 Closure 编译器类型检查器,并完全依赖于 TypeScript 编译器。我们能够编写 TypeScript 编写的文件,并使用 TypeScript 专用功能(例如接口、泛型等),这对我们日常工作有很大帮助。我们越来越有信心,TypeScript 编译器能够捕获类型错误和回归问题,这正是我们在首次开始进行此迁移时所希望的。与许多其他迁移一样,此次迁移缓慢、细致且有时很有挑战性,但我们认为,迁移带来的好处证明了迁移是值得的。

下载预览渠道

不妨考虑将 Chrome Canary 版开发者版Beta 版用作默认开发浏览器。通过这些预览渠道,您可以使用最新的 DevTools 功能、测试尖端的 Web 平台 API,并在用户发现问题之前发现您网站上的问题!

与 Chrome 开发者工具团队联系

您可以使用以下选项讨论与 DevTools 相关的新功能、更新或任何其他内容。