前言
最近看到这样一篇文章:《要喷也得先做做功课吧?驳Tailwind不好论》
个人觉得说的还是有一定道理的,就是该作者的语气态度可能稍微冲了点:
不过他说的确实有道理,如果这种原子化工具真的如评论区里那帮人说的那么不堪的话,又怎么会达到百万级的周下载量?而且还呈现出一路增长的态势,当然有人可能会说:哪一路增长了?你看那条曲线,在最右边都掉的不行了,肯定是马上要 G
了。
其实这些周下载量会在某几周呈现出极速下降的趋势一般都不是因为这玩意要没落了,哪有没落的这么快的,一两周用户量就少一半?出现这种情况的原因是因为欧美那边放假了,比方说复活节、圣诞节什么的,大家都出去玩了没人工作了所以才呈现出大幅下降的趋势。当然也不仅仅是欧美,某东方神秘大国放长假时也会呈现出类似效果。这个作者截图的时候可能刚好是欧美有什么假期,不信的话我们现在再来截一遍图看看:
卧槽!他截的是 tailwindcss
的下载量么?怎么我截的时候是五百多万的下载量?差的也太多了点,不过五百多万的下载量更证明了这个工具有多受欢迎,为了有个对比我们再来截一下 Vue
的数据:
可以看到 Vue
的下载量也不过三百来万,所以你是信评论区下面的那些小卡了米说这玩意有多垃圾多不好维护?还是信全球开发者闭着眼投票出来的用户数据?
体验
作为一名已经使用了多年的用户,我可以先跟大家分享一下自己的体验,以及用了这么久的优缺点,是否像评论区说的那么不好用,以及又是否像那些粉丝说的那么好。
首先说一下当初为什么要选择 Tailwind CSS
,原因其实很简单:求新!
当时刚来这家公司不久,可能也就三四个月?但我作为一个新人却接到了一个重任:低代码平台!这个低代码是给公司运营同事用的,他们经常会出一些活动页面,这些页面技术含量并不高,交互也比较简单,主要是大量的图片以及一些表格表单等。但架不住页面多啊,总让我们做这些页面非常浪费人力,所以领导就想做一个低代码平台来分担我们的压力,他们运营想要什么页面自己搭建就好了,而我们只根据他们的需求来定制一个专属的低代码平台。
虽然不知道为什么这个重担会委派给我这个新人,但我还是非常开心的(感谢我阳总)因为这个项目只在公司内部使用,所以可以不用考虑那么些个兼容性什么的,我可以放心大胆的使用最新鲜的技术,终于不用再兼容那些个破浏览器啦!
而且这个项目还给了我很大的自由度,那时候 Vite 2.0
才刚发布不久,都没多少人用。甚至 Vue 3
都还算得上是比较新鲜的技术,至少在市面上用 Vue 3
开发的项目还不是很常见。但我当时想的是既然是一个船新项目那就不要留下什么技术债了,虽然当年我选的技术不是很成熟,但随着时间的推移慢慢的他们都会变成熟的嘛!如果现在就束手束脚的还用什么 vue-cli
+ vue2
,随着时间的推移这个项目又跟公司现在维护的老项目没什么区别了。
当然有人可能会问:你怎么知道 Vite
这些新技术会越来越趋向成熟?万一发展一半像 Snowpack
一样噶了呢?我当然不知道某项技术未来的发展趋势,但当时的我就是有一种迷之自信,就是坚信尤雨溪出品的项目一定会有所发展,结果现在 Vite
发展的确实不错,尤大出品,必属精品。
一开始用的是 Vue3
+ TSX
,那时候是 3.0
,还没有 setup
语法糖,每个变量还需要在底部 return
出去很麻烦,但用了 JSX
就不用挨个 return
了,而且 JSX
的灵活性也能让我很方便的使出一些骚操作。
但 JSX
和 CSS
的结合不像 .vue
文件那么方便,而且 Vue
也不像 React
似的有那么多 CSS-in-JS
的库,不过好在 Vite
原生支持 CSS Module
,于是采纳了 TSX
+ CSS Module
的方案,感觉也挺好用的。
然而某一天我突然刷到了一篇 Tailwind CSS
的文章,其实以前也刷到过,但对这玩意的印象也就一般。不过今时不同往日,一方面是新项目我想把以前没用过的东西都用一遍(前提是 Star
数很高以及 npm
下载量还可以)另一方面感觉这个技术有点击中我的痛点,一方面是 TSX
+ CSS Module
确实没有 .vue
文件来的方便,毕竟要写两个文件,那有人可能会说你可以写成这样:
html
<script lang="tsx">
...
</script>
<style scoped>
...
</style>
当年真的没有这种语法,这是 3.2
才加上去的,当时只有 3.0
这个版本可以选,而且我也不知道后来他能搞这么多语法糖进去,我还以为一直都会写成这样呢:
html
<template>
...
</template>
<script lang="ts">
export default {
setup () {
...
return {
a,
b,
c,
d,
e,
f,
g,
...
}
}
}
</script>
但 Tailwind
的出现让我觉得可以不用再写成两个文件了,一个.tsx
文件就搞定了。并且另一个痛点就是起名,比方说某个元素可能仅仅只需要往下稍微移一点,只需要设置一个 margin-top
就行,但你还需要给它起个类名,有时我就直接起名 mt
,当然这个类名还遭到了批评,让我不要这么写,所以后来我就干脆直接在标签上写个 style
:
html
<div style="margin-top: 2px">
...
</div>
你说这不就和 Tailwind
有点类似了么?我看评论区好多喷 Tailwind
的都说当样式多了以后可能会写出这样的代码难以维护:
html
<div class="relative p-3 col-start-1 row-start-1 flex flex-col-reverse rounded-lg bg-gradient-to-t from-black/75 via-black/0 sm:bg-none sm:row-start-2 sm:p-0 lg:row-start-1">
<h1 class="mt-1 text-lg font-semibold text-white sm:text-slate-900 md:text-2xl dark:sm:text-white">Beach House in Collingwood</h1>
<p class="text-sm leading-4 font-medium text-white sm:text-slate-500 dark:sm:text-slate-400">Entire house</p>
</div>
说实话写成这样确实有点恶心,看起来也不是很容易,但别忘了 Tailwind
并不会影响你原有的写法,如果真的有比较复杂的样式你就写在 css
文件里或者写在 .vue
的 <style>
标签里就行了啊:
html
<template>
<div class="card">
<h1 class="title">{{ xxx }}</h1>
<p class="content">{{ xxx }}</p>
</div>
</template>
<style scoped>
.card { ... }
.title { ... }
.content { ... }
</style>
然后只在简单样式的情况下使用 Tailwind
,按照这个原则把他俩结合到一起:
html
<template>
<div class="card">
<h1 class="title w-10 h-5">{{ xxx }}</h1>
<div class="mt-2 px-4" />
<p class="content">{{ xxx }}</p>
</div>
</template>
<style scoped>
.card { ... }
.title { ... }
.content { ... }
</style>
这样不就能达到很完美的互补了么?Tailwind
解决了简单样式不想写类名的困扰,也免除了上面写一个类名,然后滚动个几百行跑到下面定义这个类名,结果却仅仅只是写了个 margin-top
!而且也比写 style
简洁,还能获得更小的打包尺寸。
更小的打包尺寸怎么讲?比方说我原来的方案,不想写类名有时就直接写 style
:
html
<div style="margin-top: 2px">
...
</div>
但 style
就只能作用在单个元素上,另一个元素也需要 margin-top: 2px
,那是不是就相当于写了俩 margin-top
?但它俩的样式一模一样,此时类的优势就体现出来了:
html
<!-- 某组件 -->
<div class="mt-2" />
<!-- 其他组件 -->
<p class="mt-2" />
有人可能会想:这刚能节省几个字符?有道是积少成多,用 Tailwind
的地方多了,重复的样式也不可避免的会变多,那到底能节省多大的尺寸呢?会不会有可能尺寸反而变得更大了呢?这正是本篇文章将要进行的实验,不过在实验之前我们还是先来说说这玩意的缺点。
缺陷
我知道为什么老有人担心用 Tailwind
会写出这样的代码:
html
<div class="relative p-3 col-start-1 row-start-1 flex flex-col-reverse rounded-lg bg-gradient-to-t from-black/75 via-black/0 sm:bg-none sm:row-start-2 sm:p-0 lg:row-start-1">
<h1 class="mt-1 text-lg font-semibold text-white sm:text-slate-900 md:text-2xl dark:sm:text-white">Beach House in Collingwood</h1>
<p class="text-sm leading-4 font-medium text-white sm:text-slate-500 dark:sm:text-slate-400">Entire house</p>
</div>
虽然我知道复杂样式正常写,简单样式再用 Tailwind
的方案:
html
<template>
<div class="card">
<h1 class="title">{{ xxx }}</h1>
<div class="mt-2 px-4" />
<p class="content w-10 h-5">{{ xxx }}</p>
</div>
</template>
<style scoped>
.card { ... }
.title { ... }
.content { ... }
</style>
但刚开始用的时候我的强迫症还是迫使了自己进行二选一,为了像 .vue
文件那样只写一个 .jsx
而不用再多写一个 xxx.module.css
,毕竟这个后缀挺长的,每次写都在消耗我的耐心。于是我的强迫症还是让我写出了类似这样的代码(不过没这么夸张):
html
<div class="relative p-3 col-start-1 row-start-1 flex flex-col-reverse rounded-lg bg-gradient-to-t from-black/75 via-black/0 sm:bg-none sm:row-start-2 sm:p-0 lg:row-start-1">
<h1 class="mt-1 text-lg font-semibold text-white sm:text-slate-900 md:text-2xl dark:sm:text-white">Beach House in Collingwood</h1>
<p class="text-sm leading-4 font-medium text-white sm:text-slate-500 dark:sm:text-slate-400">Entire house</p>
</div>
当一行属性太长的时候我们通常都会给它换个行:
html
<h1
class="
mt-1
text-lg
font-semibold
text-white
sm:text-slate-900
md:text-2xl
dark:sm:text-white
"
>
Beach House in Collingwood
</h1>
看起来是不是好多了?但假如此时我们需要一个动态类名,在 .vue
文件里我们可以写成这样:
html
<h1
:class="{ title: isLoading }"
class="
mt-1
text-lg
font-semibold
text-white
sm:text-slate-900
md:text-2xl
dark:sm:text-white
"
>
Beach House in Collingwood
</h1>
也就是说我们可以写把同一个属性的静态内容和动态内容分开写,这在 .vue
文件中是完全没问题的,但到了 .jsx
这边:
它就一直给你飘红,注意我这里用的是 .jsx
,还不是 .tsx
,.tsx
会让你连编译都通不过。所以怎么办?那就只能这么办咯:
jsx
<a class={[{ title: isLoading }, 'mt-1 text-lg font-semibold text-white sm:text-slate-900 md:text-2xl dark:sm:text-white']} />
不换行还好(其实也挺乱的)但只要一换行:
肯定有人会说:那你把单引号改成反引号不就得了:
这样确实没问题了,但问题在于该项目用了保存文件时自动运行 prettier
和 eslint
,换行啥的根本不用自己操心,随便写,写完 command
+ s
一按,文件立马给你格式化:
这个功能特别方便,我很喜欢用,但用了反引号,换行就得靠你自己手动对齐了:
这真的让人挺不爽的,而且写成这样的话提示也都没了,静态 class
里会显示你写的颜色(需提前安装插件):
但写成动态 + 字符串形式的话这些效果通通会消失:
当然这也不是什么太大不了的事,看不见颜色也没那么大的影响。但在很多情况下是先写静态样式,然后才发现这块忘考虑 Loading
时的样式,再给加上动态代码,在 .vue
文件里写起来就很方便:
但在 .jsx
这边可就遭老罪咯:
除非你一开始就想好这个标签上是否有动态类,不然的话改起来你就说麻不麻烦吧?当然这也不应该算是 Tailwind
的缺点,应该算是 JSX
的缺点。但这个缺点被我随后刷到的 Windi CSS
解决了,当时刚用 Tailwind
不久,就刷到了 Windi CSS
,那时候的前端真是日新月异,我才刚用熟,就又出替代品了:
当时 Tailwind
还在 2.x
,没有 JIT
模式,速度方面被 Windi
吊打。而且 Windi
有个属性模式很实用,原本 Tailwind
都是写在 class
属性内的,但实际上有很多重复的前缀,比如:
这个 class
已经写的稍微有点长了,但换成 Windi
后:
html
<button
text="sm white"
font="mono light"
p="y-2 x-4"
border="2 rounded blue-200"
/>
一下子就短了好多,不再像之前那样一眼望去一大串类名了,而且也很好的解决了换行问题,毕竟你用的属性都被分了组,一组里的内容反而是有限的,比方说你想给个边距,那就是 m
属性,边距刚能有几个选项啊,这么多顶头了:
html
<button
m="t-1 b-2 l-3 r-4 x-5 y-6"
/>
通常情况下根本就写不了这么多,并且它还没有占用 class
属性,假如突然想加一个类名,就不用像以前似的那么麻烦了:
并且 Windi
最牛逼的一个功能就是自定义语法,这正是我非常需要的一个特性,比如说我需要 margin-top: 16.5px
,但 Tailwind
根本就没定义那么细,它的预置内容里没有 16.5px
,所以我只能把它写进 xxx.module.css
里。但 Windi
说了:预置里没有 16.5px
是吧?没关系!你只需要写成这样:
html
<button m="t-16.5px" />
Tailwind CSS
现在倒是支持这种语法,不过需要加个中括号有些略显麻烦:
html
<button m="t-[16.5px]" />
但当年的 Tailwind
是没有这些个花里胡哨的,在我眼里 Windi
简直就是 Tailwind Pro Max Ultra
!于是乎我二话不说就把 Tailwind
换成了 Windi
,然后把之前 class
里的那一坨挨个重写成属性模式,文件有点多,花了不少时间。而且由于用的是 .tsx
,这些没定义的属性会报错:
而且也容易让人分不清你写的到底是 Windi
的属性还是真的要传给组件的属性,Windi
早就替你想到了这一点,可以给属性加前缀,我加的前缀是:
js
import { defineConfig } from 'vite-plugin-windicss';
export default defineConfig({
attributify: {
prefix: 'css-:',
separator: '__',
},
});
为什么要用 css-:
这个前缀,一是带冒号会有一个不同的颜色,容易区分于普通属性:
二是我意外发现的神奇事件,只要属性是字母 + -
+ :
,TS
就不会再报错了:
等到周五写周报的时候我傻眼了,我这周大部分时间都用在重构上了,没怎么开发新功能,咋写啊?
本周工作:
Tailwind CSS
⇉Windi CSS
结果果然不出我所料,我被叫去小黑屋谈话了:
你应该以项目功能为主,不要老想着天天重构项目。前端是个变化非常迅速的行业,你要是追新的话你永远也追不完,它一天就能给你产出来一堆新技术来,难道每次出新技术你都要重构一遍么?
后来果不其然,Windi CSS
也 G
了。那时候我听说 antfu
进入了 windi
团队,然后准备出一个 UnoCSS
,记得原话说的好像是将会用 UnoCSS
来当作 Windi
的引擎,你可以理解为 Uno
是 Windi
团队的一次激进实验。所以我一直都很期待,但后来慢慢的发现 Windi
的更新频率越来越低,不知怎么回事,结果一查说是 Windi
的作者在推特还是哪跟人发生了一次激烈争吵,心灰意冷了。当然这并不是官方说法,官方说法是这样的(机译):
我刚被约谈没多久,你就告诉我 Windi
也 G
了?我现在换成 Uno
是不是过段日子又特么出新品了?这玩意出的怎么比手机都快?为了防止再次挨骂,这次我就没换,因为 Windi
也确实用的好好的没什么毛病。不过这个 Uno
不是我每天都在用的洗面奶么:
于是 Windi
就一直用到现在,不过写法跟 Uno
应该也没啥太大区别,就当是 Windi
的继任者好了。扯远了,咱们继续来说缺点,Tailwind
还有一个缺点,不过缺点这个词用的也不是特别准确,因为它既是缺点又是优点,具体取决于你所开发的项目类型。看到这有人可能会说:你跟我在这玩薛定谔的猫呐:
这个缺点就是单位,当你写了 mt-1
这个类时,你可能会以为是 margin-top: 1px
,然而实际上却是 margin-top: 0.25rem
,那 0.25rem
又是多少呢?按 1rem = 16px
来换算,它应该是 16px * 0.25
,这个数乍一看不是那么好算是吧?我们可以把 0.25
看作四分之一,16px * 1/4
就相当于 16px
除以 4
...
难道每次写 Tailwind
的时候都要心算一遍么?太累了吧?这玩意用上个两三年之后是不是就成心算高手了?但你要是换个角度来看的话这个设计又成为了一个优点,就是当你写一个不是那么精细的响应式 H5
的项目,这玩意还蛮好用的,因为 1rem
具体等于多少 px
是可以根据屏幕尺寸来进行动态变化的。不过对于我们来讲都是有设计图的,少 1px
测试都会提 bug
,所以我更希望的是 mt-1
就代表 margin-top: 1px
,既好记又精细。
于是 UnoCSS
说:这需求我熟啊!我可以很方便的进行预设:
并且也提供了各种常用预设:
总之不仅性能好,而且还非常灵活。
优势
不能光说缺陷不说优势是不?那我就谈谈这两年使用上让我觉得非常舒心的地方,就是有时候改版需要改点样式,但改版也不至于说跟原来的样式一点都不一样,大部分都是一样的,只有少部分变化。假如按钮往下移一点、banner
变宽点这种小改,然后进入到项目中,找到按钮,发现这款按钮有个类名叫:
html
<button class="casino-header-user-avatar-btn">
...
</button>
大部分人都会直接搜这个类名,搜到以后直接加一个 margin-top
对吧?但一搜却发现根本搜不到这个类名,因为这种类名一般都是通过 CSS
预处理器定义而来的,比方说这个项目叫 casino
,然后写 header
的样式:
css
.casino {
... /* 此处省略 N 行干扰代码 */
&-header {
... /* 此处省略 N 行干扰代码 */
&-user {
... /* 此处省略 N 行干扰代码 */
&-avatar {
... /* 此处省略 N 行干扰代码 */
&-btn {
...
}
... /* 此处省略 N 行干扰代码 */
}
... /* 此处省略 N 行干扰代码 */
}
... /* 此处省略 N 行干扰代码 */
}
... /* 此处省略 N 行干扰代码 */
}
按理说一个组件最好不要超过 300
行,但有的复杂组件经常超过 500
行,甚至上千行的我都遇到过(应该也写过),当搜不到一个类名时那就要疯狂滚动鼠标滚轮,滚到冒烟自己去下面一行行找,找到哪个才是控制 button
的类,再去添加样式。这还不是最痛苦的,最痛苦的是这个按钮不止一个类名,或者有人用了这种写法:
css
.xx {
... /* 此处省略 N 行干扰代码 */
.xxx {
... /* 此处省略 N 行干扰代码 */
button {
margin-top: 3px;
}
}
... /* 此处省略 N 行干扰代码 */
}
当我们好不容易找到了对应的类名并且加入了 margin-top
却发现不生效时,我们就只好打开控制台找到元素看看是什么样式把我们的 margin
给覆盖了,找了以后又去一顿搜,总之挺麻烦的。后来干脆就不这么干了,下次再有这种就直接在标签里写一个 style="margin-top: 6px"
,写着写着突然就意识到:这不跟写 Tailwind
差不多么?
当然肯定会有人说哪差不多了,style
有更高的权重,能覆盖你在各种类下产生相互影响的样式,你要是写 Tailwind
能覆盖么?但假如一开始这个项目就是用 Tailwind
写的,我们可以在类名里看到:
html
<button class="w-10 h-4 bg-yellow mt-3">
...
</button>
我们是不是就可以省去滚动几百行代码去找类名的这么一个步骤,直接在标签上改成 mt-6
就行了?如果用的是 UnoCSS
属性模式的话那就更方便了,直接找到 m
这个属性改里面的数字就行了:
html
<button
w="10"
h="4"
bg="yellow"
m="t-6"
>
...
</button>
看到这有人可能会说,你看这个属性模式也没节省什么代码,class
里面有四个类:class="w-10 h-4 bg-yellow mt-3"
,换成属性模式后不还是四个属性么?
别忘了 UnoCSS
是可以非常方便的自定义插件的,比方说最常用的宽高,它俩被分为了两个属性对吧?我们可以自定义一个 size
属性表示宽高:
html
<button
size="w-10 h-4"
bg="yellow"
m="t-6"
>
...
</button>
然后还可以把与颜色相关的分为一组,比方说前景色、背景色等:
html
<button
size="w-10 h-4 border-box"
color="yellow bg-blue"
m="t-6"
>
...
</button>
再把盒模型分为一组,包括 padding
、margin
、border
、box-sizing
等:
html
<button
size="w-10 h-4"
color="yellow bg-blue"
box="mt-6 pb-4 border-box"
>
...
</button>
这样可不仅仅只是代码行数减少了,更是可以通过分组一眼望去就能大概看出是个什么样式。
还有就是简单样式时也很方便,以往常见的写法都是上面定义一个类名,然后滚轮一顿滚,再在下面写样式,然后滚轮再滚回去,滚回去的过程中就找不到是哪行了,反正当代码多起来的时候我就是这种感受。而用了 Tailwind
之后想写什么样式我就直接写在标签里了,根本不用滚!
尤其是在 jsx
文件中,jsx
本来无法写样式,当然我指的是 .vue
文件中 <style>
里的那种样式,不是这种:
jsx
const btnStyle = { color: 'pirple' }
<button style={btnStyle}>...</button>
当然也不是 CSS-in-JS
那种:
jsx
const Flex = styled.div`
display: flex;
align-items: center;
justify-content: center;
`
<Flex>...</Flex>
但用了原子化 CSS
就让 jsx
也能很方便的拥有样式了,虽说复杂样式比较捉襟见肘,还得借助其它方案,但简单样式时真的还蛮好用的。
实验
那么接下来我们就来做一个实验,当然这个实验并不是为了对比 Tailwind
和 Uno
谁更快,肯定是 Uno
快,官方宣传是 5
倍快。
这个实验主要是想搞清楚这些个原子化 CSS
究竟会不会缩小打包后的体积,CSS
的体积必然会有所减少,但与此同时 HTML
体积又会有所增大,此消彼长的情况下究竟谁能够更胜一筹?
实验如下:
Vue 3
只用Tailwind
写一个TodoList
Vue 3
只用UnoCSS
的属性模式写一个TodoList
Vue 3
复杂样式用普通写法、简单样式用Tailwind
写一个TodoList
Vue 3
复杂样式用普通写法、简单样式用UnoCSS
的属性模式写一个TodoList
Vue 3
不用任何原子化CSS
写一个TodoList
Vue 3
不用任何原子化CSS
(但用内置的scoped
)写一个TodoList
Vue 3
只用CSS Module
写一个TodoList
然后分别将其打包,对比一下各自的体积大小:
一个 TodoList
写这么花?别提了,一开始做了一版中规中矩的比较简陋 简洁的 TodoList
,哪成想打包出来的产物差异很小,几乎可以忽略不计,没有任何参考价值。仔细想想也是,毕竟一个 TodoList
刚能有几行代码?原子化 CSS
肯定是样式文件越多,重复性才越高,才越有实验的价值。于是我才把样式尽可能往复杂了写,写的时候还发现 Vue
唯一公认的拖拽库断代了,具体可以看一下这篇:
但没想到即使我把 TodoList
写的都这么花了,打包出来的产物依然还是没有什么太大区别。后来分析了一下,一方面是无论 TodoList
写的有多花,它的体量还是太小了。另一方面则是这款 TodoList
的复杂样式和我们日常开发项目的那种复杂样式并不属于同一种复杂,咱们日常开发时很少会出现这么炫酷的动画和交互,这些动画基本上也不太容易重复。但那些普通元素什么的很多,每个元素单拎出来都不复杂,但组合到一起就麻烦了,又要考虑定位、又要考虑相互之间的作用什么的,同时也会出现更多的雷同 CSS
代码。所以最好的方式是将已有的项目分别用 Tailwind
、Uno
、单文件组件以及 CSS Module
等方式重构几遍,然后再对比打包后的差异。
思来想去,一个至少达到中型规模的项目才能够达到测试的目的(真没法用巨石应用来测试,巨石应用需要好多人合作开发数年才行,有多庞大就不说了,肯定也是屎山项目,太费劲)所以我打算把自己熟悉的项目重新改造,最终此消彼长依然没有什么太大差距:
所以说如果你用原子化 CSS
工具是为了减少打包体积的话,那你可以洗洗睡了,但如果你是为了写起来更爽的话,那确实还值得一试。