我最常被问到的问题之一是如何在微后端处理 CSS。 尽管任何 UI 部分仍然需要样式,但它们也是全局共享的,因此它们是潜在的冲突来源。
在这篇文章中,我想回顾一下可用于驯服 CSS 并使其增强以开发微后端的不同策略。 如果这里的任何内容对您来说听起来很合理,也可以考虑研究“微后端的艺术”。
本文的代码可以在 github.com/piral-samples/css-in-mf 中找到。 不一定要测试示例实现。
CSS的处理会影响每个微后端解决方案吗? 让我们检查可用的类型来验证这一点。
微后端的类型
我过去写过很多关于存在哪些类型的微后端、为什么存在以及何时应该使用哪些类型的微后端架构的文章。 采用 Web 方法意味着使用 iframe 来使用来自不同微后端的 UI 片段。 在这些情况下,没有任何限制,因为无论如何每个片段都是完全隔离的。 在任何其他情况下,无论您的解决方案使用客户端还是服务器端组合(或两者之间的组合),您最终都会得到在浏览器中评估的样式。 因此,在所有其他情况下,您都关心 CSS。 让我们看看这里有哪些可用选项。
无特殊处理
好吧,第一个似乎是最(或者根据观点,最不)明显的解决方案是不进行任何特殊处理。 相反,每个微后端都可以附带额外的样式表,然后在渲染微后端的组件时附加这些样式表。
理想情况下,每个组件只会在第一次渲染时加载所需的样式,并且由于任何此类样式都可能与现有样式冲突,因此我们也可以假装在微后端渲染中的任何组件时加载所有样式。 风格有问题。
这些方法的问题在于,当给定像 div 或 diva 这样的通用选择器时,我们还会重新设计其他元素,而不仅仅是原始微后端的片段。 更糟糕的是,类和属性也不是万无一失的措施。 类似的类 .foobar 也可以在另一个微后端中使用。 您将在解决方案/默认引用的演示存储库中找到两个冲突的微后端的示例。
摆脱这些痛苦的一个好方法是进一步隔离组件 - 就像 Web 组件一样。
ShadowDOM
在自定义元素中,我们可以打开一个 Shadowroot 将元素附加到与其父文档有效隔离的专用迷你文档。 总的来说,这听起来是个好主意,但与此处介绍的所有其他解决方案一样,它不是强制性的。
理想情况下,微后端可以自由决定如何实现组件。 为此,实际的 ShadowDOM 集成必须由微后端完成。
使用 ShadowDOM 有一些缺点。 最重要的是,全局样式不会影响 ShadowDOM,尽管 ShadowDOM 内部的样式会在内部保留。 乍一看,这显然是一个优势,但是,由于每篇文章的主要目标只是隔离微后端的风格,因此您可能会错过诸如应用单独的全局设计系统(例如 Bootstrap)之类的要求。 要使用 ShadowDOM 进行样式设置,我们可以通过引用或标记样式将样式加载到 ShadowDOM 中。 我们实际上需要它,因为 ShadowDOM 是无样式的,但外部样式不会传播到其中。 我们可以使用捆绑器将 .css (或类似 .shadow.css 的内容)视为原始文本,而不是仅仅编写一些内联样式。 这样,我们就可以得到一些文本。
piral-cli-esbuild 对于 esbuild,我们可以按如下方式配置 prefab 配置:
module.exports = function(options) {
options.loader['.css'] = 'text';
options.plugins.splice(0, 1);
return options;
};
这将删除初始 CSS 处理程序 (SASS) 并为 .css 文件配置标准加载器。 现在,shadowDOM 中的各个样式的工作方式如下:
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)来看它是透明的,即只有它的内容会反映在渲染树中。 它托管一个包含单个样式元素的 ShadowDOM。 style 的内容设置为 style.css 文件的文本内容。
您将在解决方案/shadow-dom 引用的演示存储库中找到两个冲突的微后端的示例。
域组件阻止使用 ShadowDOM 的另一个原因是,并非每个 UI 框架都可以处理 ShadowDOM 中的元素。 因此,无论如何都必须找到替代方案。 一种方法是改用一些 CSS 约定。
使用命名约定
如果每个微后端都遵守全局 CSS 约定,就可以从根本上防止冲突。 最简单的约定是在每个类上方添加微后端的名称。 例如,如果一个微后端称为“购物”,另一个微后端称为“结帐”,则两者都会将其活动类分别重命名为“购物活动”/“结账活动”。
这同样适用于可能冲突的其他名称。 例如,如果有一个名为shopping的微后端,我们可以将主按钮的ID从primary-button更改为shopping-primary-button。 如果由于某种原因我们需要设置元素的样式,我们应该使用后代选择器(例如 .shoppingimg)来设置 img 标签的样式。 这将应用于具有 Shopping 类的元素内的 img 元素。 这些技术的问题在于购物微后端可能使用其他微后端的元素。 如果我们看到div.shopping>div.checkoutimg,虽然通过结帐微后端hosts/integrates img引入的组件,但它仍然会受到购物微后端CSS样式的影响。 这并不理想。
您将在引用的演示存储库中找到两个冲突的微后端的示例,网址为。
虽然命名约定在一定程度上解决了问题,但它们仍然容易出错且使用起来很麻烦。 如果我们重命名微后端会怎样? 如果微后端在不同的应用程序中获得不同的名称怎么办? 如果我们有时忘记使用命名约定怎么办? 这就是工具帮助我们的地方。
CSS模块
手动添加前缀和防止命名冲突的最简单方法之一是使用 CSS 模块。 根据您选择的打包工具,这可能是开箱即用的功能,也可能通过修改某些配置来实现。
// Import "default export" from CSS
import styles from './style.modules.css';
// Apply
Active
导出模块是生成的模块,它将原始类名(例如 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 模块,如上所示。
您将在解决方案/css-modules 引用的演示存储库中找到两个冲突的微后端的示例。
使用 CSS 模块有一些缺点。 首先,它引入了几个句子扩展来区分我们想要导出的样式(因此需要预处理/散列)和应该保持不变的样式(即可以稍后使用而无需导出的样式)。 另一种方式就是直接在JS文件中引入CSS。
JS 中的 CSS
CSS-in-JS 最近名声不好,但我认为这是一个误解。 我还更喜欢将其称为“CSS-in-Components”,因为它为组件本身带来了样式。 一些框架(Astro、Svelte 等)甚至允许其他方法直接执行此操作。 一个经常被提及的缺点是性能问题,这通常是由在浏览器中编译 CSS 引起的。 然而,这并不总是必要的,在最好的情况下,CSS-in-JS 库实际上是构建时驱动的,即没有任何性能缺陷。
然而,当我们谈论 CSS-in-JS(或 CSS-in-Components)时,我们需要考虑各种可用的选项。 为简单起见,我只包含三个:Emotion、StyledComponents 和 VanillaExtract。 让我们看看它们如何帮助我们在一个应用程序上集成多个微后端时防止冲突。
情感
Emotion 是一个非常棒的库,它为 React 等框架提供了可访问性功能,但不需要此类框架作为先决条件。 情感可以很好地优化和预先计算,并允许我们使用各种可用的 CSS 技术。
使用“纯粹”的情感相对简单; 首先安装包:
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
Hello from Blue!
css 帮助器允许我们编译 CSS、解析它并将其放入样式表中。 返回值是生成的类名。
如果我们真的想使用React,我们还可以使用Emotion中的jsx鞋工厂(它引入了一个名为css的新标准属性)或styled helper:
npm i @emotion/react @emotion/styled
现在感觉样式就像是 React 本身的一部分。 例如,样式帮助器允许我们定义新组件:
const Output = styled.output`
border: 1px dashed red;
padding: 1rem;
font-weight: bold;
`;
// later
I am groot (from red)
相反,CSS 辅助属性使我们能够缩短符号:
Hello from Red!
总而言之,这会生成不会冲突的类名,并提供强大的性能来防止样式混乱。 非常有风格的助手,灵感来自流行的风格组件库。
您将在解决方案/情感引用的演示存储库中找到两个冲突的微后端的示例。
样式组件
styled-components 库可以说是最流行的 CSS-in-JS 解决方案,并且常常是此类解决方案声誉不佳的原因。 从历史上看,它实际上是在浏览器中编写 CSS,但在过去几年中,它们在这方面确实取得了长足的进步。 如今,您还可以对您使用的样式进行一些特别好的服务器端组合。
与情感相比,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
Hello from Blue!
原理和情感是一样的。 因此本地安装typescript,让我们举例说明另一种选择,即从一开始就尝试实现零成本,而不是事后才添加它。 您将在引用的演示存储库中找到两个冲突的微后端的示例:solutions/styled -components
香草精
我之前提到的使用类型处理组件并避免不必要的运行时成本的方法正是最新一代 CSS-in-JS 库所采用的方法。 最有潜力的库之一是@vanilla-extract/css。 它允许您直接在 JavaScript 中编译 CSS 并静态提取类名,从而减少包大小并提高性能。 这是以类型安全且高效的方式管理样式的一个有前途的选择。 使用该库有两种主要形式:
const { vanillaExtractPlugin } = require("@vanilla-extract/esbuild-plugin");
module.exports = function (options) {
options.plugins.push(vanillaExtractPlugin());
return options;
};
为了使 VanillaExtract 正常工作,我们需要编译 .css.ts 文件而不是普通的 .css 或 .sass 文件。 这样的文件可能如下所示:
import { style } from "@vanilla-extract/css";
export const heading = style({
color: "blue",
});
这是有效的 TypeScript 代码。 最终我们将获得一个类名的导入 - 就像我们从 CSSmodules、Emotion 等中获得的一样。为此,上述样式将按如下方式应用:
import { heading } from "./Page.css.ts";
// later
Blue Title (should be blue)
这将在构建时完全处理,无需任何运行时成本。
您将在引用的演示存储库中找到两个冲突的微后端的示例: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 带有重置层和基础层。 而且本地安装typescript,在微后端中,我们通常不关心这些层。 这是应用程序 shell 或编排应用程序的关注点,而不是域应用程序的关注点。
之后,CSS 将被已指定的 Tailwind 中的类替换:
Hello from Red!
您将在解决方案/tailwind 引用的演示存储库中找到两个冲突的微后端的示例。
比较
迄今为止提出的几种方法都是微后端的可行选择。 一般来说,该溶液也可以混合。 一个微后端可以使用 ShadowDOM,而另一个微后端可以使用 Emotion。 第三个库可能是 VanillaExtract。 最重要的是,所选择的解决方案不会产生冲突,而且没有(巨大的)运行时成本。 似乎某些方法比其他方法更有效,但它们都提供了所需的样式隔离。
性能影响很大程度上取决于实现方法。 例如,对于CSS-in-JS,如果解析和组合是在运行时完成的,则可能会造成很大的性能影响。 如果样式已经预先解析并且仅在运行时组合,则对性能的影响可能较小。 对于像 VanillaExtract 这样的解决方案,几乎没有任何性能影响。
对于 ShadowDOM 来说,主要的性能影响可能是 ShadowDOM 内部元素的投影或连接(基本上为零)以及标签样式的重新评估。 然而,这是相当低的,甚至可能会带来一些性能优势,给定的样式总是切中要点,但仅专用于要在 ShadowDOM 中显示的某个组件。 在示例中,我们有以下捆绑包大小:
对于 Emotion 和 StyledComponents,此数字仅供参考,因为运行时可能(但可能应该)共享。 据报道,给定的微后端示例非常小(所有 UI 片段的总大小为 3KB)。 对于更大的微后端,丢失当然不会像这里描述的那么问题。
ShadowDOM 解决方案尺寸的减小可以通过我们提供的简单实用脚本来解释,该脚本可以轻松地将现有的 React 渲染包装到 ShadowDOM 中(无需创建新的树结构)。 如果这样的实用程序脚本集中共享,其大小将更接近其他更轻量级的解决方案。
推理
在微后端解决方案中处理 CSS 并不一定很困难,只需从一开始就以结构化、有序的方式完成,否则就会出现冲突和问题。 一般来说,建议选择 CSS Modules、Tailwind 或可扩展的 CSS-in-JS 实现等解决方案。