重新構(gòu)想原子化 CSS
感謝印記中文的 QC-L[1] 對(duì)本文進(jìn)行翻譯,英文原文: English Version[2]。
本文會(huì)比往期文章相對(duì)長(zhǎng)些。這是我個(gè)人的一個(gè)重大的工具發(fā)布,有許多內(nèi)容我想談?wù)摗H绻隳芑ㄐr(shí)間讀完,不勝感激,希望能對(duì)你有所幫助 :)
推薦訪問?https://antfu.me/posts/reimagine-atomic-css-zh 以獲得最好的閱讀體驗(yàn)
什么是原子化 CSS?
首先,讓我們?yōu)?原子化 CSS (Atomic CSS) 給出適當(dāng)?shù)亩x:
John Polacek 在 文章 Let’s Define Exactly What Atomic CSS is[3] 中寫道:
Atomic CSS is the approach to CSS architecture that favors small, single-purpose classes with names based on visual function.
譯文:
原子化 CSS 是一種 CSS 的架構(gòu)方式,它傾向于小巧且用途單一的 class,并且會(huì)以視覺效果進(jìn)行命名。
有些人可能會(huì)稱其為函數(shù)式 CSS,或者 CSS 實(shí)用工具。本質(zhì)上,你可以將原子化的 CSS 框架理解為這類 CSS 的統(tǒng)稱:
.m-0?{
??margin:?0;
}
.text-red?{
??color:?red;
}
/*?...?*/
市面上有不少實(shí)用至上的 CSS 框架,如 Tailwind CSS[4],Windi CSS[5] 以及 Tachyons[6] 等。
同時(shí)有些 UI 庫(kù)也會(huì)附帶一些 CSS 工具類作為框架的補(bǔ)充,如 Bootstrap[7] 和 Chakra UI[8]。
這篇文章并不打算和你討論使用原子化 CSS 的優(yōu)缺點(diǎn),相信你己經(jīng)在各種地方聽到了許多討論。今天我們將從框架作者的角度來(lái)聊聊如何權(quán)衡設(shè)計(jì)以構(gòu)建出那些你喜歡的框架,它們的局限性,以及如何能將它們做得更好以使得我們的日常工作受惠。
背景
在正式開始前,先來(lái)聊聊背景。如果你還不認(rèn)識(shí)我,我叫 Anthony Fu,是 Vite[9] 團(tuán)隊(duì)的成員,也是 Vitesse[10] (Vite 社區(qū)最受歡迎的起手模板之一) 的作者。我享受原子化 CSS 帶來(lái)的快速開發(fā)體驗(yàn),而因此選擇了 Tailwind CSS[11] 作為 Vitesse 的默認(rèn) UI 框架。雖然 Vite 較 Webpack 等工具相比,在加載速度上有了大幅提升,但由于 Tailwind 生成了數(shù) MB 的 CSS,使得加載與更新 CSS 成為了整個(gè) Vite 應(yīng)用的性能瓶頸。我曾以為這是使用為了原子式 CSS 的一種權(quán)衡,直到我發(fā)現(xiàn)了 Windi CSS[12]。

