本文为翻译 本文译者为 360 奇舞团前端开发工程师
原文标题:CSS in Micro Frontends 原文作者:Florian Rappl 原文地址:https://dev.to/florianrappl/css-in-micro-frontends-4jai
我被问得最多的问题之一是如何在微前端中处理 CSS。毕竟,样式始终是任何UI片段所需要的东西,然而,它也是全局共享的东西,因此它是潜在的冲突来源。
在这篇文章中,我想回顾一下现有的不同策略来驯服 CSS 并使其扩展以开发微前端。如果这里的任何内容对您来说听起来合理,那么也可以考虑研究“微前端的艺术”。
本文的代码可以在github.com/piral-samples/css-in-mf找到。请务必检查示例实现。
CSS 的处理是否会影响每个微前端解决方案?让我们检查可用的类型来验证这一点。
微前端的类型
过去我写了很多关于存在哪些类型的微前端、为什么存在以及何时应该使用什么类型的微前端架构的文章。采用 Web 方法意味着使用 iframe 来使用来自不同微前端的 UI 片段。在这种情况下,没有任何限制,因为无论如何每个片段都是完全隔离的。在任何其他情况下,无论您的解决方案使用客户端还是服务器端组合(或介于两者之间的东西),您最终都会得到在浏览器中评估的样式。因此,在所有其他情况下,您都会关心 CSS。让我们看看这里有哪些选项。
无特殊处理
好吧,第一个也许是最(或根据观点,最不)明显的解决方案是不进行任何特殊处理。相反,每个微前端都可以附带额外的样式表,然后在渲染微前端的组件时附加这些样式表。
理想情况下,每个组件仅在首次渲染时加载所需的样式,但是,由于这些样式中的任何一个都可能与现有样式冲突,我们也可以假装在微前端的任何组件渲染时加载所有有问题的样式。这种方法的问题在于,当给出诸如div或之类的通用选择器时div a,我们还将重新设计其他元素的样式,而不仅仅是原始微前端的片段。更糟糕的是,类和属性也不是故障保护措施。类似的类.foobar也可以用在另一个微前端中。您将在引用的演示存储库中找到两个冲突的微前端的示例,网址为solutions/default。
摆脱这种痛苦的一个好方法是进一步隔离组件————就像 Web 组件一样。
Shadow DOM
在自定义元素中,我们可以打开一个shadow root来将元素附加到专用的迷你文档,该迷你文档实际上与其父文档相互隔离。总的来说,这听起来是一个好主意,但与这里介绍的所有其他解决方案一样,并没有强制要求。理想情况下,微前端可以自由决定如何实现组件。因此,实际的 Shadow DOM 集成必须由微前端完成。
使用 Shadow DOM 有一些缺点。最重要的是,虽然 Shadow DOM 内部的样式保留在内部,但全局样式也不会影响 Shadow DOM。乍一看,这似乎是一个优势,但是,由于整篇文章的主要目标只是隔离微前端的样式,因此您可能会错过诸如应用某些全局设计系统(例如 Bootstrap)之类的要求。link要使用 Shadow DOM 进行样式设置,我们可以通过引用或标签将样式放入 Shadow DOM 中style。由于 Shadow DOM 是无样式的,并且外部的样式不会传播到其中,因此我们实际上需要它。除了编写一些内联样式之外,我们还可以使用捆绑器将.css(或者类似的东西.shadow.css)视为原始文本。这样,我们只会得到一些文本。
piral-cli-esbuild对于 esbuild,我们可以配置如下的预制配置:
module.exports = function(options) {options.loader['.css'] = 'text';options.plugins.splice(0, 1);return options;
};
这会删除初始 CSS 处理器 (SASS) 并为.css文件配置标准加载器。现在,shadow DOM 中的某些样式的工作方式如下:
import css from "./style.css";customElements.define(name, class extends HTMLElement {constructor() {super();this.attachShadow({ mode: "open" });}connectedCallback() {this.style.display = "contents";const style = this.shadowRoot.appendChild(document.createElement('style'));style.textContent = css;}});
上面的代码是一个有效的自定义元素,从样式的角度来看它将是透明的(display: contents),即只有其内容会反映在渲染树中。它托管一个包含单个style元素的Shadow DOM。style 的内容设置为style.css文件的文本内容。
您将在引用的演示存储库中找到两个冲突的微前端的示例,网址为solutions/shadow-dom。
域组件避免使用Shadow DOM 的另一个原因是,并不是每个 UI 框架都能够处理Shadow DOM 中的元素。因此,无论如何都必须寻找一种替代方案。其中一种方式是转而使用一些 CSS 约定。
使用命名约定
如果每个微前端都遵循全局CSS约定,那么就可以在根上避免冲突。最简单的约定是在每个类前面加上微前端的名称。例如,如果调用一个微前端shopping,并调用另一个微前端checkout,则两者都会将其active类分别重命名为shopping-active/ checkout-active。
这同样适用于其他可能存在冲突的名称。例如,如果有一个名为 shopping 的微前端,那么我们可以将主按钮的ID从 primary-button 改为 shopping-primary-button。如果因某种原因需要为一个元素添加样式,我们应该使用后代选择器,例如 .shopping img,来为 img 标签添加样式。这样会应用于具有 shopping 类的元素内部的 img 元素。这种方法的问题在于 shopping 微前端可能还会使用其他微前端的元素。如果我们看到 div.shopping > div.checkout img,即使通过 checkout 微前端引入的组件承载/集成了 img,它仍然会受到 shopping 微前端 CSS 的样式影响。这并不理想。
您将在引用的演示存储库中找到两个冲突的微前端的示例,网址为https://github.com/piral-samples/css-in-mf/tree/main/solutions/default。
尽管命名约定在一定程度上解决了问题,但它们仍然容易出错并且使用起来很麻烦。如果我们重命名微前端会怎样?如果微前端在不同的应用程序中获得不同的名称怎么办?如果我们在某些时候忘记应用命名约定怎么办?这就是工具帮助我们的地方。
CSS Modules
自动添加前缀并避免命名冲突的最简单方法之一是使用 CSS 模块。根据您选择的打包工具,这可能是开箱即用的功能,或者通过更改一些配置实现。
// Import "default export" from CSSimport styles from './style.modules.css';// Apply<div className={styles.active}>Active</div>
导入的模块是一个生成的模块,将原始类名(例如 active)映射到生成的类名。生成的类名通常是CSS规则内容与原始类名混合后的哈希值。这样,生成的类名应该尽可能唯一.
例如,让我们考虑一个使用esbuild构建的微前端. 对于esbuild,您需要一个插件(esbuild-css-modules-plugin)和相应的配置更改来包含 CSS 模块。
使用Piral我们只需要调整已有的配置piral-cli-esbuild。我们删除标准 CSS 处理(使用 SASS)并替换为插件:
const cssModulesPlugin = require('esbuild-css-modules-plugin');module.exports = function(options) {options.plugins.splice(0, 1, cssModulesPlugin());return options;
};
现在我们可以像上面展示的那样在我们的代码中使用 CSS 模块了。
您将在引用的演示存储库中找到两个冲突的微前端的示例,网址为solutions/css-modules。
使用 CSS 模块会带来一些缺点。首先,它引入了几个语法扩展来区分我们想要导入的样式(因此需要进行预处理/哈希)和应保持原样的样式(即稍后无需导入即可使用的样式) 另一种方式是将 CSS 直接引入到 JS 文件中。
CSS-in-JS
CSS-in-JS 最近的名声很差,但是,我认为这是一个误解。我也更喜欢将其称为“CSS-in-Components”,因为它为组件本身带来了样式。一些框架(Astro、Svelte 等)甚至允许通过其他方式直接执行此操作。经常被提及的缺点是性能问题,这通常是由于在浏览器中编写 CSS 造成的。然而,这并不总是必要的,在最好的情况下,CSS-in-JS 库实际上是构建时间驱动的,即没有任何性能缺陷。然而,当我们谈论 CSS-in-JS(或 CSS-in-Components)时,我们需要考虑现有的各种选择。为简单起见,我只包含三个:Emotion、Styled Components和Vanilla Extract。让我们看看它们如何帮助我们在一个应用程序中将多个微前端整合在一起时避免冲突。
Emotion
Emotion 是一个非常棒的库,它为诸如React之类的框架提供了辅助功能,但并不要求将这些框架设置为先决条件。Emotion可以很好地优化和预先计算,并允许我们使用各种可用的 CSS 技术。
使用“pure”Emotion相对来说很简单;首先安装包:
npm i @emotion/css
现在您可以在代码中使用它,如下所示
import { css } from '@emotion/css';const tile = css`background: blue;color: yellow;flex: 1;display: flex;justify-content: center;align-items: center;`;// later<div className={tile}>Hello from Blue!</div>
css 助手允许我们编写 CSS,将其解析并放置在样式表中。返回值是生成的类名。
如果我们特别想使用 React,我们还可以使用Emotion 中的 jsx 工厂(引入一个名为 css 的新标准属性)或 styled 助手:
npm i @emotion/react @emotion/styled
现在感觉很像样式是 React 本身的一部分。例如,styled助手允许我们定义新组件:
const Output = styled.output`border: 1px dashed red;padding: 1rem;font-weight: bold;`;// later<Output>I am groot (from red)</Output>
相反,css助手属性使我们能够缩短表示法:
<div css={`background: red;color: white;flex: 1;display: flex;justify-content: center;align-items: center;`}>Hello from Red!</div>
总而言之,这生成的类名不会冲突,并提供了避免样式混乱的强大性能。特别是 styled 助手深受流行的 styled-components 库的启发。
您将在引用的演示存储库中找到两个冲突的微前端的示例,网址为:solutions/emotion。
Styled Components
styled-components库可以说是最受欢迎的CSS-in-JS解决方案,而且往往也是这类解决方案声誉不佳的原因。从历史上看,它实际上是关于在浏览器中组合CSS,但在过去几年中,它们确实在这方面取得了巨大进展。现在,您也可以对所使用的样式进行一些非常好的服务器端组合.
与emotion相比,styled-components库(对于React)需要安装一些少量的包。唯一的缺点是类型定义是事后添加的,因此您需要安装两个包以获得完整的TypeScript支持:
npm i styled-components --save
npm i @types/styled-components --save-dev
安装后,该库就已经完全可用:
import styled from 'styled-components';const Tile = styled.div`background: blue;color: yellow;flex: 1;display: flex;justify-content: center;align-items: center;
`;// later
<Tile>Hello from Blue!</Tile>
原理与 相同emotion。因此, 让我们探讨另一种尝试从一开始就实现零成本的选择, 而不是事后添加的.您将在引用的演示存储库中找到两个冲突的微前端的示例,网址为: solutions/styled-components
Vanilla Extract
我之前提到的利用类型接近组件并避免不必要的运行时成本的方法正是最新一代的CSS-in-JS库所涵盖的。其中最有潜力的库之一是@vanilla-extract/css。它允许你在JavaScript中直接编写CSS,并静态提取类名,从而减小打包大小,提高性能。这是一种有前途的选择,可以以类型安全和高效的方式管理样式。使用该库有两种主要方式:
与你的打包工具/框架集成
直接使用 CLI 在这个例子中,我们选择了第一种方式———— 通过与 esbuild 集成。为了使集成正常工作,我们需要使用该@vanilla-extract/esbuild-plugin包。现在我们将其集成到构建过程中。使用piral-cli-esbuild配置, 我们只需要将其添加到配置的插件中即可:
const { vanillaExtractPlugin } = require("@vanilla-extract/esbuild-plugin");module.exports = function (options) {options.plugins.push(vanillaExtractPlugin());return options;
};
为了使 Vanilla Extract 正常工作,我们需要编写.css.ts文件而不是普通文件.css或.sass文件。这样的文件可能如下所示:
import { style } from "@vanilla-extract/css";
export const heading = style({color: "blue",
});
这是有效的 TypeScript 代码。最终我们将获得一个类名的导出————就像我们从 CSS modules、Emotion 等中获得的那样。因此,最终,上述样式将会应用如下:
import { heading } from "./Page.css.ts";// later
<h2 className={heading}>Blue Title (should be blue)</h2>
这将在构建时完全处理,不会有任何运行时成本。
您将在引用的演示存储库中找到两个冲突的微前端的示例,网址为:solutions/vanilla-extract。
您可能会感兴趣的另一种方法是使用 CSS 实用程序库,例如 Tailwind。
CSS 实用程序,例如 Tailwind
这是一个独立的类别,但考虑到Tailwind是该类别中的主导工具,我只会介绍Tailwind。Tailwind的主导地位甚至到了一些人问“你是写CSS还是Tailwind?”这样的地步。这与2010年左右jQuery在DOM操作领域的主导地位非常相似,当时人们会问“这是JavaScript还是jQuery?”
无论如何,使用CSS实用库的优势在于样式是基于使用而生成的。这些样式不会冲突,因为它们始终由实用库以相同的方式定义。因此,每个微前端只需提供所需的实用库部分,以按预期显示微前端。
如果使用 Tailwind 和 esbuild,我们还需要安装以下软件包:
npm i autoprefixer tailwindcss esbuild-style-plugin
esbuild的配置比以前略微复杂一些。esbuild-style-plugin本质上是esbuild的一个PostCSS插件,所以必须正确配置
const postCssPlugin = require("esbuild-style-plugin");module.exports = function (options) {const postCss = postCssPlugin({postcss: {plugins: [require("tailwindcss"), require("autoprefixer")],},});options.plugins.splice(0, 1, postCss);return options;
};
在这里,我们移除了默认的CSS处理插件(SASS),并用PostCSS插件替代它——同时使用autoprefixer和tailwindcss这两个PostCSS扩展。现在我们需要添加一个有效的tailwind.config.js文件:
module.exports = {content: ["./src/**/*.tsx"],theme: {extend: {},},plugins: [],
};
这本质上是配置 Tailwind 的最低要求。它只是提到tsx应该扫描文件以了解 Tailwind 实用程序类的使用情况。然后找到的类将被放入 CSS 文件中。
因此,CSS 文件还需要知道生成/使用的声明应包含在哪里。作为最低要求,我们只有以下CSS内容:
@tailwind utilities;
还有其他@tailwind指令。例如,Tailwind自带一个重置和基础层。但是,在微前端中,我们通常不关心这些层。这属于应用程序 shell 或编排应用程序的关注范围,而不是领域应用程序的关注点。
然后,CSS将被来自Tailwind中已经指定的类所替代:
<div className="bg-red-600 text-white flex flex-1 justify-center items-center">Hello from Red!</div>
您将在引用的演示存储库中找到两个冲突的微前端的示例,网址为solutions/tailwind。
比较
到目前为止,所提出的几种方法都是微前端的可行选择。总的来说,这些解决方案也可以混合使用。一个微前端可以采用Shadow DOM方法,而另一个微前端可以使用Emotion。第三个库可能会选择Vanilla Extract。最重要的是所选择的解决方案不会产生冲突,并且没有(巨大的)运行时成本。虽然有些方法比其他方法更高效,但它们都提供了所需的样式隔离性。性能影响在很大程度上取决于实现方式。例如,对于CSS-in-JS,如果解析和组合都在运行时完成,可能会产生很大的性能影响。如果样式已经预解析,只在运行时组合,则可能性能影响较小。对于类似Vanilla Extract这样的解决方案,几乎没有任何性能影响。
对于 Shadow DOM,主要的性能影响可能是 Shadow DOM 内部元素的投影或移动(本质上为零)以及标签的重新评估style。然而,这是相当低的,甚至可能会产生一些性能优势,给定的样式总是切中要害,并且仅专用于要在Shadow DOM 中显示的某个组件。在示例中,我们有以下捆绑包大小:对于Emotion和Styled Components,这些数字仅供参考,因为运行时可能(并且很可能应该)被共享。此外,给定的微前端示例确实很小(所有UI片段的总大小为3KB)。对于一个更大的微前端,增长肯定不会像这里描述的那样成为问题。
Shadow DOM解决方案的大小增加可以解释为我们提供的简单实用脚本,用于将现有的React渲染轻松包装到Shadow DOM中(而不创建新的树结构)。如果这样的实用脚本在中心共享,那么其大小将更接近于其他更轻量级的解决方案。
结论
在微前端解决方案中处理CSS并不需要变得困难,只需要从一开始就以有结构、有序的方式进行处理,否则就会出现冲突和问题。通常情况下,建议选择 CSS 模块、Tailwind 或可扩展的 CSS-in-JS 实现等解决方案.
- END -
关于奇舞团
奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。