如何在微前端中處理 CSS?畢竟,樣式始終是任何 UI 片段所需要的東西,但是,它也是全局共享的東西,因此是潛在的衝突來源。

在這篇文章中,我想回顧一下現有的不同策略來馴服 CSS 並使其擴展以開發微前端。如果這裡的任何內容對您來說聽起來很合理,那麼也可以考慮研究一下“微前端的藝術”

本文的程式碼可以在github.com/piral-samples/css-in-mf找到。請務必查看示例實現。

CSS 的處理是否會影響每個微前端解決方案?讓我們檢查可用的類型來驗證這一點。

原文出處:https://dev.to/florianrappl/css-in-micro-frontends-4jai

微前端的類型

過去我寫了很多關於存在哪些類型的微前端、為什麼存在以及何時應該使用什麼類型的微前端架構的文章。採用 Web 方法意味著使用 iframe 來使用來自不同微前端的 UI 片段。在這種情況下,沒有任何限制,因為無論如何每個片段都是完全隔離的。

在任何其他情況下,無論您的解決方案使用客戶端還是伺服器端組合(或介於兩者之間的東西),您最終都會得到在瀏覽器中評估的樣式。因此,在所有其他情況下,您都會關心 CSS。讓我們看看這裡有哪些選項。

無特殊處理

好吧,第一個 - 也許是最(或根據觀點,最不)明顯的解決方案是不進行任何特殊處理。相反,每個微前端都可以附帶額外的樣式表,然後在渲染微前端的元件時附加這些樣式表。

理想情況下,每個元件僅在首次渲染時加載所需的樣式,但是,由於這些樣式中的任何一個都可能與現有樣式衝突,我們也可以假裝在微前端的任何元件渲染時加載所有“有問題的”樣式。

衝突

這種方法的問題在於,當給出諸如“div”或“div a”之類的通用選擇器時,我們還將重新設置其他元素的樣式,而不僅僅是原始微前端的片段。更糟糕的是,類和屬性也不是故障保護措施。像“.foobar”這樣的類也可以在另一個微前端中使用。

您將在引用的演示存儲庫中找到兩個衝突的微前端的示例,網址為 solutions/default

擺脫這種痛苦的一個好方法是進一步隔離元件 - 就像 Web 元件一樣。

影子 DOM

在自定義元素中,我們可以打開一個影子根來將元素附加到專用的迷你文件,該迷你文件實際上與其父文件屏蔽。總的來說,這聽起來是一個好主意,但與這裡介紹的所有其他解決方案一樣,沒有硬性要求。

Shadow DOM

理想情況下,微前端可以自由決定“如何”實現元件。因此,實際的 Shadow DOM 集成必須由微前端完成。

使用 Shadow DOM 有一些缺點。最重要的是,雖然 Shadow DOM 內部的樣式保留在內部,但全局樣式也不會影響 Shadow DOM。乍一看,這似乎是一個優勢,但是,由於整篇文章的主要目標只是隔離微前端的樣式,因此您可能會錯過諸如應用某些全局設計系統(例如 Bootstrap)之類的要求。

要使用 Shadow DOM 進行樣式設置,我們可以通過“link”引用或“style”標籤將樣式放入 Shadow DOM 中。由於 Shadow DOM 是無樣式的,並且外部的樣式不會傳播到其中,因此我們實際上需要它。除了編寫一些內聯樣式之外,我們還可以使用捆綁器將“.css”(或者類似“.shadow.css”的內容)視為原始文本。這樣,我們只會得到一些文本。

對於 esbuild,我們可以配置 piral-cli-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)。

域元件避免使用影子 DOM 的另一個原因是,並非每個 UI 框架都能夠處理影子 DOM 中的元素。因此,無論如何都必須尋找替代解決方案。一種方法是轉而使用一些 CSS 約定。

使用命名約定

如果每個微前端都遵循全局 CSS 約定,那麼就可以在元級別上避免衝突。最簡單的約定是在每個類前面加上微前端的名稱。因此,舉例來說,如果一個微前端稱為“shopping”,另一個微前端稱為“checkout”,那麼兩者都會將其“active”類分別重命名為“shopping-active”/“checkout-active”。

約定

這同樣適用於其他可能存在衝突的名稱。舉個例子,在微前端稱為“shopping”的情況下,我們將其稱為“shopping-primary-button”,而不是像“primary-button”這樣的ID。如果由於某種原因,我們需要設置元素的樣式,我們應該使用後代選擇器(例如“.shopping img”)來設置“img”標籤的樣式。現在,這適用於具有“shopping”類的 some 元素中的“img”元素。這種方法的問題是購物微前端也可能使用其他微前端的元素。如果我們看到“div.shopping > div.checkout img”怎麼辦?儘管“img”現在由通過“checkout”微前端帶來的元件託管/集成,但它的樣式將由“shopping”微前端 CSS 設計。這並不理想。

儘管命名約定在一定程度上解決了問題,但它們仍然容易出錯並且使用起來很麻煩。如果我們重命名微前端會怎樣?如果微前端在不同的應用程式中獲得不同的名稱怎麼辦?如果我們在某些時候忘記應用命名約定怎麼辦?這就是工具幫助我們的地方。

CSS 模塊

自動引入一些前綴並避免命名衝突的最簡單方法之一是使用 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 模塊,如上所示。

CSS 模塊有一些缺點。首先,它附帶了一些標準 CSS 的語法擴展。這對於區分我們想要導入的樣式(因此要進行預處理/哈希)和應保持原樣的樣式(即稍後在不導入的情況下使用)是必要的。另一種方法是將CSS直接帶入JS文件中。

CSS-in-JS