Windi CSS[13] 是從零開始編寫的 Tailwind CSS 的替代方案。它的零依賴,也不要求用戶安裝 PostCSS 和 Autoprefixer。更為重要的是,它支持 按需生成。Windi CSS 不會(huì)一次生成所有的 CSS,而是只會(huì)生成你在代碼中實(shí)際使用到的原子化 CSS。這與 Vite 按需使用的理念不謀而合,也因此,我為它編寫了 一個(gè) Vite 插件[14]。不出所料,從一個(gè)簡(jiǎn)單的測(cè)試上可以看到它比 Tailwind 要快了 20~100 倍[15]。
項(xiàng)目進(jìn)展相當(dāng)順利,Windi CSS 也快速成長(zhǎng)為一個(gè)團(tuán)隊(duì),我們也引入了許多創(chuàng)新,如 自動(dòng)值推導(dǎo)[16],可變修飾組[17],Shortcuts[18],在 DevTools 中進(jìn)行設(shè)計(jì)[19],屬性化模式[20] 等。作為結(jié)果,Tailwind 也 因此[21] 使用了同樣的技術(shù)并推出了自己的 JIT 按需引擎[22]。
剖析原子化 CSS
在文章開始前,我們來(lái)聊聊原子化 CSS 的工作原理。
傳統(tǒng)方案
制作原子化 CSS 的傳統(tǒng)方案其實(shí)就是提供所有你可能需要用到的 CSS 工具。例如,你可能會(huì)用預(yù)處理器(這里選用的是 SCSS)生成如下代碼:
// style.scss
@for $i from 1 through 10 {
.m-#{$i} {
margin: $i / 4 rem;
}
}
編譯結(jié)果為:
.m-1?{?margin:?0.25?rem;?}
.m-2?{?margin:?0.5?rem;?}
/*?...?*/
.m-10?{?margin:?2.5?rem;?}
現(xiàn)在你可以直接使用 class="m-1" 來(lái)設(shè)置邊距。但正如你所見,用這種方法的情況下,你不能使用除了 1 到 10 之外的邊距,而且,即使你只使用了其中一條 CSS 規(guī)則,但還是要為其余幾條規(guī)則的文件體積買單。如果之后你還想支持不同的 margin 方向,使用比如 mt 代表 margin-top,mb 代表 margin-bottom 等,加上這 4 個(gè)方向以后,你的 CSS 大小會(huì)變成原來(lái)的 5 倍。如果再有使用到像 :hover 和 :focus 這樣的偽類時(shí),體積還會(huì)得更變大。以此類推,每多加一個(gè)工具類,往往意味著你 CSS 文件的大小也會(huì)隨之增加。這也就是為什么傳統(tǒng)的 Tailwind 生成的 CSS 文件會(huì)有數(shù) MB 的大小。
為了解決這個(gè)問題,Tailwind 通過(guò)使用 PurgeCSS[23] 來(lái)掃描你的大包產(chǎn)物并刪除你不需要的規(guī)則。這得以使其在生產(chǎn)環(huán)境中 CSS 文件縮減為幾 KB。然而,請(qǐng)注意,這個(gè)清除操作僅在生成構(gòu)建下有效,而開發(fā)環(huán)境下仍要使用包含了所有規(guī)則巨大的 CSS 文件。這在 Webpack 中表現(xiàn)可能并不明顯,但在 Vite 中卻有著巨大的影響,畢竟其他內(nèi)容的加載都非常迅捷。
既然生成再清除的方法存在局限性,那是否有更好的解決方案?
按需生成
"按需生成" 的想法引入了一種全新的思維方式。讓我們先來(lái)對(duì)比下這些方案:

傳統(tǒng)的方式不僅會(huì)消耗不必要的資源(生成了但未使用),甚至有時(shí)更是無(wú)法滿足你的需求,因?yàn)榭倳?huì)有部分需求無(wú)法包含在內(nèi)。

