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

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

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

开发者工具中的上一个 CSS 状态

开发者工具通过两种不同的方式实现 CSS:一种针对开发者工具的旧版部分中使用的 CSS 文件,一种针对在开发者工具中使用的现代网络组件

开发者工具中的 CSS 实现早在多年前就已定义过,如今已过时。开发者工具坚持使用 module.json 模式,因此需要花费大量精力来移除这些文件。移除这些文件的最后一个阻碍因素是 resources 部分,该部分用于在 CSS 文件中加载。

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

开发者工具中的任何 CSS 文件都被视为“旧版”,因为它们是使用 module.json 文件加载的,而该文件正在移除中。所有 CSS 文件都必须列在 CSS 文件所在目录下的 module.json 文件中的 resources 下。

其余 module.json 文件的示例:

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

然后,这些 CSS 文件将填充一个名为 Root.Runtime.cachedResources 的全局对象映射,作为从路径到其内容的映射。为了将样式添加到开发者工具中,您需要调用 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);

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

  • 不支持突出显示语法。为内嵌 CSS 提供语法突出显示的插件往往不如在 .css 文件中编写的 CSS 语法突出显示和自动补全功能好。
  • 构建性能开销。内嵌 CSS 还意味着需要进行 lint 检查两次:一个用于 CSS 文件,另一个用于内嵌 CSS。如果所有 CSS 都是用独立的 CSS 文件编写的,我们可以消除这种性能开销。
  • 缩减大小方面的挑战。内嵌 CSS 无法轻松缩减,因此所有 CSS 均无法缩减。开发者工具的发布 build 的文件大小也因同一网络组件的多个实例引入的重复 CSS 而增大。

我的实习项目的目标是为 CSS 基础架构寻找一种解决方案,这种解决方案可以同时与旧版基础架构和开发者工具中使用的新网络组件一起使用。

研究潜在解决方案

问题可分为两部分:

  • 了解构建系统如何处理 CSS 文件。
  • 了解开发者工具如何导入和使用 CSS 文件。

我们研究了每个部分的不同潜在解决方案,下面列出了这些解决方案。

导入 CSS 文件

在 TypeScript 文件中导入和使用 CSS 的目标是尽可能遵循网页标准、在整个开发者工具中强制保持一致性,并避免 HTML 中出现重复的 CSS。我们还希望能选择一种解决方案,将我们的更改迁移到新的网络平台标准,例如 CSS 模块脚本。

出于这些原因,@import 语句和 标记似乎并不适合开发者工具。它们与开发者工具其余部分中的导入不一致,并导致出现无样式内容闪烁 (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(开发者工具现在已经使用好几年了,因为 Shadow DOM)。

Bundler 选项

我们需要一种将 CSS 文件转换为 CSSStyleSheet 对象的方法,以便在 TypeScript 文件中轻松操控该对象。我们考虑将 Rollupwebpack 都视为能够实现此转型的潜在捆绑器。开发者工具已在其正式版 build 中使用 Rollup,但在使用我们当前的构建系统时,如果将任一捆绑器添加到正式版 build,可能存在性能问题。我们与 Chromium 的 GN 构建系统的集成使得捆绑更加困难,因此捆绑器往往无法与当前的 Chromium 构建系统完美集成。

相反,我们探索了使用当前 GN 构建系统来代替我们执行这种转换的选项。

在开发者工具中使用 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 文件,并将其转换为 JavaScript 文件,该文件会默认导出 CSSStyleSheet 对象。这太棒了,我们可以导入 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 规则迁移旧版代码

虽然网络组件可以轻松手动迁移,但迁移旧版 registerRequiredCSS 的过程更复杂。注册旧版样式的两个主要函数是 registerRequiredCSScreateShadowRootWithCoreStyles。我们认为迁移这些调用的步骤相当机械,因此我们可以使用 ESLint 规则来应用修复程序并自动迁移旧代码。开发者工具已经使用了一些特定于开发者工具代码库的自定义规则。这很有帮助,因为 ESLint 已经将代码解析为一个抽象语法树(abbr. AST),我们可以查询用于注册 CSS 的特定调用节点。

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

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

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

下一步做什么?

到目前为止,Chromium 开发者工具中的所有网络组件都已迁移为使用新的 CSS 基础架构,而不是使用内嵌样式。registerRequiredCSS 的大部分旧版用法也已迁移至新系统。唯一要做的就是移除尽可能多的 module.json 文件,然后迁移当前的基础架构,以便将来实现 CSS 模块脚本!

下载预览渠道

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

与 Chrome 开发者工具团队联系

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

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