背景(叠个甲)
近期吹捧 CSS 原子化的方案的文章多了起来,踩一捧一的文章看多了,有时忍不住就想理论几句,但评论有限反而激起了更多的波澜
这篇文章只在掘金发,仅仅只阐述我个人对当前 CSS 各种方案的看法,想到哪里写哪里,看不下去可以不看。内容和标题具备着一定的引战意味,但我本意不是抨击任何技术,如果有不同的观点,一切以你为准!!!
流派的定义
我个人习惯把 css 的使用分成以下这些流派
- 原子类 ------ 代表有:UnoCSS,TailwindCSS
- CSS-IN-JS ------ 代表有:Emotion,Styled Components,StyleX
- Vue 派 ------ Vue 会在编译期间动态混入属性,在结合 CSS 选择器,以此达到样式隔离的目的。这种方案属于编译方案之一,但使用体验比其他方案好很多,在我这自成一派
- 编译派 ------ 泛指除了 Vue 派之外的所有在编译期间生成最终成品 CSS 的方案。比如 CSS Modules,Less,Sass,Stylus
只有明确了界限才能分情况讨论,技术和业务分别有好有坏,我们先技术再业务,不要嘴里说着 A,脑子里想着 B,不然永远都不会有结果。
原子类
原子类并不是近些年才有的,之所以火起来,我觉得还是因为 Tailwind 把使用体验做起来了的原因。
在 Tailwind 最开始有风声的时候我就已经是它的用户之一了,刚开始使用时真得是非常非常惊艳。不用思考类名,即便不看文档,靠着插件的代码提示就能快速写样式,内置的各种主题颜色边距等等,让完全不懂 UI 的我也能快速画出一个能看得过去的页面。我写过的文章之中,私下写过的众多小库、博客,那段时间都有了 Tailwind 的影子。
对这东西祛魅的起点是从我和一些大佬的聊天,以及真正的作为前端负责人把该技术全面应用到业务中开始的。大佬是指那些掏钱才能听的分享会里加微信联系到的,名字肯定是不能透露了。
结果上来说,大家普遍的看法是
- 这就是变相的把样式内联进了最终产出的 HTML 中,按照已经验证了好多年的分离原则来说,这东西不用想就知道是错的
- 全都内联进去 class 会非常的臃肿
- 原子化的规则需要遵守,有上手门槛,如果有人不遵守规则,那么原子化带来的收益就会大打折扣------即如果有人开始自己写非原子化的 CSS 代码,就会接二连三的出现更多,原子化的体积优势会变得荡然无存
更多的我也记不得了,我的想法很简单,大家都是干活的,怎么舒服怎么来,任何技术都不是完美的,只要有事大于弊端,我就是内联样式又怎么样呢?过去的思想不应该成为现在的枷锁,于是我把原子化的思想暴力的带进了日常。
原子类-深度开发使用
对于业务层来说
开发体验是一级棒的。我从零手把手带过一个刚毕业的实习生,我不让他看任何文档,配置我给你配置好,按照写 CSS 结合代码提示靠直觉写就行,他给我的反馈是,用起来很爽。对于其他干过的人来说,他们只管找封装的样式组件用,老油条了也是,丝毫没感觉到什么上手难度的问题。
配置文件
前两年 windcss
还活着,UnoCSS 的噱头也很猛,编译性能、代码提示能力、灵活度、代码美观、开发体验,都是要比 Tailwind 要强的,可能还是因为没有先发优势,再加上维护者没有 Tailwind 稳定吧,热度是有,但下载量反应一切,用的人就是没那么多。
我手上有普通的单仓库项目也有 monorepo 项目,早期 UnoCSS 在 monorepo 项目中支持很差,当然也有可能是我配置的问题,在monorepo 项目中经常出现样式代码生成不出来,或代码提示缺失的问题,这对于想把更灵活的 UnoCSS 带进项目的我来说,打击还挺大的。
主要是我意识到一个问题,我喜欢的或许不是原子化带来的性能和体积优势,我喜欢的是它可以让我不用过多思考乱七八糟的事,只想着怎么写样式就行的简单感。
体积优势
后来的时间里,忘了什么时候了,我对体积这个宣传标语有了质疑。原子类体积小的原理是,相同的类无论用了多少次,始终只生成一次,所以当项目规模越庞大,生成的 CSS 体积优势就越明显。
体积小的原理非常的简单,清晰且透明。但是大家可有想过,CSS 的体积是小了,可为了让它生效我们需要把大量的、重复的样式类名一次次,一遍遍的重复写到若干个 div 里。
html
<div class="flex justify-center items-center w-fit..."></div>
<style>
.cls { display: flex; justify-content: center; align-items: center; }
</style>
对于常用的 flex 布局等等,直接写 CSS 封装通用的样式类组装即可,这是要比原子类到处重复写体积要小的,如果封装的足够好,体积大也是必要的那种程度的大,因为抽象程度很高。
Tailwind 虽然也提供了组件工具等等,也支持封装样式工具类、样式组件,从而达到封装的目的。但是官方也说了相比之下更推荐封装样式组件来使用,而不是过度的封装很多样式组合工具,这反而失去体积优势,使其退化成原始 CSS 的模样。
对比之下可以发现,在追求完美的情况下,原子类体积优势是有但也不至于差出数量级的程度,在最差的情况下两者相同,而人,恰恰就不是完美的。
作为团队的负责人来说,我不可能天天盯着谁违反尽量使用原子类的规则了,或者是封装太多工具类。或者谁到处滥用后,打回去重写吧,你同意老板都不会同意,这还没算上老项目的技术栈。所以我们取中值,允许部分不完美的产生,那么原子类在实际使用的体积优势就得再次打个折扣。
主题系统
很多低级前端不懂主题,*以为主题就是简单的颜色变换,实际上的主题也是个系统!*比如圆角,宽高,字体,行高,阴影,等等。即便是把颜色单独拿出来说,明暗主题的底层逻辑差异是很大的,比如如果都是亮色,我们可以通过动态调整明度或亮度,或混入白色,来实现不同梯度的颜色阶层,Element 系列就这么做的。但如果是暗色也按照这种模式玩,取值就变得有讲究了,主题色的选择也得有讲究,饱和度不够在暗色下会很脏,饱和度高在亮色下会很难看。
主题系统的水并不浅,门道也有很多,我也是略知一二,想了解更多可以看 Ant Design 的博客。而 Tailwind 在做主题层面,我直白的说就是很 低级。
默认情况下,为了做到主题系统我们是需要对 Tailwind 之类的配置做非常深度的定制化,默认提供的能用但互相很独立,很难按照 UI 设计师的要求,构建成熟的、有相关的企业特色的定制化主题系统。深度定制还需要去研究它的配置文件,是写死好呢,还是封装呢,还是规则生成呢,如果后续某个规则变动了,我又是否好改呢?
大公司的主题系统一旦成型,基本就不怎么会经常变动了,小改动无伤大雅。但是如果是小公司呢,UI 三天两头的变动,设计师也产不出成熟的主题系统,还有想要各种样式批量的变来变去的,改动一旦变得频繁,对于搞这东西配置的人简直就是个灾难。不封装改动的地方太多,封装又怕封装过度。
动态主题
主题在最暴力的做法下,要想做的好看,其实往往是多套的,否则还得依赖算法。比如 Ant 就可以动态的根据值,调整所有的样式结构,结果还很美观。Element 是在编译期间做的,内部看源码其实也有些基本的梯度算法在里面。
而适合我们开发者的,要么是用 Ant 这种能反补项目需求的,要么是通过 CSS 魔改,原子类则很难在这里有所作为。真要说结合,我发现的最好的做法是,一开始就定义很多 CSS 变量,原子类不定义样式而是引用 CSS 变量,第三方全靠原生 CSS 把变量覆盖进去,这种做法我认为很扭曲,但这并不妨碍它还能玩的下去。所以 Tailwind 这类原子类的产品对结合了本项目外的东西,操作空间就小很多了,开发幸福度会小非常多。
国外现在还出现了只定义结构,样式自由填充的东西,这东西我是无福消受了,我光是业务开发就已经很累了,你还让我开发前再给你完善下 UI 库样式,我会疯掉的。
库开发
我写过很多库,还为它们写过若干篇文章,挖了不少坑,承诺过许多,(虽然很多都不维护了,因为我有了更好的想法和选择,有些已经是公司项目的主力了,但搞开源太耗费精力还没什么下载量,我也是累的没心思了=口=)
说会到正题,比如我要封装一个 <Button/>
组件,我需要有以下参数的效果
-
hover
鼠标移入给内部文字和图标都高亮 -
active
鼠标按下内部文字和图标一个深色 -
readonly
只读状态,保留所有样式效果,但屏蔽掉所有事件,再加个鼠标不让点的样式,优先级高于disabled
-
disabled
禁用状态,置灰,屏蔽所有事件,当存在readonly
要自动失效 -
loading
加载状态,加载期间改变样式,不让点,不给hover/active
效果
我们假设如果用 Tailwind 做会变成什么样子,我需要在运行时根据不同的状态,在 class/className
中来判断用哪些原子类。有些状态是跨层级的,即满足 XXX,我需要让下个层级,或下下下层级的某个东西发生样式变化,原子类的跨层级操作很弱,我只能传参数下去,一层层像 React 的 props 被无数人吐槽的那样一层层传,然后每个层级还有自己的样式类,我还需要一个第三方库来帮我合并不同格式下的类名。
关键是很多时候组件本身并不知道被哪个组件用了,像是 Element Plus 中局部的表单组件会有单独为 <FormItem/>
组件符合的逻辑样式。
<Button/>
组件是最最最简单的组件之一了,写起来简直想死,我都不敢想复杂组件是什么样的灾难现场,可能是我菜吧,我解决不了,没有跨层级选择器带来的封装优势。
还有个问题我有疑问但没试过,感兴趣可以去试试,即我组件库如果依赖了 Tailwind 之类的原子库,但我打包时并不打包样式而是留给真正使用项目的开发者去一并打包,不知道是否可行。不然组件库一份,项目一份,除了它们其他第三方的都来一份,重复重复再重复了属于是。
再有就是如果是微前端项目,我不同的项目共享可能不太合适,那么我的 monorepo 式或模块联邦进来的其他项目模块,这能共享收益吗?我也没试过,因为我已经不想在深入挖掘它了,因为我有更好的选择 ------ CSS-IN-JS
。
强绑定
苛刻点说,组件库作为常用的第三方库,如果想做的通用就不可能强绑定一个原子样式库,除非它不影响项目。但这样的话上述所说的重复的问题就百分百存在了,它内部能用外部传入的样式并生效,但是生成样式表却是靠项目里的原子样式库编译,这也太不稳定了,移花接木呢。
升级
还有个想吐槽的事情,在 tailwind 出 beta 版本时,我现在项目里还是 v3,我想尝试升级玩玩。因为很多类名变了,我就用升级工具跑了一遍,跑完工程文件到处飘红,我不升级直接运行,发现我封装的很多失效了。
因为配置可以不写在 tailwind.config.js
里了,我如果把配置拎出来,我代码提示有没了。总之心碎了一地,懒得折腾了。
vue 派 && 编译派
对于我来说,如果要选择一个兜底的方案,我一定选 Vue 派,毕竟谁用谁知道。拿动态变量来说,Vue 已经能开箱即用提供变量,结合预处理器还能循环,写写梯度算法,偷偷懒,关键是这东西开箱即用,然后就能放弃思考开始写样式了。会的多的人就用用预处理器,或 PostCSS 的高级功能,不会的就老老实实写普通 CSS,其他常用的 Vue 都给你搞好了。
其他编译层面的,要么是不成熟,要么是没 Vue 全面。有点新意的,都是在抄作业,说的就是抄 Tailwind 那一批,那我为什么不直接用源头的 Tailwind 或 UnoCSS 呢,它们更加的可靠、稳定、全面、维护也积极,社区也大。
CSS-IN-JS
先明确表明下我对它的定义:把样式以 js 的方式去编写,后续无论是通过,静态编译、动态编译、行内插入、动态引用,在我这统统都属于是 cssInJS 的范畴
emotion
和 styled-component
只是这个技术的代表库,但并不表示全部,css in js
是一种把 css 用 js 去表示的技术手段的统称
我实际做过测试,技术栈是 emotion + react18 + jotai + acro-design
,即使是业务代码这种乱糟糟的东西也不会产生性能瓶颈,因为谷歌浏览器实在是太强了。我其实还从0自己撸过相关的 CSS-IN-JS 的 SSR 到博客,但体量太小就跳过吧,结果就是稳得很。
诚然css-in-js在频繁更新下性能很差,但在代码不是特别屎的情况下,一般触碰不到谷歌浏览器的性能极限。如果有人说因为这东西产生性能瓶颈了,常见的无非以下分类
- 极端场景,比如 figma 绘图做样式(举个例子,我不知道 figma 用的什么)
- 代码本身就很屎,一个操作不知道让
react
刷了多少回了都是,再快的 hash 算法也遭不住这样搞 - 跟风的人
- 团队级别的大规模使用
属于场景 1 的,其实什么手段都不好使了,都是特殊场景特殊处理了。场景3的很幼稚跳过。
场景2和场景4是比较常见的,代码屎技术菜,还吐槽是技术的锅,这种人你甚至都没法和他讲道理,他会觉得你才是那个彩笔。
团队级别大规模使用面临的困境,其实和 tailwind 大规模使用的困境一样,原子类这种是怕有人不守规则,css-in-js
是怕有人依靠着灵活瞎写代码,量变达到质变。一直有人盯着审查代码是很累的,所以各退一步,大家各自的团队喜欢哪个用哪个即可。
CSS-IN-JS 的舒适区
业务代码的特点就是,鱼龙混杂,什么美酒加药山珍海味,和厕所都混在一起,你永远不知道你吃一口甜的,那是真的美酒还是裹着巧克力的屎。规章制度和代码审查只能做到延缓屎山的形成,却一定是无法改变其屎山的结局的,更加细致的原因梳理可以看我往期的文章
《为什么我们的项目总是会避无可避的变成屎山?到底要如何进行有效的防腐措施?》 链接就不放了,有心之人自己找去看就行了。
既然业务领域无法发光发热甚至被各种踩踏,那我们换个角度思考呢,比如所有需要重度封装的样式领域,或极度灵活的封装库
对于这种越是灵活,封装+自由+适配+XXX,等等这种灵活度拉满的场景,css-in-js
技术就是css神器,它能在运行时扩展插件,自定义规则,过的去的性能加上各种维度上的极致灵活。给你举我自己包括大厂(阿里)目前都在用的几个场景,比如一款极致灵活的低代码配置表单和表格,ui库,自定义微前端。
有人可能会说css里也有变量,也有函数,有xxx,但我要说的是,尤其是对于基建项目来说,本身执行时机就不密集,整个生命周期也就聊聊数次,性能根本不成问题,这时最需要的就是极致的灵活,css-in-js就是在写代码,代码可以任意组合,可以写任意逻辑,有条件的动态决定使用哪些,引入哪些等等,这种表现力用纯原生css或只在编译期做会非常的束手束脚,操作的空间相比也会少很多。
这样说有些抽象,我来举例一些拆来揉碎的来聊
主题系统
文章上面提到了很多,对于组件库来说,为了尽量做到通用,可定制化,仅仅只是改变个颜色什么的还远远不够,还要能改,字体、行高、圆角、各种维度尺寸(表单组件的大中小,弹窗的大中小...)、阴影、字号等等。光能改不行啊,还得还看,至少得能看得过去吧,所以算法是少不了的。现在我公司的项目的主题系统,就是我基于 ant
的主题算法魔改的。试问原子化拿什么做到,靠满屏的 css 变量引用么,看着乱,调试还麻烦。补充下,ant的css变量是css in js
生成的,目的是为了让开发者改值
动态引用
之前提到过,如果是微前端之类的项目,我们有什么有什么有效的手段,来减少重复库引用造成的 css 体积膨胀呢 ------ 我不同项目依赖了同一个库,这个库会被打包很多份,每加载一个就得重复引用一份
我们如果只讨论组件库的话,css-in-js
可以做到,让样式 js 惰性引用,创建时检查下全局,如果没有在加载,虽然打包都会打包进去,但实际使用只会存在一份,并且还能做到任何一个子份都能控制所有加载项目的主题,支持运行期间动态变化,动态计算。定性完全没有问题,因为除了首屏,没有手动触发也不会计算第二次。
至于怎么管理这些公共的组件,不让它们首屏全部加载,这是额外的话题了,需要一个异步项目、组件、模块,加载管理器
低代码
像我的公司有各种低代码,低代码表格、低代码表单、低代码大屏、低代码BI分析、低代码页面
假设我用 tailwind 做是可以做到,此时老板和实施人员说,我要动态数量,动态样式怎么办?比如我有一个表格,我同时有以下要求
- 奇偶行,2种颜色
- 表头,表身,统计,三块区域会有默认的底色
- 有行选中,列选中,行hover,列hover,分别有不同的颜色
- 我的单元格没有跨行跨列时会存在,满足某个条件动态变色
- 有跨行跨列就会变成,固定某些单元格变色
这还是只是冰山一角,原子类的方案需要在编译期间扫描,关键是我都不知道会被设置成什么怎么给编译器扫描呢。有些有规律的东西可以通过定义 css 变量,再用 tailwind 引用来做到,但如果是 css-in-js
,我在保存配置时直接把样式字符串生成好,一个 <style/>
就完事了,我都不需要知道里面有什么。
动态编译
css-in-js
是有运行时的,就是像 postcss/scss/less/stylus
之类的编译器,这可是能在运行时生效的编译器,这意味着只要我想,scss/less/stylus/tailwind/unocss
等等我可以用 css-injs
动态做到同样的效果
有样式污染问题怎么办?挂个 layer
级联层插件 或 动态加前缀 或 生成hash类名
重复引用重复加载怎么办?挂个引用插件
代码想写 字符串,样式对象,或嵌套怎么办?把 emotion
底层依赖的那个运行时拉过来(忘了名字了)
某某功能不支持怎么办?我插件本身都可以是异步的,要哪个加载哪个
有性能问题怎么办?谁让你一股脑把所有东西首屏就全部拉过来用的,我需要哪个动态加载哪个就行。而且我们现在的场景是辅助业务开发的库视角,对于有能力做这种层面事的人,稍微用点手段把耗时均摊下,再怎么耗时的任务都不至于产生性能问题
总结
我无意拉踩 Tailwind 之类的原子库,只是跨它的文章太多了,拉踩 CSS-IN-JS 的也多,那我就多说点 CSS-IN-JS 的好。
我自己的团队中,我是多种技术混着用的,因为架构是我搭的,我会考虑到团队接受度,业务的复杂度,和可能会面临的混乱程度对症下药。大致说下就是,所有库封装样式相关的绝大多数都是自研的 CSS-IN-JS,项目中全局的样式 90% 是 Sass 代码,因为它们的静态属性很强没必要折腾,设计到参数的全局级别的动态代码也是 CSS-IN-JS,Vue 的样式方案兜底,Tailwind 我用来给组员提供开发幸福度的,我只需要它的便捷性即可。
经常有人说,也有人问我有什么是 A 做不到,而 B 才能做到的。这种问题在我看来没什么意义,属于是非黑即白太极端了。对于样式方案来说,最终都是要打成原生 CSS 使用的,不然浏览器不认识就不会生效啊。
有能力驾驭各种技术对症下药肯定是更好的,因为不存在完美的技术,人得要灵活变通。而在牛逼的技术也要服务于业务,满足不了老板谁给你发钱让你猖狂呢。
一个 Tailwind 并没有牛到单骑走天下,CSS-IN-JS 也没有查到人人喊打,成了过街老鼠的地步。甚至前后端不分离都有独属于他的用武之地(我也写过文档,是我的一个大厂朋友到现在他都很钟爱的技术,还有时不时的赞助)。
言尽于此,有营养的评论会回复,能说服我的我也愿意大大方方的认错,牛头不对马嘴纯想发泄的我承认你比我厉害,随便喷,886。