通過(guò)調(diào)換 "生成" 和 "掃描" 的順序,"按需" 會(huì)為你節(jié)省浪費(fèi)的計(jì)算開銷和傳輸成本,同時(shí)可以靈活地實(shí)現(xiàn)預(yù)生成無(wú)法實(shí)現(xiàn)的動(dòng)態(tài)需求。另外,這種方法可以同時(shí)在開發(fā)和生產(chǎn)中使用,提供了一致的開發(fā)體驗(yàn),使得 HMR (Hot Module Replacement, 熱更新) 更加高效。
為了實(shí)現(xiàn)這一點(diǎn),Windi CSS 和 Tailwind JIT 都采用了預(yù)先掃描源代碼的方式。下面是一個(gè)簡(jiǎn)單示例:
import?glob?from?'fast-glob'
import?{?promises?as?fs?}?from?'fs'
//?通常這個(gè)是可以配置的
const?include?=?['src/**/*.{jsx,tsx,vue,html}']
async?function?scan()?{
??const?files?=?await?glob(include)
??for?(const?file?of?files)?{
????const?content?=?await?fs.readFile(file,?'utf8')
????//?將文件內(nèi)容傳遞給生成器并配對(duì)?class?的使用情況
??}
}
await?scan()
//?掃描會(huì)在構(gòu)建/服務(wù)器啟動(dòng)前完成
await?buildOrStartDevServer()
為了在開發(fā)期間提供 HMR,通常會(huì)啟動(dòng)一個(gè) 文件系統(tǒng)監(jiān)聽器[24]:
import?chokidar?from?'chokidar'
chokidar.watch(include).on('change',?(event,?path)?=>?{
??//?重新讀取文件
??const?content?=?await?fs.readFile(file,?'utf8')
??//?將新的內(nèi)容重新傳遞給生成器
??//?清除?CSS?模塊的緩存并觸發(fā)?HMR?事件
})
因此,通過(guò)按需生成方式,Windi CSS 獲得了比傳統(tǒng)的 Tailwind CSS 快 100 倍左右[25] 的性能。
痛癢
我現(xiàn)在在我?guī)缀跛械膽?yīng)用中都在使用 Windi CSS,而且效果顯著,性能優(yōu)異,HMR 瞬間完成幾乎無(wú)法察覺。自動(dòng)值推導(dǎo)[26] 和 屬性化模式[27] 更是提升了我的開發(fā)體驗(yàn)。到這里,我本該可以睡上一個(gè)好覺去想想其他事情了,但是有時(shí)候,它還是會(huì)瘙你癢癢打擾你的美夢(mèng)。
我發(fā)現(xiàn)最令人討厭的是,和很多時(shí)候我不清楚我得到的結(jié)果是什么,以及怎么樣做才能讓它生效。對(duì)我而言,最好最理想的原子化 CSS 應(yīng)該是直覺性的。一旦學(xué)會(huì),它應(yīng)該非常直觀易懂,并且可以推導(dǎo)出其他已知情況。如果你知道 mt-1 是上邊距的一倍單位,你就會(huì)直覺地認(rèn)為 mb-2 是下邊距的兩倍單位。當(dāng)它正常工作時(shí),是直覺使然,但當(dāng)它不起作用時(shí),會(huì)令人沮喪和困惑。
例如,我們知道 Tailwind 中的 border-2 標(biāo)識(shí)邊框?qū)挾葹?2px,4 表示 4px,6 表示 6px,8 表示 8px,但當(dāng)你使用 border-10 卻不起作用(你甚至需要一些時(shí)間來(lái)發(fā)現(xiàn)這件事!)。你可能會(huì)說(shuō)這是故意而為之,以使得設(shè)計(jì)系統(tǒng)具有一致性。不如這樣,我們來(lái)做個(gè)小測(cè)驗(yàn),如果想要讓 border-10 正常工作,應(yīng)該如何做?
在你的全局樣式中的某個(gè)位置添加這樣一個(gè)工具類?
.border-10?{
??border-width:?10px;
}
快速且直觀,最重要的是,它的確解決了你的需求。但是,如果什么都需要我自己手動(dòng)添加,那我們?yōu)槭裁催€需要使用 Tailwind ?
如果你對(duì) Tailwind 了解深入一些,那你可能知道它可以進(jìn)行額外配置。所以你需要花 5 分鐘,檢索他們的文檔。你將得到如下方案[28]:
//?tailwind.config.js
module.exports?=?{
??theme:?{
????borderWidth:?{
??????DEFAULT:?'1px',
??????'0':?'0',
??????'2':?'2px',
??????'3':?'3px',
??????'4':?'4px',
??????'6':?'6px',
??????'8':?'8px',
??????'10':?'10px'?//?<--?here
????}
??}
}
這似乎很合理,我可以把我需要的情況都列出來(lái),回去繼續(xù)工作了...等一下,我剛剛進(jìn)行到哪里了?因?yàn)檫@樣一個(gè)工具的丟失而被打斷,除了配置,我們還會(huì)需要時(shí)間重新找回原本正在進(jìn)行的工作的上下文。接著,如果我想設(shè)置邊框顏色,我還需要查詢文檔,然后如何進(jìn)行配置。也許有人喜歡這樣的工作流程,但這并不適合我,我并不享受被本該直覺性工作的工具打斷的我的工作流程。
Windi CSS 對(duì)規(guī)則相對(duì)寬松一些,會(huì)盡可能地根據(jù)你使用的 class 提供相應(yīng)的實(shí)用工具類。在這種情況下,border-10 在 Windi 中可以做到開箱即用。但是,由于 Windi 需要與 Tailwind 兼容,它還必須使用與 Tailwind 完全相同的配置項(xiàng)。盡管數(shù)字推斷的問題得到了解決,但如果你想添加一些自定義的工具,這將是一場(chǎng)噩夢(mèng)。下面是一個(gè)來(lái)自 Tailwind 文檔[29] 的示例:
//?tailwind.config.js
const?_?=?require('lodash')
const?plugin?=?require('tailwindcss/plugin')
module.exports?=?{
??theme:?{
????rotate:?{
??????'1/4':?'90deg',
??????'1/2':?'180deg',
??????'3/4':?'270deg',
????}
??},
??plugins:?[
????plugin(function({?addUtilities,?theme,?e?})?{
??????const?rotateUtilities?=?_.map(theme('rotate'),?(value,?key)?=>?{
????????return?{
??????????[`.${e(`rotate-${key}`)}`]:?{
????????????transform:?`rotate(${value})`
??????????}
????????}
??????})
??????addUtilities(rotateUtilities)
????})
??]
}
將產(chǎn)生如下代碼:
.rotate-1\/4?{
??transform:?rotate(90deg);
}
.rotate-1\/2?{
??transform:?rotate(180deg);
}
.rotate-3\/4?{
??transform:?rotate(270deg);
}
產(chǎn)生的 CSS 代碼甚至比結(jié)果還要長(zhǎng)。并且難以閱讀和維護(hù),同時(shí),它破壞了按需應(yīng)變的能力。
Tailwind 的 API 和插件系統(tǒng)沿用了舊的思維方式進(jìn)行設(shè)計(jì),并不能適應(yīng)新的按需方式。其核心工具是在生成器中鍛造出來(lái)的,而且其定制化功能相當(dāng)有限。因此,我開始思考,如果我們可以放棄這些歷史包袱,并以隨需應(yīng)變思想重新設(shè)計(jì)它,我們可以得到什么?
向你介紹 UnoCSS
**UnoCSS**[30] - 具有高性能且極具靈活性的即時(shí)原子化 CSS 引擎。
該項(xiàng)目誕生于我在國(guó)慶期間的做的一些隨機(jī)實(shí)驗(yàn)。從使用者的角度出發(fā)去探索靈活性和直觀性的最佳平衡,加上按需生成的思想,這些實(shí)驗(yàn)的最終結(jié)果在不少方面甚至超出了我的預(yù)期。接下來(lái)讓我為你逐一介紹:
引擎
UnoCSS 是一個(gè)引擎,而非一款框架,因?yàn)樗?strong style="color: rgb(149, 151, 21);">并未提供核心工具類,所有功能可以通過(guò)預(yù)設(shè)和內(nèi)聯(lián)配置提供。
我們?cè)O(shè)想 UnoCSS 能夠通過(guò)預(yù)設(shè)模擬大多數(shù)已有原子化 CSS 框架的功能。也有可能會(huì)被用作創(chuàng)建一些新的原子化 CSS 框架的引擎。例如:
import?UnocssPlugin?from?'@unocss/vite'
//?以下預(yù)設(shè)目前還不存在,
//?歡迎大家踴躍貢獻(xiàn)!
import?PresetTachyons?from?'@unocss/preset-tachyons'
import?PresetBootstrap?from?'@unocss/preset-bootstrap'
import?PresetTailwind?from?'@unocss/preset-tailwind'
import?PresetWindi?from?'@unocss/preset-windi'
import?PresetAntfu?from?'@antfu/oh-my-cool-unocss-preset'
export?default?{
??plugins:?[
????UnocssPlugin({
??????presets:?[
????????//?PresetTachyons,
????????PresetBootstrap,
????????//?PresetTailwind,
????????//?PresetWindi,
????????//?PresetAntfu
????????//?選擇其中一個(gè)...或多個(gè)!
??????]
????})
??]
}
讓我們來(lái)看看如何使它們成為可能:
直觀且完全可定制
UnoCSS 的主要目標(biāo)是直觀性和可定制性。它可以讓你在數(shù)十秒內(nèi),定義你自己的 CSS 工具。
靜態(tài)規(guī)則
原子化 CSS 可能數(shù)量相當(dāng)龐大。因此,規(guī)則定義直接了當(dāng)對(duì)于閱讀和維護(hù)非常重要。如需為 UnoCSS 創(chuàng)建一個(gè)自定義規(guī)則,你可以這樣寫:
rules:?[
??['m-1',?{?margin:?'0.25rem'?}]
]
當(dāng)在用戶代碼庫(kù)中檢測(cè)到 m-1 時(shí),就會(huì)產(chǎn)生如下 CSS:
.m-1?{?margin:?0.25rem;?}
動(dòng)態(tài)規(guī)則
想要使其動(dòng)態(tài)化,可以將匹配器修改為正則表達(dá)式,將主體改為一個(gè)函數(shù):
rules:?[
??[/^m-(\d)$/,?([,?d])?=>?({?margin:?`${d?/?4}rem`?})],
??[/^p-(\d)$/,?(match)?=>?({?padding:?`${match[1]?/?4}rem`?})],
]
其中,主題函數(shù)的第一個(gè)參數(shù)為匹配結(jié)果,所以你可以對(duì)它進(jìn)行解構(gòu)以獲得正則表達(dá)式的匹配組。
例如,當(dāng)你使用:
<div?class="m-100">
??<button?class="m-3">
????<icon?class="p-5"?/>
????My?Button
??button>
div>
就會(huì)產(chǎn)生相應(yīng)的 CSS:
.m-100?{?margin:?25rem;?}
.m-3?{?margin:?0.75rem;?}
.p-5?{?padding:?1.25rem;?}
這樣就行了。而現(xiàn)在,你只需要使用相同的模式添加更多的實(shí)用工具類,你就擁有了屬于自己的原子化 CSS!
可變修飾
可變修飾 (Variants)[31] 在 UnoCSS 中也是簡(jiǎn)單且強(qiáng)大的。這里有幾個(gè)示例:
variants:?[
??//?支持所有規(guī)則的?`hover:`
??{
????match:?s?=>?s.startsWith('hover:')???s.slice(6)?:?null,
????selector:?s?=>?`${s}:hover`,
??},
??//?支持?`!`?前綴,使規(guī)則優(yōu)先級(jí)更高
??{
????match:?s?=>?s.startsWith('!')???s.slice(1)?:?null,
????rewrite:?(entries)?=>?{
??????//?在所有?CSS?值中添加?`?!important`
??????entries.forEach(e?=>?e[1]?+=?'?!important')
??????return?entries
????},
??}
],
你可以參考 文檔[32] 了解更多細(xì)節(jié)。
預(yù)設(shè)
你可以將自己的自定義規(guī)則和可變修飾打包成預(yù)設(shè),與他人分享,或是使用 UnoCSS 作為引擎創(chuàng)建你自己的原子化 CSS 框架!
同時(shí),我們?cè)诎l(fā)布時(shí)也提供了 一些預(yù)設(shè)[33] 供你快速上手。
值得一提的是,默認(rèn)的 `@unocss/preset-uno`[34] 預(yù)設(shè)(實(shí)驗(yàn)階段)是一系列流行的原子化框架的 通用超集,包括了 Tailwind CSS,Windi CSS,Bootstrap,Tachyons 等。
例如,ml-3(Tailwind),ms-2(Bootstrap),ma4(Tachyons),mt-10px(Windi CSS)均會(huì)生效。
.ma4?{?margin:?1rem;?}
.ml-3?{?margin-left:?0.75rem;?}
.ms-2?{?margin-inline-start:?0.5rem;?}
.mt-10px?{?margin-top:?10px;?}
了解更多關(guān)于默認(rèn)預(yù)設(shè)的信息[35]。
靈活性
截止目前為止,我們都在向你展示如何使用 UnoCSS 來(lái)模仿 Tailwind 和其他原子化框架的行為,即便 UnoCSS 讓這件事變得十分容易,但僅此一點(diǎn)可能也不會(huì)在最終使用者的方面產(chǎn)生太大影響。
一起來(lái)見識(shí)下 UnoCSS 真正的威力:
屬性化模式
屬性化模式 (Attributify Mode)[36] 是 Windi CSS 最受歡迎的特性之一。它能幫助你通過(guò)使用屬性更好地組織和分組你的實(shí)用工具類。
它會(huì)把你的冗長(zhǎng)的 Tailwind 代碼(難以閱讀與編輯):
<button?class="bg-blue-400?hover:bg-blue-500?text-sm?text-white?font-mono?font-light?py-2?px-4?rounded?border-2?border-blue-200?dark:bg-blue-500?dark:hover:bg-blue-600">
??Button
button>
變成:
<button?
??bg="blue-400?hover:blue-500?dark:blue-500?dark:hover:blue-600"
??text="sm?white"
??font="mono?light"
??p="y-2?x-4"
??border="2?rounded?blue-200"
>
??Button
button>
在更好的按類型進(jìn)行組織的同時(shí),也節(jié)省了重復(fù)輸入相同前綴的時(shí)間。
在 UnoCSS 中,我們也實(shí)現(xiàn)了屬性化模式,只使用 **一個(gè)可變修飾**[37] 和 **一個(gè)提取器**[38],總共 代碼行數(shù)不超過(guò) 100!更重要的是,它直接適用于你自定義的任何規(guī)則!
除了 Windi CSS 的屬性化模式,僅需改動(dòng)幾行代碼,我們還實(shí)現(xiàn)了無(wú)值的屬性的支持:
<div?class="m-2?rounded?text-teal-400"?/>
現(xiàn)在變?yōu)椋?/p>
<div?m-2?rounded?text-teal-400?/>
整個(gè)屬性化模式是通過(guò) `@unocss/preset-attributify`[39] 預(yù)設(shè)提供的,詳細(xì)的使用方法請(qǐng)參考其文檔。
純 CSS 圖標(biāo)
如果你讀過(guò)我之前的文章 Journey with Icons Continues[40],你一定知道我對(duì)圖標(biāo)非常熱衷,并且在積極研究圖標(biāo)的各種解決方案。這次,憑借 UnoCSS 的靈活性,我們甚至可以擁有純 CSS 的圖標(biāo)。你沒看錯(cuò),它是純 CSS 的圖標(biāo),不需要任何 JavaScript!讓我們來(lái)看看它長(zhǎng)什么樣子:
<div?class="i-ph-anchor-simple-thin"?/>
<div?class="i-mdi-alarm?text-orange-400?hover:text-teal-400"?/>
<div?class="i-logos-vue?text-3xl"?/>
<button?class="i-carbon-sun?dark:i-carbon-moon"?/>
<div?class="i-twemoji-grinning-face-with-smiling-eyes?hover:i-twemoji-face-with-tears-of-joy"?/>

