微前端中的CSS
在這篇文章中,我想回顧一下現(xiàn)有的不同策略來(lái)馴服 CSS 并使其擴(kuò)展以開發(fā)微前端。如果這里的任何內(nèi)容對(duì)您來(lái)說(shuō)聽起來(lái)合理,那么也可以考慮研究“微前端的藝術(shù)”。
本文的代碼可以在github:piral-samples/css-in-mf找到。請(qǐng)務(wù)必檢查示例實(shí)現(xiàn)。
CSS 的處理是否會(huì)影響每個(gè)微前端解決方案?讓我們檢查可用的類型來(lái)驗(yàn)證這一點(diǎn)。
(更|多優(yōu)質(zhì)內(nèi)|容:java567 點(diǎn) c0m)
微前端的類型
過(guò)去,我寫了很多關(guān)于存在哪些類型的微前端、為什么存在以及何時(shí)應(yīng)該使用什么類型的微前端架構(gòu)的文章。采用 Web 方法意味著使用 iframe 來(lái)使用來(lái)自不同微前端的 UI 片段。在這種情況下,沒(méi)有任何限制,因?yàn)闊o(wú)論如何每個(gè)片段都是完全隔離的。
在任何其他情況下,無(wú)論您的解決方案使用客戶端還是服務(wù)器端組合(或介于兩者之間的東西),您最終都會(huì)得到在瀏覽器中評(píng)估的樣式。因此,在所有其他情況下,您都會(huì)關(guān)心 CSS。讓我們看看這里有哪些選項(xiàng)。
無(wú)特殊處理
好吧,第一個(gè) - 也許是最(或根據(jù)觀點(diǎn),最不)明顯的解決方案是不進(jìn)行任何特殊處理。相反,每個(gè)微前端都可以附帶額外的樣式表,然后在渲染微前端的組件時(shí)附加這些樣式表。
理想情況下,每個(gè)組件僅在首次渲染時(shí)加載所需的樣式,但是,由于這些樣式中的任何一個(gè)都可能與現(xiàn)有樣式?jīng)_突,我們也可以假裝在微前端的任何組件渲染時(shí)加載所有有問(wèn)題的樣式。
這種方法的問(wèn)題在于,當(dāng)給出諸如div或 之類的通用選擇器時(shí)div a,我們還將重新設(shè)計(jì)其他元素的樣式,而不僅僅是原始微前端的片段。更糟糕的是,類和屬性也不是故障保護(hù)措施。類似的類.foobar也可以用在另一個(gè)微前端中。
您將在引用的演示存儲(chǔ)庫(kù)中找到兩個(gè)沖突的微前端的示例,網(wǎng)址為solutions/default。
擺脫這種痛苦的一個(gè)好方法是進(jìn)一步隔離組件 - 就像 Web 組件一樣。
影子 DOM
在自定義元素中,我們可以打開影子根以將元素附加到專用的迷你文檔,該迷你文檔實(shí)際上與其父文檔屏蔽??偟膩?lái)說(shuō),這聽起來(lái)是一個(gè)好主意,但與此處介紹的所有其他解決方案一樣,沒(méi)有硬性要求。
理想情況下,微前端可以自由決定如何實(shí)現(xiàn)組件。因此,實(shí)際的 Shadow DOM 集成必須由微前端完成。
使用 Shadow DOM 有一些缺點(diǎn)。最重要的是,雖然 Shadow DOM 內(nèi)部的樣式保留在內(nèi)部,但全局樣式也不會(huì)影響 Shadow DOM。乍一看,這似乎是一個(gè)優(yōu)勢(shì),但是,由于整篇文章的主要目標(biāo)只是隔離微前端的樣式,因此您可能會(huì)錯(cuò)過(guò)諸如應(yīng)用某些全局設(shè)計(jì)系統(tǒng)(例如 Bootstrap)之類的要求。
link要使用 Shadow DOM 進(jìn)行樣式設(shè)置,我們可以通過(guò)引用或標(biāo)簽將樣式放入 Shadow DOM 中style。由于 Shadow DOM 是無(wú)樣式的,并且外部的樣式不會(huì)傳播到其中,因此我們實(shí)際上需要它。除了編寫一些內(nèi)聯(lián)樣式之外,我們還可以使用捆綁器將.css(或者類似的東西.shadow.css)視為原始文本。這樣,我們只會(huì)得到一些文本。
piral-cli-esbuild對(duì)于 esbuild,我們可以配置如下的預(yù)制配置:
?module.exports = function(options) {
? ?options.loader['.css'] = 'text';
? ?options.plugins.splice(0, 1);
? ?return options;
?};
這會(huì)刪除初始 CSS 處理器 (SASS) 并為.css文件配置標(biāo)準(zhǔn)加載器?,F(xiàn)在,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;
? ?}
?});
上面的代碼是一個(gè)有效的自定義元素,從樣式角度 ( display: contents) 來(lái)看,它是透明的,即只有其內(nèi)容會(huì)反映在渲染樹中。它托管一個(gè)包含單個(gè)style元素的影子 DOM。的內(nèi)容style設(shè)置為文件的文本style.css。
您將在引用的演示存儲(chǔ)庫(kù)中找到兩個(gè)沖突的微前端的示例,網(wǎng)址為solutions/shadow-dom。
域組件避免使用 Shadow DOM 的另一個(gè)原因是,并非每個(gè) UI 框架都能夠處理 Shadow DOM 中的元素。因此,無(wú)論如何都必須尋找替代解決方案。一種方法是轉(zhuǎn)而使用一些 CSS 約定。
使用命名約定
如果每個(gè)微前端都遵循全局 CSS 約定,那么就可以在元級(jí)別上避免沖突。最簡(jiǎn)單的約定是為每個(gè)類添加微前端名稱的前綴。因此,例如,如果調(diào)用一個(gè)微前端shopping并調(diào)用另一個(gè)微前端checkout,則兩者都會(huì)將其active類分別重命名為shopping-active/ checkout-active。
這同樣適用于其他可能存在沖突的名稱。舉個(gè)例子,在微前端primary-button稱為. 如果由于某種原因,我們需要設(shè)置元素的樣式,我們應(yīng)該使用后代選擇器,例如設(shè)置標(biāo)簽的樣式?,F(xiàn)在這適用于具有該類的某些元素內(nèi)的元素。這種方法的問(wèn)題是購(gòu)物微前端也可能使用其他微前端的元素。如果我們看到會(huì)怎樣?盡管現(xiàn)在由通過(guò)微前端帶來(lái)的組件托管/集成,但它仍將由微前端 CSS 設(shè)計(jì)樣式。這并不理想。shopping-primary-buttonshopping.shopping imgimgimgshoppingdiv.shopping > div.checkout imgimgcheckout``shopping
您將在引用的演示存儲(chǔ)庫(kù)中找到兩個(gè)沖突的微前端的示例,網(wǎng)址為github:piral-samples/css-in-mf/tree/main/solutions/default。
盡管命名約定在一定程度上解決了問(wèn)題,但它們?nèi)匀蝗菀壮鲥e(cuò)并且使用起來(lái)很麻煩。如果我們重命名微前端會(huì)怎樣?如果微前端在不同的應(yīng)用程序中獲得不同的名稱怎么辦?如果我們?cè)谀承r(shí)候忘記應(yīng)用命名約定怎么辦?這就是工具幫助我們的地方。
CSS 模塊
自動(dòng)引入一些前綴并避免命名沖突的最簡(jiǎn)單方法之一是使用 CSS 模塊。根據(jù)您選擇的捆綁器,這可以是開箱即用的,也可以通過(guò)一些配置更改來(lái)實(shí)現(xiàn)。
?// Import "default export" from CSS
?import styles from './style.modules.css';
?
?// Apply
?<div className={styles.active}>Active</div>
導(dǎo)入的模塊是一個(gè)生成的模塊,保存將其原始類名(例如,active)映射到生成的類名的值。生成的類名通常是 CSS 規(guī)則內(nèi)容與原始類名混合的哈希值。這樣,名稱應(yīng)該盡可能唯一。
作為一個(gè)例子,讓我們考慮一個(gè)使用esbuild. 因?yàn)閑sbuild您需要一個(gè)插件 ( esbuild-css-modules-plugin) 和相應(yīng)的配置更改以包含 CSS 模塊。
使用Piral我們只需要調(diào)整已經(jīng)帶來(lái)的配置piral-cli-esbuild。我們刪除標(biāo)準(zhǔn) CSS 處理(使用 SASS)并用插件替換:
?const cssModulesPlugin = require('esbuild-css-modules-plugin');
?
?module.exports = function(options) {
? ?options.plugins.splice(0, 1, cssModulesPlugin());
? ?return options;
?};
現(xiàn)在我們可以在代碼中使用 CSS 模塊,如上所示。
您將在引用的演示存儲(chǔ)庫(kù)中找到兩個(gè)沖突的微前端的示例,網(wǎng)址為solutions/css-modules。
CSS 模塊有一些缺點(diǎn)。首先,它附帶了一些標(biāo)準(zhǔn) CSS 的語(yǔ)法擴(kuò)展。這對(duì)于區(qū)分我們想要導(dǎo)入的樣式(因此要進(jìn)行預(yù)處理/哈希)和應(yīng)保持原樣的樣式(即稍后在不導(dǎo)入的情況下使用)是必要的。另一種方法是將CSS直接帶入JS文件中。
JS 中的 CSS
CSS-in-JS 最近的名聲很差,但是,我認(rèn)為這是一個(gè)誤解。我也更喜歡將其稱為“CSS-in-Components”,因?yàn)樗鼮榻M件本身帶來(lái)了樣式。一些框架(Astro、Svelte 等)甚至允許通過(guò)其他方式直接執(zhí)行此操作。經(jīng)常被提及的缺點(diǎn)是性能 - 這通常是由于在瀏覽器中編寫 CSS 造成的。然而,這并不總是必要的,在最好的情況下,CSS-in-JS 庫(kù)實(shí)際上是構(gòu)建時(shí)間驅(qū)動(dòng)的,即沒(méi)有任何性能缺陷。
然而,當(dāng)我們談?wù)?CSS-in-JS(或 CSS-in-Components)時(shí),我們需要考慮現(xiàn)有的各種選項(xiàng)。為簡(jiǎn)單起見,我只包含三個(gè):情感、樣式組件和香草提取物。讓我們看看它們?nèi)绾螏椭覀冊(cè)趯⑽⑶岸苏系揭粋€(gè)應(yīng)用程序中時(shí)避免沖突。
情感
Emotion 是一個(gè)非??岬膸?kù),它附帶了 React 等框架的幫助程序,但沒(méi)有將這些框架設(shè)置為先決條件。情感可以很好地優(yōu)化和預(yù)先計(jì)算,并允許我們使用可用的 CSS 技術(shù)的完整庫(kù)。
使用“純粹”情感相當(dāng)容易;首先安裝包:
?npm i @emotion/css
現(xiàn)在您可以在代碼中使用它,如下所示:
?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 中的工廠(引入一個(gè)名為 的新標(biāo)準(zhǔn) prop css)或styled助手:
?npm i @emotion/react @emotion/styled
現(xiàn)在感覺(jué)很像樣式是 React 本身的一部分。例如,styled幫助器允許我們定義新組件:
?const Output = styled.output`
? ?border: 1px dashed red;
? ?padding: 1rem;
? ?font-weight: bold;
?`;
?
?// later
?<Output>I am groot (from red)</Output>
相反,css輔助屬性使我們能夠稍微縮短符號(hào):
?<div css={`
? ?background: red;
? ?color: white;
? ?flex: 1;
? ?display: flex;
? ?justify-content: center;
? ?align-items: center;
?`}>
? ?Hello from Red!
?</div>
總而言之,這會(huì)生成不會(huì)沖突的類名,并提供避免樣式混合的穩(wěn)健性。這個(gè)styled助手尤其受到流行styled-components圖書館的啟發(fā)。
您將在引用的演示存儲(chǔ)庫(kù)中找到兩個(gè)沖突的微前端的示例,網(wǎng)址為solutions/emotion。
樣式組件
該styled-components庫(kù)可以說(shuō)是最流行的 CSS-in-JS 解決方案,并且常常是此類解決方案聲譽(yù)不佳的原因。從歷史上看,這實(shí)際上是在瀏覽器中編寫 CSS 的全部?jī)?nèi)容,但在過(guò)去幾年中,他們確實(shí)極大地推進(jìn)了這一點(diǎn)。今天,您也可以對(duì)所使用的樣式進(jìn)行一些非常好的服務(wù)器端組合。
與安裝(React)相比,emotion需要更少的軟件包。唯一的缺點(diǎn)是打字是事后才想到的 - 所以你需要安裝兩個(gè)包才能完全喜歡 TypeScript:
?npm i styled-components --save
?npm i @types/styled-components --save-dev
安裝后,該庫(kù)就已經(jīng)完全可用:
?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。因此,讓我們探索另一種選擇,嘗試從一開始就實(shí)現(xiàn)零成本,而不是事后的想法。
您將在引用的演示存儲(chǔ)庫(kù)中找到兩個(gè)沖突的微前端的示例,網(wǎng)址為solutions/styled-components。
香草精
我之前寫的關(guān)于利用類型更接近組件(并避免不必要的運(yùn)行時(shí)成本)的內(nèi)容正是最新一代 CSS-in-JS 庫(kù)所涵蓋的內(nèi)容。最有前途的庫(kù)之一是@vanilla-extract/css.
使用該庫(kù)有兩種主要方式:
與您的捆綁器/框架集成
直接使用 CLI
在此示例中,我們選擇前者 - 并將其集成到esbuild. 為了使集成正常工作,我們需要使用該@vanilla-extract/esbuild-plugin包。
現(xiàn)在我們將其集成到構(gòu)建過(guò)程中。使用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。我們最終會(huì)得到一個(gè)類名的導(dǎo)出 - 就像我們從 CSS 模塊、Emotion 中得到的一樣 - 你明白了。
所以最后,上面的樣式將像這樣應(yīng)用:
?import { heading } from "./Page.css.ts";
?
?// later
?<h2 className={heading}>Blue Title (should be blue)</h2>
這將在構(gòu)建時(shí)完全處理——而不是運(yùn)行時(shí)成本。
您將在引用的演示存儲(chǔ)庫(kù)中找到兩個(gè)沖突的微前端的示例,網(wǎng)址為solutions/vanilla-extract。
您可能會(huì)感興趣的另一種方法是使用 CSS 實(shí)用程序庫(kù),例如 Tailwind。
CSS 實(shí)用程序,例如 Tailwind
這是一個(gè)獨(dú)立的類別,但我認(rèn)為既然 Tailwind 是這個(gè)類別中的主要工具,我將只介紹 Tailwind。Tailwind 的主導(dǎo)地位甚至達(dá)到了甚至有人問(wèn)“你寫 CSS 還是 Tailwind?”之類的問(wèn)題。這與 jQuery 在 DOM 操作領(lǐng)域的統(tǒng)治地位非常相似。2010 年,人們問(wèn)“這是 JavaScript 還是 jQuery?”。
無(wú)論如何,使用 CSS 實(shí)用程序庫(kù)的優(yōu)點(diǎn)是根據(jù)使用情況生成樣式。這些樣式不會(huì)沖突,因?yàn)閷?shí)用程序庫(kù)始終以相同的方式定義它們。因此,每個(gè)微前端將僅附帶實(shí)用程序庫(kù)中根據(jù)需要顯示微前端所需的部分。
如果使用 Tailwind 和 esbuild,我們還需要安裝以下軟件包:
?npm i autoprefixer tailwindcss esbuild-style-plugin
esbuild的配置比之前復(fù)雜一點(diǎn)。本質(zhì)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;
?};
在這里,我們刪除了默認(rèn)的 CSS 處理插件 (SASS),并將其替換為 PostCSS 插件 - 使用PostCSS的autoprefixer和擴(kuò)展。tailwindcss
現(xiàn)在我們需要添加一個(gè)有效的tailwind.config.js文件:
?module.exports = {
? ?content: ["./src/**/*.tsx"],
? ?theme: {
? ? ?extend: {},
? ?},
? ?plugins: [],
?};
這本質(zhì)上是配置 Tailwind 的最低要求。它只是提到tsx應(yīng)該掃描文件以了解 Tailwind 實(shí)用程序類的使用情況。然后找到的類將被放入 CSS 文件中。
因此,CSS 文件還需要知道生成/使用的聲明應(yīng)包含在哪里。至少我們只有以下 CSS:
?@tailwind utilities;
還有其他@tailwind說(shuō)明。例如,Tailwind 帶有重置和基礎(chǔ)層。然而,在微前端中,我們通常不關(guān)心這些層。這屬于應(yīng)用程序 shell 或編排應(yīng)用程序的關(guān)注范圍 - 而不是域應(yīng)用程序。
然后,CSS 將被替換為 Tailwind 中已指定的類:
?<div className="bg-red-600 text-white flex flex-1 justify-center items-center">Hello from Red!</div>
您將在引用的演示存儲(chǔ)庫(kù)中找到兩個(gè)沖突的微前端的示例,網(wǎng)址為solutions/tailwind。
比較
迄今為止提出的幾乎每種方法都是微前端的可行競(jìng)爭(zhēng)者。一般來(lái)說(shuō),這些溶液也可以混合。一個(gè)微前端可以采用影子 DOM 方法,而另一個(gè)微前端則對(duì) Emotion 感到滿意。第三個(gè)圖書館可能會(huì)選擇使用香草精。
最后,唯一重要的是所選擇的解決方案是無(wú)碰撞的,并且不會(huì)帶來(lái)(巨大的)運(yùn)行時(shí)成本。雖然某些方法比其他方法更有效,但它們都提供了所需的樣式隔離。
方法遷移工作可讀性穩(wěn)健性性能影響習(xí)俗中等的高的低的沒(méi)有任何CSS 模塊低的高的中等的無(wú)到低影子 DOM低到中高的高的低的JS 中的 CSS高的中到高高的無(wú)到高順風(fēng)高的中等的高的沒(méi)有任何
性能影響很大程度上取決于實(shí)施。例如,對(duì)于 CSS-in-JS,如果解析和組合在運(yùn)行時(shí)完全完成,您可能會(huì)產(chǎn)生很大的影響。如果樣式已經(jīng)預(yù)先解析但僅在運(yùn)行時(shí)組合,則影響可能很小。如果使用像香草精這樣的解決方案,您基本上不會(huì)產(chǎn)生任何影響。
對(duì)于 Shadow DOM,主要的性能影響可能是 Shadow DOM 內(nèi)部元素的投影或移動(dòng)(本質(zhì)上為零)以及標(biāo)簽的重新評(píng)估style。然而,這是相當(dāng)?shù)偷?,甚至可能?huì)產(chǎn)生一些性能優(yōu)勢(shì),因?yàn)榻o定的樣式總是切中要害,并且僅專用于要在影子 DOM 中顯示的某個(gè)組件。
在示例中,我們有以下捆綁包大小:
方法索引 [kB]頁(yè)碼 [kB]張數(shù) [kB]總體 [kB]尺寸 [%]默認(rèn)1.7191.2030.2453.167100%習(xí)俗1.7611.2410.2693.271103%CSS 模塊2.1492.39404.543143%影子 DOM10.0441.264011.308357%情感1.6701.63225.78529.087918%樣式組件1.6181.61263.07366.3032093%香草精1.8001.2570.3143.371106%順風(fēng)1.8531.2470.7143.814120%
對(duì)這些數(shù)字持保留態(tài)度,因?yàn)樵谇楦泻蜆邮浇M件的情況下,運(yùn)行時(shí)可以(并且可能甚至應(yīng)該)共享。另外,給出的示例微前端確實(shí)很?。ㄋ?UI 片段的總體大小為 3kB)。對(duì)于更大的微前端,增長(zhǎng)肯定不會(huì)像這里描述的那么問(wèn)題。
Shadow DOM 解決方案的大小增加可以通過(guò)我們提供的簡(jiǎn)單實(shí)用腳本來(lái)解釋,該腳本可以輕松地將現(xiàn)有的 React 渲染包裝到 Shadow DOM 中(無(wú)需生成新樹)。如果這樣的實(shí)用程序是集中共享的,那么其大小將更接近其他更輕量級(jí)的解決方案。
結(jié)論
在微前端解決方案中處理 CSS 并不困難 - 只需從一開始就以結(jié)構(gòu)化和有序的方式完成,否則就會(huì)出現(xiàn)沖突和問(wèn)題。一般來(lái)說(shuō),建議選擇 CSS 模塊、Tailwind 或可擴(kuò)展的 CSS-in-JS 實(shí)現(xiàn)等解決方案。