CSS-in-JS 最近的名聲很差,但是,我認為這是一個誤解。我也更喜歡將其稱為“CSS-in-Components”,因為它為元件本身帶來了樣式。一些框架(Astro、Svelte 等)甚至允許通過其他方式直接執行此操作。經常被提及的缺點是性能 - 這通常是由於在瀏覽器中編寫 CSS 造成的。然而,這並不總是必要的,在最好的情況下,CSS-in-JS 庫實際上是建置時間驅動的,即沒有任何性能缺陷。

CSS-in-JS

然而,當我們談論 CSS-in-JS(或 CSS-in-Components)時,我們需要考慮現有的各種選項。為簡單起見,我只包含三個:情感、樣式元件和香草提取物。讓我們看看它們如何幫助我們在將微前端整合到一個應用程式中時避免衝突。

Emotion

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,我們還可以使用 Emotion 中的 jsx 工廠(引入了一個名為 css 的新標準 prop)或 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”庫的啟發。

樣式元件

“styled-components”庫可以說是最流行的 CSS-in-JS 解決方案,並且常常是此類解決方案聲譽不佳的原因。從歷史上看,這實際上是在瀏覽器中編寫 CSS 的全部內容,但在過去幾年中,他們確實極大地推進了這一點。今天,您也可以對所使用的樣式進行一些非常好的伺服器端組合。

與“emotion”相比,安裝(針對 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>

其原理與“情感”相同。因此,讓我們探索另一種選擇,嘗試從一開始就實現零成本,而不是事後的想法。

Vanilla Extract

我之前寫的關於利用類型更接近元件(並避免不必要的執行時成本)的內容正是最新一代 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>

這將在建置時完全處理——而不是執行時成本。

您可能會感興趣的另一種方法是使用 CSS 實用程序庫,例如 Tailwind。

CSS 實用程序,例如 Tailwind

這是一個獨立的類別,但我認為既然 Tailwind 是這個類別中的主要工具,我將只介紹 Tailwind。 Tailwind 的主導地位甚至達到了甚至有人問“你寫 CSS 還是 Tailwind?”之類的問題。這與 jQuery 在 DOM 操作領域的統治地位非常相似。 2010 年,人們問“這是 JavaScript 還是 jQuery?”。

無論如何,使用 CSS 實用程序庫的優點是根據使用情況生成樣式。這些樣式不會衝突,因為實用程序庫始終以相同的方式定義它們。因此,每個微前端將僅附帶實用程序庫中根據需要顯示微前端所需的部分。

Tailwind

如果使用 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>

比較

迄今為止提出的幾乎每種方法都是微前端的可行競爭者。一般來說,這些溶液也可以混合。一個微前端可以採用影子 DOM 方法,而另一個微前端則對 Emotion 感到滿意。第三個圖書館可能會選擇使用香草精。

最後,唯一重要的是所選擇的解決方案是無碰撞的,並且不會帶來(巨大的)執行時成本。雖然某些方法比其他方法更有效,但它們都提供了所需的樣式隔離。

方法 遷移工作 可讀性 穩健性 性能影響
大會 中等
CSS 模塊 中等 從無到低
影子 DOM 低到中
JS 中的 CSS 中到高 從無到高
順風 中等

性能影響很大程度上取決於實施。例如,對於 CSS-in-JS,如果解析和組合在執行時完全完成,您可能會產生很大的影響。如果樣式已經預先解析但僅在執行時組合,則影響可能很小。如果使用像香草精這樣的解決方案,您基本上不會產生任何影響。

對於 Shadow DOM,主要的性能影響可能是 Shadow DOM 內部元素的投影或移動(本質上為零)以及“style”標籤的重新評估。然而,這是相當低的,甚至可能會產生一些性能優勢,給定的樣式總是切中要害,並且僅專用於要在影子 DOM 中顯示的某個元件。

在示例中,我們有以下捆綁包大小:

方法 索引 [kB] 頁碼 [kB] 表 [kB] 總體 [kB] 尺寸 [%]
默認 1.719 1.719 1.203 1.203 0.245 0.245 3.167 3.167 100%
大會 1.761 1.761 1.241 1.241 0.269 0.269 3.271 3.271 103%
CSS 模塊 2.149 2.149 2.394 2.394 0 4.543 4.543 143%
影子 DOM 10.044 10.044 1.264 1.264 0 11.308 11.308 357%
情感 1.670 1.670 1.632 1.632 25.785 25.785 29.087 29.087 918%
樣式元件 1.618 1.618 1.612 1.612 63.073 63.073 66.303 66.303 2093%
香草精 1.800 1.800 1.257 1.257 0.314 0.314 3.371 3.371 106%
順風 1.853 1.853 1.247 1.247 0.714 0.714 3.814 3.814 120%

對這些數字持保留態度,因為在情感和样式元件的情況下,執行時可以(並且可能甚至應該)共享。另外,給定的示例微前端確實很小(所有 UI 片段的總體大小為 3kB)。對於更大的微前端,增長肯定不會像這裡描述的那麼問題。

Shadow DOM 解決方案的大小增加可以通過我們提供的簡單實用腳本來解釋,該腳本可以輕鬆地將現有的 React 渲染包裝到 Shadow DOM 中(無需生成新樹)。如果這樣的實用程序是集中共享的,那麼其大小將更接近其他更輕量級的解決方案。

結論

在微前端解決方案中處理 CSS 並不困難 - 只需從一開始就以結構化和有序的方式完成,否則就會出現衝突和問題。一般來說,建議選擇 CSS 模塊、Tailwind 或可擴展的 CSS-in-JS 實現等解決方案。


共有 0 則留言