开发者工具架构刷新:对 DevTools 中的 CSS 基础架构进行现代化改造
本文属于一系列博文的一部分,介绍了我们对 DevTools 架构所做的更改以及其构建方式。我们将介绍 CSS 在开发者工具中的运作方式,以及我们如何对开发者工具中的 CSS 进行现代化改造,为最终迁移到在 JavaScript 文件中加载 CSS 的 Web 标准解决方案做好准备。
开发者工具中的 CSS 先前状态
DevTools 以两种不同的方式实现了 CSS:一种适用于 DevTools 的旧版部分中使用的 CSS 文件,另一种适用于 DevTools 中使用的现代 Web 组件。
开发者工具中的 CSS 实现定义于多年前,现已过时。开发者工具一直坚持使用 module.json
模式,并且在移除这些文件方面付出了巨大的努力。移除这些文件的最后一道障碍是 resources
部分,该部分用于加载 CSS 文件。
我们希望花些时间探索各种潜在解决方案,这些方案最终可能会演变为 CSS 模块脚本。目的是消除旧系统造成的技术债务,同时简化向 CSS 模块脚本的迁移过程。
由于 DevTools 中的所有 CSS 文件都是使用 module.json
文件加载的,而 module.json
文件正在被移除,因此这些 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。我们还希望能够选择一种解决方案,使我们能够将所做的更改迁移到新的 Web 平台标准,例如 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 文件中轻松操控它。我们将 Rollup 和 webpack 都视为可能为我们执行此转换的捆绑程序。DevTools 已在其正式版 build 中使用 Rollup,但如果将这两种捆绑器添加到正式版 build,在使用我们当前的 build 系统时可能会出现性能问题。我们与 Chromium 的 GN 构建系统的集成使得打包变得更加困难,因此打包工具往往无法与当前的 Chromium 构建系统良好集成。
我们改为探索了使用当前 GN 构建系统来执行此转换的选项。
在 DevTools 中使用 CSS 的新基础架构
新解决方案涉及使用 adoptedStyleSheets
向特定 Shadow DOM 添加样式,同时使用 GN 构建系统生成可供 document
或 ShadowRoot
采用的 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;
使用 ESLint 规则迁移旧版代码
虽然 Web 组件可以轻松手动迁移,但迁移 registerRequiredCSS
的旧版用法的过程要复杂得多。注册旧版样式的两个主要函数是 registerRequiredCSS
和 createShadowRootWithCoreStyles
。我们认为,由于迁移这些调用的步骤非常机械,因此可以使用 ESLint 规则来应用修复程序并自动迁移旧代码。DevTools 已经在使用一些专门针对 DevTools 代码库的自定义规则。这很有用,因为 ESLint 会将代码解析为抽象语法树(简称 AST)。AST),并且我们可以查询调用 CSS 注册的特定调用节点。
在编写迁移 ESLint 规则时,我们遇到的最大问题是捕获边缘情况。我们希望确保在了解哪些边界情况值得捕获以及哪些边界情况应手动迁移之间取得适当的平衡。我们还希望确保能够在构建系统未自动生成导入的 .css.js
文件时告知用户,以免在运行时出现任何文件未找到错误。
使用 ESLint 规则进行迁移的一个缺点是,我们无法更改系统中所需的 GN build 文件。用户必须在每个目录中手动进行这些更改。虽然这需要做更多的工作,但却是确认要导入的每个 .css.js
文件实际上是由构建系统生成的好方法。
总体而言,使用 ESLint 规则进行此迁移非常有帮助,因为我们能够快速将旧版代码迁移到新的基础架构,并且 AST 随时可用意味着我们还可以处理规则中的多个边界情况,并使用 ESLint 的 fixer API 可靠地自动修正这些情况。
接下来该怎么做?
到目前为止,Chromium DevTools 中的所有 Web 组件都已迁移为使用新的 CSS 基础架构,而不是使用内嵌样式。registerRequiredCSS
的大多数旧版用法也已迁移为使用新系统。现在只需移除尽可能多的 module.json
文件,然后迁移当前的基础架构,以便日后实现 CSS 模块脚本!
下载预览渠道
不妨考虑将 Chrome Canary 版、开发者版或 Beta 版用作默认开发浏览器。通过这些预览版渠道,您可以使用最新的 DevTools 功能、测试尖端的 Web 平台 API,并帮助您在用户发现问题之前发现网站上的问题!
与 Chrome DevTools 团队联系
您可以使用以下选项讨论与 DevTools 相关的新功能、更新或任何其他内容。
- 请访问 crbug.com 向我们提交反馈和功能请求。
- 在 DevTools 中,依次选择 More options > Help > Report a DevTools issue 以报告 DevTools 问题。
- 向 @ChromeDevTools 发送推文。
- 在 “开发者工具的新变化”YouTube 视频或 “开发者工具提示”YouTube 视频中留言。