微前端中的CSS

发布时间 2023-06-27 07:09:40作者: 晓风晓浪

我被问得最多的问题之一是如何在微前端中处理 CSS。毕竟,样式始终是任何UI 片段所需要的东西,然而,它也是全局共享的东西,因此是潜在的冲突来源。

在这篇文章中,我想回顾一下现有的不同策略来驯服 CSS 并使其扩展以开发微前端。如果这里的任何内容对您来说听起来合理,那么也可以考虑研究“微前端的艺术”。

本文的代码可以在github:piral-samples/css-in-mf找到。请务必检查示例实现。

CSS 的处理是否会影响每个微前端解决方案?让我们检查可用的类型来验证这一点。

(更|多优质内|容:java567 点 c0m)

 

微前端的类型

过去,我写了很多关于存在哪些类型的微前端、为什么存在以及何时应该使用什么类型的微前端架构的文章。采用 Web 方法意味着使用 iframe 来使用来自不同微前端的 UI 片段。在这种情况下,没有任何限制,因为无论如何每个片段都是完全隔离的。

在任何其他情况下,无论您的解决方案使用客户端还是服务器端组合(或介于两者之间的东西),您最终都会得到在浏览器中评估的样式。因此,在所有其他情况下,您都会关心 CSS。让我们看看这里有哪些选项。

无特殊处理

好吧,第一个 - 也许是最(或根据观点,最不)明显的解决方案是不进行任何特殊处理。相反,每个微前端都可以附带额外的样式表,然后在渲染微前端的组件时附加这些样式表。

理想情况下,每个组件仅在首次渲染时加载所需的样式,但是,由于这些样式中的任何一个都可能与现有样式冲突,我们也可以假装在微前端的任何组件渲染时加载所有有问题的样式。

 

这种方法的问题在于,当给出诸如div或 之类的通用选择器时div a,我们还将重新设计其他元素的样式,而不仅仅是原始微前端的片段。更糟糕的是,类和属性也不是故障保护措施。类似的类.foobar也可以用在另一个微前端中。

您将在引用的演示存储库中找到两个冲突的微前端的示例,网址为solutions/default。

摆脱这种痛苦的一个好方法是进一步隔离组件 - 就像 Web 组件一样。

影子 DOM

在自定义元素中,我们可以打开影子根以将元素附加到专用的迷你文档,该迷你文档实际上与其父文档屏蔽。总的来说,这听起来是一个好主意,但与此处介绍的所有其他解决方案一样,没有硬性要求。

 

理想情况下,微前端可以自由决定如何实现组件。因此,实际的 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元素的影子 DOM。的内容style设置为文件的文本style.css。

您将在引用的演示存储库中找到两个冲突的微前端的示例,网址为solutions/shadow-dom。

域组件避免使用 Shadow DOM 的另一个原因是,并非每个 UI 框架都能够处理 Shadow DOM 中的元素。因此,无论如何都必须寻找替代解决方案。一种方法是转而使用一些 CSS 约定。

使用命名约定

如果每个微前端都遵循全局 CSS 约定,那么就可以在元级别上避免冲突。最简单的约定是为每个类添加微前端名称的前缀。因此,例如,如果调用一个微前端shopping并调用另一个微前端checkout,则两者都会将其active类分别重命名为shopping-active/ checkout-active。

 

这同样适用于其他可能存在冲突的名称。举个例子,在微前端primary-button称为. 如果由于某种原因,我们需要设置元素的样式,我们应该使用后代选择器,例如设置标签的样式。现在这适用于具有该类的某些元素内的元素。这种方法的问题是购物微前端也可能使用其他微前端的元素。如果我们看到会怎样?尽管现在由通过微前端带来的组件托管/集成,但它仍将由微前端 CSS 设计样式。这并不理想。shopping-primary-buttonshopping.shopping imgimgimgshoppingdiv.shopping > div.checkout imgimgcheckout``shopping

您将在引用的演示存储库中找到两个冲突的微前端的示例,网址为github:piral-samples/css-in-mf/tree/main/solutions/default。

尽管命名约定在一定程度上解决了问题,但它们仍然容易出错并且使用起来很麻烦。如果我们重命名微前端会怎样?如果微前端在不同的应用程序中获得不同的名称怎么办?如果我们在某些时候忘记应用命名约定怎么办?这就是工具帮助我们的地方。

CSS 模块

自动引入一些前缀并避免命名冲突的最简单方法之一是使用 CSS 模块。根据您选择的捆绑器,这可以是开箱即用的,也可以通过一些配置更改来实现。

 

 // Import "default export" from CSS
 import 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 的语法扩展。这对于区分我们想要导入的样式(因此要进行预处理/哈希)和应保持原样的样式(即稍后在不导入的情况下使用)是必要的。另一种方法是将CSS直接带入JS文件中。

JS 中的 CSS

