在开发者工具中对 CSS 基础架构进行现代化改造

开发者工具架构刷新:对 DevTools 中的 CSS 基础架构进行现代化改造

本文属于一系列博文的一部分,介绍了我们对 DevTools 架构所做的更改以及其构建方式。我们将介绍 CSS 在开发者工具中的运作方式,以及我们如何对开发者工具中的 CSS 进行现代化改造,为最终迁移到在 JavaScript 文件中加载 CSS 的 Web 标准解决方案做好准备。

开发者工具中的 CSS 先前状态

DevTools 以两种不同的方式实现了 CSS:一种适用于 DevTools 的旧版部分中使用的 CSS 文件,另一种适用于 DevTools 中使用的现代 Web 组件

DevTools 中的 CSS 实现是在很多年前定义的,现在已经过时。开发者工具一直使用 module.json 模式,而且我们投入了大量精力来移除这些文件。移除这些文件的最后一道障碍是 resources 部分,该部分用于加载 CSS 文件。

我们希望花些时间探索各种潜在解决方案,这些方案最终可能会演变为 CSS 模块脚本。目的是消除旧系统造成的技术债务,同时简化向 CSS 模块脚本的迁移过程。

由于使用 module.json 文件(正在移除)加载,因此 DevTools 中的所有 CSS 文件都被视为“旧版”。所有 CSS 文件都必须在与 CSS 文件位于同一目录的 module.json 文件中列在 resources 下。

剩余 module.json 文件示例:

{
  "resources": [
    "serviceWorkersView.css",
    "serviceWorkerUpdateCycleView.css"
  ]
}

然后,这些 CSS 文件会填充一个名为 Root.Runtime.cachedResources 的全局对象映射,以作为从路径到其内容的映射。如需将样式添加到 DevTools,您需要使用要加载的文件的确切路径调用 registerRequiredCSS

registerRequiredCSS 调用示例:

constructor() {
  
  this.registerRequiredCSS('ui/legacy/components/quick_open/filteredListWidget.css');
  
}

这会检索 CSS 文件的内容,并使用 appendStyle 函数将其作为 <style> 元素插入页面中:

使用内嵌样式元素添加 CSS 的 appendStyle 函数

const content = Root.Runtime.cachedResources.get(cssFile) || '';

if (!content) {
  console.error(cssFile + ' not preloaded. Check module.json');
}

const styleElement = document.createElement('style');
styleElement.textContent = content;
node.appendChild(styleElement);

在引入现代 Web 组件(使用自定义元素)时,我们最初决定在组件文件中通过内嵌 <style> 标记使用 CSS。这带来了一些挑战:

  • 缺少语法突出显示支持。为内嵌 CSS 提供语法突出显示的插件通常不如为在 .css 文件中编写的 CSS 提供的语法突出显示和自动补全功能。
  • 构建性能开销。内嵌 CSS 还意味着需要进行两次 lint 检查:一次针对 CSS 文件,一次针对内嵌 CSS。如果所有 CSS 都编写在独立的 CSS 文件中,我们就可以消除这一性能开销。
  • 缩减大小方面的挑战。内嵌 CSS 无法轻松缩减,因此没有任何 CSS 被缩减。由于同一 Web 组件的多个实例引入了重复的 CSS,因此 DevTools 的发布 build 的文件大小也增加了。

我的实习项目的目标是为 CSS 基础架构找到一个解决方案,使其既适用于旧版基础架构,也适用于 DevTools 中使用的新 Web 组件。

研究潜在解决方案

此问题可分为两部分:

  • 了解构建系统如何处理 CSS 文件。
  • 了解 DevTools 如何导入和使用 CSS 文件。

我们针对各个部分探讨了不同的潜在解决方案,下面列出了相应的解决方案。

导入 CSS 文件

在 TypeScript 文件中导入和使用 CSS 的目标是尽可能遵循 Web 标准,在整个 DevTools 中强制执行一致性并避免 HTML 中出现重复的 CSS。我们还希望能够选择一个解决方案,让我们能够将我们所做的更改迁移到新的网络平台标准,例如 CSS 模块脚本。

因此,@import 语句和 标记似乎并不适合开发者工具。它们与 DevTools 中其余部分的导入内容不一致,会导致未样式化内容闪烁 (FOUC)。迁移到 CSS 模块脚本会更难,因为必须明确添加导入内容,并且处理方式与 <link> 标记不同。

const output = LitHtml.html`
<style> @import "css/styles.css"; </style>
<button> Hello world </button>`
const output = LitHtml.html`
<link rel="stylesheet" href="styles.css">
<button> Hello World </button>`

使用 @import<link> 的潜在解决方案。