與可變修飾結(jié)合,你甚至可以根據(jù)懸停狀態(tài)或顏色模式來(lái)切換圖標(biāo)。得益于 Iconify[41] 項(xiàng)目,你可以從一百余個(gè)熱門圖標(biāo)集合(Material Design Icons, Ant Design Icons 等等)中獲得 超過(guò)一萬(wàn)個(gè)圖標(biāo) 供你按需使用。
同樣的,這個(gè)功能的實(shí)現(xiàn)代碼并未超過(guò) 100 行。具體請(qǐng)參考 `@unocss/preset-icons`[42] 預(yù)設(shè)的實(shí)現(xiàn)了解其中的魔法。
希望這些預(yù)設(shè)可以讓你對(duì) UnoCSS 的靈活性有一個(gè)大致的了解。它還處于一個(gè)非常早期的階段,有很多可能性等待我們?nèi)ヌ剿鳌?/p>
CSS 作用域
在使用 Tailwind / Windi 時(shí),我遇到的另一個(gè)問題就是 樣式預(yù)檢 (Preflight)[43]。預(yù)檢功能重置了原生元素,并為 CSS 變量提供了一些兜底方案,在開發(fā)一個(gè)只使用 Tailwind/Windi 的新應(yīng)用時(shí),效果很棒。但當(dāng)你想讓它們與其他 UI 框架一起工作,或者使用 Tailwind 編寫一些共享組件時(shí),預(yù)檢往往會(huì)引入許多沖突,破壞你現(xiàn)有的 UI。
因此,UnoCSS 采取了另一個(gè)霸氣的操作,不支持預(yù)檢。相反,它將 CSS 重置的控制權(quán)完全留給了用戶 (或 UnoCSS 上層框架),讓他們使用適合自己的方案 - Normalize.css,Reset.css 或者 UI 框架自帶的CSS 重置等。
這也使得 UnoCSS 在 CSS 作用域上有了更多可能性。例如,我們?cè)?Vite 插件上有一個(gè)實(shí)驗(yàn)性的 scoped-vue 模式,可以為每個(gè)組件生成作用域樣式,你可以安全地使用原子化 CSS 作為組件庫(kù),而無(wú)需擔(dān)心與用戶的 CSS 發(fā)生沖突。比如:
<template>
??<div?class="m-2?rounded"><slot>div>
<template>
<style?scoped>
.m-2{margin:0.5rem;}
.rounded{border-radius:0.25rem;}
style>
我們還在嘗試更多的可能性,比如支持 Web Component,MPA 情況下的 CSS 代碼分割,以及模塊級(jí)別的 CSS 作用域等。
性能
考慮到 UnoCSS 帶來(lái)的靈活性和想象力,坦率地說(shuō),我認(rèn)為性能可能不是那么重要的事情。出于好奇,我寫了一個(gè) 簡(jiǎn)單的 benchmark[44] 來(lái)比較性能。結(jié)果令人驚訝:
10/21/2021,?2:17:45?PM
1656?utilities?|?x50?runs
none????????????????????????????8.75?ms?/????0.00?ms?
unocss???????v0.0.0????????????13.72?ms?/????4.97?ms?(x1.00)
windicss?????v3.1.9???????????980.41?ms?/??971.66?ms?(x195.36)
tailwindcss??v3.0.0-alpha.1??1258.54?ms?/?1249.79?ms?(x251.28)
從結(jié)果來(lái)看,UnoCSS 可以比 Tailwind 的 JIT 引擎快 200 倍!說(shuō)實(shí)話,在按需生成的情況下,Windi 和 Tailwind JIT 都已經(jīng)算是超快了,UnoCSS 的性能提升感知度可能沒有那么高。然而,幾乎為零的開銷意味著你可以將 UnoCSS 整合到你現(xiàn)有的項(xiàng)目中,作為一個(gè)增量解決方案與其他框架一同協(xié)作,而不需要擔(dān)心性能損耗。
在開發(fā)時(shí),UnoCSS 做了很多性能上的優(yōu)化。如果你感興趣,可以參考:
跳過(guò)解析,不使用 AST
從內(nèi)部實(shí)現(xiàn)上看,Tailwind 依賴于 PostCSS 的 AST 進(jìn)行修改,而 Windi 則是編寫了一個(gè)自定義解析器和 AST。考慮到在開發(fā)過(guò)程中,這些工具 CSS 的并不經(jīng)常變化,UnoCSS 通過(guò)非常高效的字符串拼接來(lái)直接生成對(duì)應(yīng)的 CSS 而非引入整個(gè)編譯過(guò)程。同時(shí),UnoCSS 對(duì)類名和生成的 CSS 字符串進(jìn)行了緩存,當(dāng)再次遇到相同的實(shí)用工具類時(shí),它可以繞過(guò)整個(gè)匹配和生成的過(guò)程。
單次迭代
正如前文所述,Windi CSS 和 Tailwind JIT 都依賴于對(duì)文件系統(tǒng)的預(yù)掃描,并使用文件系統(tǒng)監(jiān)聽器來(lái)實(shí)現(xiàn) HMR。文件 I/O 不可避免地會(huì)引入開銷,而你的構(gòu)建工具實(shí)際上需要再次加載它們。那么我們?yōu)槭裁床恢苯永靡呀?jīng)被工具讀取過(guò)的內(nèi)容呢?
除了獨(dú)立的生成器核心以外,UnoCSS 有意只提供了 Vite 插件(以后可能考慮其他的集成),這使得它能夠?qū)W⒂谂c Vite 的最佳集成。
在 Vite 中,transform 的鉤子將與所有的文件及其內(nèi)容一起被迭代。因此,我們可以寫一個(gè)插件來(lái)收集它們,比如:
export?default?{
??plugins:?[
????{
??????name:?'unocss',
??????transform(code,?id)?{
????????//?過(guò)濾掉無(wú)需掃描的文件
????????if?(!filter(id))?return
????????//?掃描代碼(同時(shí)也可以處理開發(fā)中的無(wú)效內(nèi)容)
????????scan(code,?id)
????????//?我們只需要內(nèi)容,所以不需要對(duì)代碼進(jìn)行轉(zhuǎn)換
????????return?null
??????},
??????resolveId(id)?{
????????return?id?===?VIRTUAL_CSS_ID???id?:?null
??????},
??????async?load(id)?{
????????//?生成的?css?會(huì)作為一個(gè)虛擬模塊供后續(xù)使用
????????if?(id?===?VIRTUAL_CSS_ID)?{
??????????return?{?code:?await?generate()?}
????????}
??????}
????}
??]
}
由于 Vite 也會(huì)處理 HMR,并在文件變化時(shí)再次執(zhí)行 transform 鉤子,這使得 UnoCSS 可以在一次加載中就完成所有的工作,沒有重復(fù)的文件 I/O 和文件系統(tǒng)監(jiān)聽器。此外,通過(guò)這種方式,掃描會(huì)依賴于模塊圖而非文件 glob。這意味著只有構(gòu)建在你應(yīng)用程序中的模塊才會(huì)影響生成的 CSS,而并非你文件夾下的任何文件。
我們還做了一些小技巧來(lái)提高性能。我可能會(huì)在以后再寫一篇關(guān)于它們的文章,但在此之前,你可以通過(guò)閱讀代碼來(lái)弄清它們 :)
現(xiàn)在能用嗎?
簡(jiǎn)而言之:可以,但有注意事項(xiàng)。
UnoCSS 仍處于實(shí)驗(yàn)階段,但由于其精簡(jiǎn)的設(shè)計(jì),生成的結(jié)果已經(jīng)非常可靠了。需要注意的一點(diǎn)是,API 還沒有最終定案,雖然我們會(huì)遵循 semver 的進(jìn)行版本發(fā)布,但是還請(qǐng)為破壞性改動(dòng)做好準(zhǔn)備。
注意:它并非被設(shè)計(jì)成 Windi CSS 或 Tailwind 的替代品(考慮等待 Windi CSS v4)。我們不建議將現(xiàn)有項(xiàng)目完全遷移到 UnoCSS。你可以在新的項(xiàng)目中試用它,或者將它作為你現(xiàn)有 CSS 框架的補(bǔ)充(例如,禁用默認(rèn)預(yù)設(shè),只使用純 CSS 圖標(biāo)的預(yù)設(shè),或者自定義你的規(guī)則)。
順便說(shuō)一句,目前 你正在閱讀的網(wǎng)站[45] 就構(gòu)建于 UnoCSS 之上,供你參考 :P。
同時(shí),歡迎分享你正在制作的預(yù)設(shè)或幫助我們貢獻(xiàn)默認(rèn)的預(yù)設(shè)。期待看到你能夠蹦出什么新想法!
關(guān)于 Windi CSS?
作為 Windi CSS 的團(tuán)隊(duì)成員,我與 Windi CSS 的創(chuàng)作者 Voorjaar[46] 緊密合作。你可以認(rèn)為 UnoCSS 是 Windi CSS 團(tuán)隊(duì)的一個(gè)激進(jìn)的實(shí)驗(yàn),如果進(jìn)展順利,它可能成為 Windi CSS v4 的新引擎。
Windi CSS 作為一個(gè)框架,將填補(bǔ) UnoCSS 作為一個(gè)引擎有意不提供的 @apply 預(yù)處理,IDE 智能提示,預(yù)處理等功能的缺失。而且它還將利用 UnoCSS 為核心工具為用戶配置提供高性能和靈活性。
在我們?yōu)?Windi v4 嵌入新引擎之前,一個(gè)使用 UnoCSS 作為 Windi CSS 擴(kuò)展的 npm 包(例如,擁有純 CSS 圖標(biāo))將很快發(fā)布。敬請(qǐng)關(guān)注 :)
結(jié)束語(yǔ)
非常感謝你的閱讀!如果你對(duì)它感興趣,請(qǐng)記得查看 `antfu/unocss`[47] 倉(cāng)庫(kù)以了解更多細(xì)節(jié),也可以通過(guò) **在線 Playground**[48] 進(jìn)行嘗試。
歡迎評(píng)論或轉(zhuǎn)發(fā) 此推文[49],讓我知道你的想法!??
參考資料
QC-L: https://github.com/QC-L
[2]English Version: /posts/reimagine-atomic-css
[3]文章 Let’s Define Exactly What Atomic CSS is: https://css-tricks.com/lets-define-exactly-atomic-css/
[4]Tailwind CSS: https://tailwindcss.com/
[5]Windi CSS: https://cn.windicss.org/
[6]Tachyons: https://tachyons.io/
[7]Bootstrap: https://getbootstrap.com/docs/5.1/utilities/api/
[8]Chakra UI: https://chakra-ui.com/docs/features/style-props
[9]Vite: https://vitejs.dev/
[10]Vitesse: https://github.com/antfu/vitesse
[11]Tailwind CSS: https://tailwindcss.com/
[12]Windi CSS: https://cn.windicss.org
[13]Windi CSS: https://cn.windicss.org
[14]一個(gè) Vite 插件: https://github.com/windicss/vite-plugin-windicss
[15]20~100 倍: https://twitter.com/antfu7/status/1361398324587163648
[16]自動(dòng)值推導(dǎo): https://cn.windicss.org/features/value-auto-infer.html
[17]可變修飾組: https://windicss.org/features/variant-groups.html
[18]Shortcuts: https://windicss.org/features/shortcuts.html
[19]在 DevTools 中進(jìn)行設(shè)計(jì): https://twitter.com/antfu7/status/1372244287975387145
[20]屬性化模式: https://twitter.com/windi_css/status/1387460661135896577
[21]因此: https://twitter.com/adamwathan/status/1371542711086559237?s=20
[22]JIT 按需引擎: https://tailwindcss.com/docs/just-in-time-mode
[23]PurgeCSS: https://purgecss.com/
[24]文件系統(tǒng)監(jiān)聽器: https://github.com/paulmillr/chokidar
[25]快 100 倍左右: https://twitter.com/antfu7/status/1361398324587163648
[26]自動(dòng)值推導(dǎo): https://cn.windicss.org/features/value-auto-infer.html
[27]屬性化模式: https://twitter.com/windi_css/status/1387460661135896577
[28]如下方案: https://tailwindcss.com/docs/border-width#border-widths
[29]Tailwind 文檔: https://tailwindcss.com/docs/plugins#escaping-class-names
[30]UnoCSS: https://github.com/antfu/unocss
[31]可變修飾 (Variants): https://cn.windicss.org/utilities/variants.html#variants
[32]文檔: https://github.com/antfu/unocss#custom-variants
[33]一些預(yù)設(shè): https://github.com/antfu/unocss#presets
[34]@unocss/preset-uno: https://github.com/antfu/unocss/tree/main/packages/preset-uno
了解更多關(guān)于默認(rèn)預(yù)設(shè)的信息: https://github.com/antfu/unocss/tree/main/packages/preset-uno
[36]屬性化模式 (Attributify Mode): https://windicss.org/posts/v30.html#attributify-mode
[37]一個(gè)可變修飾: https://github.com/antfu/unocss/blob/main/packages/preset-attributify/src/variant.ts
[38]一個(gè)提取器: https://github.com/antfu/unocss/blob/main/packages/preset-attributify/src/extractor.ts
[39]@unocss/preset-attributify: https://github.com/antfu/unocss/blob/main/packages/preset-attributify
Journey with Icons Continues: /posts/journey-with-icons-continues
[41]Iconify: https://iconify.design/
[42]@unocss/preset-icons: https://github.com/antfu/unocss/blob/main/packages/preset-icons
樣式預(yù)檢 (Preflight): https://tailwindcss.com/docs/preflight
[44]簡(jiǎn)單的 benchmark: https://github.com/antfu/unocss/tree/main/bench
[45]你正在閱讀的網(wǎng)站: https://github.com/antfu/antfu.me
[46]Voorjaar: https://github.com/voorjaar
[47]antfu/unocss: https://github.com/antfu/unocss
在線 Playground: https://unocss.antfu.me/
[49]此推文: https://twitter.com/antfu7/status/1452802545118711812