CSS-in-JS 最近的名声很差,但是,我认为这是一个误解。我也更喜欢将其称为“CSS-in-Components”,因为它为组件本身带来了样式。一些框架(Astro、Svelte 等)甚至允许通过其他方式直接执行此操作。经常被提及的缺点是性能 - 这通常是由于在浏览器中编写 CSS 造成的。然而,这并不总是必要的,在最好的情况下,CSS-in-JS 库实际上是构建时间驱动的,即没有任何性能缺陷。

 

然而,当我们谈论 CSS-in-JS(或 CSS-in-Components)时,我们需要考虑现有的各种选项。为简单起见,我只包含三个:情感、样式组件和香草提取物。让我们看看它们如何帮助我们在将微前端整合到一个应用程序中时避免冲突。

情感

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
 <div className={tile}>Hello from Blue!</div>

 

该css帮助器允许我们编写可解析并放置在样式表中的 CSS。返回值是生成的类的名称。

如果我们特别想使用 React,我们还可以使用jsxEmotion 中的工厂(引入一个名为 的新标准 prop 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库可以说是最流行的 CSS-in-JS 解决方案,并且常常是此类解决方案声誉不佳的原因。从历史上看,这实际上是在浏览器中编写 CSS 的全部内容,但在过去几年中,他们确实极大地推进了这一点。今天,您也可以对所使用的样式进行一些非常好的服务器端组合。

与安装(React)相比,emotion需要更少的软件包。唯一的缺点是打字是事后才想到的 - 所以你需要安装两个包才能完全喜欢 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。

香草精

我之前写的关于利用类型更接近组件(并避免不必要的运行时成本)的内容正是最新一代 CSS-in-JS 库所涵盖的内容。最有前途的库之一是@vanilla-extract/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 模块、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?”之类的问题。这与 jQuery 在 DOM 操作领域的统治地位非常相似。2010 年,人们问“这是 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 插件 - 使用PostCSS的autoprefixer和扩展。tailwindcss

现在我们需要添加一个有效的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。

比较

迄今为止提出的几乎每种方法都是微前端的可行竞争者。一般来说,这些溶液也可以混合。一个微前端可以采用影子 DOM 方法,而另一个微前端则对 Emotion 感到满意。第三个图书馆可能会选择使用香草精。

最后,唯一重要的是所选择的解决方案是无碰撞的,并且不会带来(巨大的)运行时成本。虽然某些方法比其他方法更有效,但它们都提供了所需的样式隔离。

方法迁移工作可读性稳健性性能影响
习俗 中等的 高的 低的 没有任何
CSS 模块 低的 高的 中等的 无到低
影子 DOM 低到中 高的 高的 低的
JS 中的 CSS 高的 中到高 高的 无到高
顺风 高的 中等的 高的 没有任何

性能影响很大程度上取决于实施。例如,对于 CSS-in-JS,如果解析和组合在运行时完全完成,您可能会产生很大的影响。如果样式已经预先解析但仅在运行时组合,则影响可能很小。如果使用像香草精这样的解决方案,您基本上不会产生任何影响。

对于 Shadow DOM,主要的性能影响可能是 Shadow DOM 内部元素的投影或移动(本质上为零)以及标签的重新评估style。然而,这是相当低的,甚至可能会产生一些性能优势,因为给定的样式总是切中要害,并且仅专用于要在影子 DOM 中显示的某个组件。

在示例中,我们有以下捆绑包大小:

方法索引 [kB]页码 [kB]张数 [kB]总体 [kB]尺寸 [%]
默认 1.719 1.203 0.245 3.167 100%
习俗 1.761 1.241 0.269 3.271 103%
CSS 模块 2.149 2.394 0 4.543 143%
影子 DOM 10.044 1.264 0 11.308 357%
情感 1.670 1.632 25.785 29.087 918%
样式组件 1.618 1.612 63.073 66.303 2093%
香草精 1.800 1.257 0.314 3.371 106%
顺风 1.853 1.247 0.714 3.814 120%

对这些数字持保留态度,因为在情感和样式组件的情况下,运行时可以(并且可能甚至应该)共享。另外,给出的示例微前端确实很小(所有 UI 片段的总体大小为 3kB)。对于更大的微前端,增长肯定不会像这里描述的那么问题。

Shadow DOM 解决方案的大小增加可以通过我们提供的简单实用脚本来解释,该脚本可以轻松地将现有的 React 渲染包装到 Shadow DOM 中(无需生成新树)。如果这样的实用程序是集中共享的,那么其大小将更接近其他更轻量级的解决方案。

结论

在微前端解决方案中处理 CSS 并不困难 - 只需从一开始就以结构化和有序的方式完成,否则就会出现冲突和问题。一般来说,建议选择 CSS 模块、Tailwind 或可扩展的 CSS-in-JS 实现等解决方案。

(更|多优质内|容:java567 点 c0m)