我们改为寻找一种将 CSS 文件作为 CSSStyleSheet 对象导入的方法,以便使用其 adoptedStyleSheets 属性将其添加到 Shadow DOM(DevTools 已经使用 Shadow DOM 几年了)。

捆绑程序选项

我们需要一种方法来将 CSS 文件转换为 CSSStyleSheet 对象,以便在 TypeScript 文件中轻松操控它。我们认为 Rollupwebpack 都是为我们进行这一转型的潜在捆绑器。DevTools 已在其正式版 build 中使用 Rollup,但如果将这两种捆绑器添加到正式版 build,在使用我们当前的 build 系统时可能会出现性能问题。我们与 Chromium 的 GN 构建系统集成增加了捆绑工作的难度,因此打包器与当前的 Chromium 构建系统无法很好地集成。

我们改为探索使用当前的 GN 构建系统执行此转换的选项。

在 DevTools 中使用 CSS 的新基础架构

新解决方案涉及使用 adoptedStyleSheets 向特定 Shadow DOM 添加样式,同时使用 GN 构建系统生成可供 documentShadowRoot 采用的 CSSStyleSheet 对象。

// CustomButton.ts

// Import the CSS style sheet contents from a JS file generated from CSS
import customButtonStyles from './customButton.css.js';
import otherStyles from './otherStyles.css.js';

export class CustomButton extends HTMLElement{
  
  connectedCallback(): void {
    // Add the styles to the shadow root scope
    this.shadow.adoptedStyleSheets = [customButtonStyles, otherStyles];
  }
}

使用 adoptedStyleSheets 有诸多好处,包括:

  • 它正在成为现代网络标准
  • 防止 CSS 重复
  • 仅将样式应用于 Shadow DOM,这样可以避免因 CSS 文件中重复的类名称或 ID 选择器而导致的任何问题
  • 轻松迁移到未来的 Web 标准,例如 CSS 模块脚本和导入断言

对该解决方案的唯一注意事项是,import 语句要求导入 .css.js 文件。为了让 GN 在构建过程中生成 CSS 文件,我们编写了 generate_css_js_files.js 脚本。构建系统现在会处理每个 CSS 文件,并将其转换为默认导出 CSSStyleSheet 对象的 JavaScript 文件。这样一来,我们就可以轻松导入 CSS 文件并加以采用了。此外,我们现在还可以轻松缩减生产 build,从而减小文件大小:

const styles = new CSSStyleSheet();
styles.replaceSync(
  // In production, we also minify our CSS styles
  /`${isDebug ? output : cleanCSS.minify(output).styles}
  /*# sourceURL=${fileName} */`/
);

export default styles;

通过脚本生成的 iconButton.css.js 示例。

使用 ESLint 规则迁移旧版代码

虽然 Web 组件可以轻松手动迁移,但迁移 registerRequiredCSS 的旧版用法的过程要复杂得多。注册旧版样式的两个主要函数是 registerRequiredCSScreateShadowRootWithCoreStyles。我们认为,由于迁移这些调用的步骤非常机械化,因此可以使用 ESLint 规则来应用修复程序并自动迁移旧版代码。DevTools 已经在使用一些专门针对 DevTools 代码库的自定义规则。这很有用,因为 ESLint 会将代码解析为抽象语法树(简称 AST)。AST),并且我们可以查询调用 CSS 注册的特定调用节点。

在编写迁移 ESLint 规则时,我们遇到的最大问题是捕获边缘情况。我们希望确保在了解哪些边界情况值得捕获以及哪些边界情况应手动迁移之间取得适当的平衡。我们还希望确保能够在构建系统未自动生成导入的 .css.js 文件时告知用户,以免在运行时出现任何文件未找到错误。

使用 ESLint 规则进行迁移的一个缺点是,我们无法更改系统中所需的 GN 构建文件。用户必须在每个目录中手动进行这些更改。虽然这需要做更多的工作,但却是确认要导入的每个 .css.js 文件实际上是由构建系统生成的好方法。

总体而言,使用 ESLint 规则进行此迁移非常有帮助,因为我们能够快速将旧代码迁移到新的基础架构,并且 AST 随时可用意味着我们还可以处理规则中的多种极端情况,并使用 ESLint 的 Fixer API 进行可靠的自动修复。

接下来该怎么做?

到目前为止,Chromium DevTools 中的所有 Web 组件都已迁移为使用新的 CSS 基础架构,而不是使用内嵌样式。registerRequiredCSS 的大多数旧用法也已迁移至新系统。现在只需移除尽可能多的 module.json 文件,然后迁移当前的基础架构,以便日后实现 CSS 模块脚本!

下载预览渠道

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

与 Chrome DevTools 团队联系

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