积土而为山,积水而为海。——《荀子·儒效》
大家好,我是柒八九。
前言
在回望过去,展望未来- 2024 React 生态一览表中讲到CSS
时,我们提到过Tailwind CSS
,并且也说会有相关的文章。
在文章中介绍到,Tailwind CSS
的受欢迎程度还是很高的。 。
不能说是遥遥领先,但是也是和另外的css
解决方案 - style components
并驾齐驱。
这不,乘着上篇文章还没凉透的,我们来今天乘热打铁,来讲讲Tailwind CSS
。
在使用 Tailwind CSS
时,由于它的原子特性,让我们写样式时,变得十分丝滑。我们只需在 HTML
中粘贴一系列不同的类,就完成了页面的粉饰工作!但随着项目的增长,类列表
也在增长。直到某一天,无数繁杂的类属性,堆砌在我们的代码中,这时候便利性和维护性就会大打折扣。然后,我们引以为豪的代码,最终会变成别人口中的 💩⛰️。
Tailwind CSS
就是CSS
的jQuery
。并非每个人都喜欢它
好了,天不早了,干点正事哇。
我们能所学到的知识点
- 前置知识点
- 使用 Tailwind CSS 的先决条件
- Tailwind CSS 的架构
- Tailwind CSS 的工作原理
- Tailwind 建议
1. 前置知识点
前置知识点,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。如果大家对这些概念熟悉,可以直接忽略
同时,由于阅读我文章的群体有很多,所以有些知识点可能我视之若珍宝,尔视只如草芥,弃之如敝履。以下知识点,请酌情使用。
如何在项目中使用 Tailwind CSS
在Tailwind CSS 官网中,为我们提供了,四种方式来使用Tailwind CSS
。
下面呢,我们就以我相对熟悉的技术(Vite+React
)来演示如何在项目中使用Tailwind CSS
。
1. 创建项目
我们是用Vite
来创建一个React+TS
项目。
yarn create vite tailwindReact --template react-ts cd tailwindReact
2. 安装 Tailwind 相关依赖
yarn add -D tailwindcss postcss autoprefixer
在安装完依赖后,我们需要通过指定命令生成tailwind css
的配置文件。
npx tailwindcss init -p
此时,会在项目的根目录下,自动生成两个文件
tailwind.config.js
postcss.config.js
紧接着,我们需要在tailwind.config.js
中配置模板路径。
export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], theme: { extend: {}, }, plugins: [], }
3. 在项目 css 中引入 tailwind 指令
我们在项目的主css
入口,引入如下的指令。
@tailwind base; @tailwind components; @tailwind utilities;
在Vite
项目中,我们一般以./src/index.css
作为主css
。当然,我们可以安装项目的不同,根据情况引入。
4. 在组件中使用 tailwind 类
// 我们在之前的步骤中,已经在其中引入了 tailwind 指令 import "./App.css"; function App() { return ( <> <h1 className="text-3xl font-bold underline">前端柒八九</h1> </> ); } export default App;
随后,我们就可以使用yarn dev
进行本地项目的部署和查验了。
2. 使用 Tailwind CSS 的先决条件
上面的问题是可以通过一些规则来规避和改变的,让我们项目即利用了Tailwind CSS
便利性时,又变成可维护性。
如果大家想在项目中使用Tailwind CSS
,可以考虑下,自己团队和项目中,是否满足下面的条件;如果不满足下面的要求,Tailwind
可能会让我们的工作变得举步维艰。
1. 统一的设计规则
首先,在项目中应该有一个设计规则。这就要求UI
小姐姐和你需要通力合作。也就是UI设计
和前端应该使用一致的设计规范。
假设我们有一个标准按钮和一些需要与该按钮颜色相同
的选项卡:
.button { background-color: rgba(247, 186, 30, 0.6); } .tab { background-color: rgba(247, 186, 30, 0.6); }
后期我们想要更改项目的颜色方案,我们需要找到此颜色的每个实例
并在所有地方进行更新。对所有实例进行手动替换,这是一步耗时且充满危险的举动,你可能会误伤其他的变量,又或者遗漏部分变量。
设计规则有助于防止这些问题,并确保 UI 元素之间的统一性。
为了实现统一的设计规则,我们只需要在tailwind.config.js
中定义这些规则:
module.exports = { theme: { colors: { primary: "rgba(247, 186, 30, 0.6)", }, }, };
在添加了一个名为primary
的新颜色后,我们可以在整个应用程序中使用bg-primary
设置背景颜色,或者使用text-primary
设置文本颜色:
<button class="bg-primary">标准按钮</button> <div class="bg-primary">第一个选项卡</div>
这样,当我们想要更改项目中的颜色方案时,我们只需在一个地方进行颜色替换:tailwind.config.js
。
如果我们项目中和 UI 小姐姐没有达成统一的设计规则,最好避免使用 Tailwind
,因为我们将不得不在类列表中写入自定义的值(如 'p-[123px] mb-[11px] gap-[3px]'
)或添加大量新的规则,这最终会给我们的代码带来很多混乱。
更糟糕的是,某些无良 UI 朝令夕改的 UI 规则,到最后终究是你一个人抗下了所有。
拥有一致的设计系统,它可以帮助开发和设计团队更好地相互理解。
2. 基于组件
由于Tailwind 类
可以直接应用于元素,在项目小的时候,这是爽到飞起的特性,但是随着需求的变更,你就会看到很多元素的类名,在一行上排布的密密麻麻的。
<div className="flex flex-col h-[calc(100%_-_5rem)] flex-1 p-6"> <div className="flex flex-col flex-1 text-white overflow-y-auto h-[600px]"></div> <div className={`${ classNames(isLoading) ? "pointer-events-none opacity-60 hover:cursor-not-allowed" : "" }border border-white/60 min-h-[120px] max-h-[240px] rounded-xl flex p-[15px] mt-4`} > <div className="flex-1 h-full overflow-y-auto"> <textarea className="m-0 resize-none box-border flex-1 h-full bg-transparent overflow-y-auto focus:ring-0 focus-visible:ring-0 leading-7 w-full text-white" /> </div> </div> </div>
如果,让你去接手上面的页面,我感觉你会抓狂到想骂人的。所有,为了引起不必要的麻烦,我们需要做出改变。
解决方案:积极采用基于组件的方式,将频繁使用的模式(在我们的情况下,出现多次的 HTML 元素)封装为单独的组件。
采用这种方法,我们可以保持 DRY 原则
。而且,我们仍然可以为我们的 Tailwind
样式保留单一的来源,并且可以在一个地方轻松更新它:
<!-- 具有一长串Tailwind类的可重用按钮: --> <button class="bg-yellow-700 border-2 font-semibold border border-gray-300 text-green p-4 rounded" > 前端柒八九 </button> <!-- 创建一个可重用组件 --> <CustomButton>前端柒八九</CustomButton>
关于基于组件的方法的最后一点建议是:避免使用@apply
指令:
.block { @apply bg-red-500 text-white p-4 rounded-lg active:bg-blue-700 active:text-yellow-300 hover:bg-blue-500 hover:text-yellow-300; }
使用这个指令,我们的代码可能看起来更清晰,但它抛弃了 Tailwind
的关键优势:在为 CSS 类命名时减少心理负担,以及更改样式时不会出现回归问题(因为使用@apply
时它们不会在组件内隔离)。此外,使用它会增加 CSS 包大小。
Tailwind
的创建者在文档中也强调了谨慎使用@apply
指令的重要性。
如果我们项目满足了这两个要求,Tailwind CSS
很可能是我们的一个很好的框架选择!如果,在项目开发过程中,我们无法满足上述的硬性要求,还是另辟蹊径哇。毕竟,条条大路通罗马。
3. Tailwind CSS 的架构
可配置的设计系统
Tailwind CSS
的核心是 tailwind.config.js
文件。这个配置文件可以使开发人员能够在项目级别建立设计系统。我们可以定义颜色、字体、断点等方面。然后,Tailwind CSS
的架构被设计为基于这个配置生成一组实用类。
实用类生成
Tailwind CSS
的架构包括一个实用类生成引擎,负责生成成千上万的实用类。使用在 tailwind.config.js
文件中指定的设计系统,它为每个 CSS 属性生成类。例如,如果在配置文件中定义了三种字体大小,Tailwind CSS
将生成三个实用类,分别对应每种字体大小。
PurgeCSS 集成
尽管有成千上万的实用类提供了广泛的设计灵活性,但可能导致臃肿的 CSS 文件。为了解决这个问题,Tailwind CSS
内置了与 PurgeCSS
的集成,PurgeCSS
是一个用于删除未使用 CSS
的工具。在生产构建过程中,PurgeCSS
扫描我们的文件并丢弃任何未使用的类,从而产生一个精简、性能优化的 CSS
文件。
插件架构
Tailwind CSS
的插件架构增加了其可扩展性和定制能力。插件允许我们创建自定义实用程序、组件,甚至添加变体。这为引入复杂的设计元素到实施基于交互的类等无限可能性打开了大门。
即时模式(JIT)
Tailwind CSS
的最新创新是即时模式(JIT)。它解决了一次性生成所有实用类的性能问题。JIT
不是一次性创建所有实用程序,而是在需要时生成类。这导致更快的构建时间和更小的文件大小,并允许使用任意值类和基于元素状态的类,如 hover
、focus
、active
等。
关于JIT
是不看起来很眼熟,在V8
处理 JS 的时候,也会用到这个技术。想了解更多,可以看我们之前写的这篇文章:V8 如何处理 JS
优势和权衡
Tailwind CSS
的实用主义架构提供了显著的优势。它提供了巨大的定制选项,并消除了覆盖样式的需要,从而提高了开发人员的生产力。使用 Tailwind CSS
,HTML 文件充当了组件样式的单一真相源。
然而,这种架构确实带来了潜在的权衡。HTML
文件可能会因多个类而变得混乱,而且与传统的 CSS
框架相比,学习曲线更陡峭。尽管存在这些权衡,开发人员通常发现收益往往超过了挑战。
4. Tailwind CSS 的工作原理
从底层实现上看,Tailwind CSS
的工作方式是你向其传递一些 CSS
文件,然后它会在这些文件中查找 @tailwind
规则。如果遇到这样的规则,它将遍历项目中的其他文件,查找 tailwind
类名,并将其注入到找到 @tailwind
规则的 CSS
文件中。
/* 输入 */ @tailwind base; @tailwind components; @tailwind utilities; .foo { color: red; }
被转换为:
.border { border-width: 1px; } .border-2 { border-width: 2px; } /* …省略很多 */ .foo { color: red; }
基于此,我们可以确定 Tailwind CSS
内部工作的几个阶段:
- 扫描
.css
文件以查找@tailwind
规则。 - 根据用户在
tailwind
配置中提供的glob
模式,查找要从中提取tailwind
类名的所有文件。
- 也就是我们在
tailwind.config.js
中配置的content
属性
- 一旦找到这些文件,提取潜在的
tailwind
类名。 - 解析潜在的
tailwind
类名以检查它们是否真的是tailwind
类名。
- 如果是,则从中生成一些
CSS
。
- 用生成的
CSS
替换原始的css
文件中的@tailwind
规则。
5. Tailwind 建议
1. 尽可能减少实用类的使用
当我们为 HTML
元素构建实用类列表
时,每个新类都会为代码后继者
增加阅读的复杂性,他们将稍后必须分析和处理代码(这也包括我们自己)。当然,这些列表是 Tailwind
的一个重要且固有的特性,但尽管如此,最好尽量减少实用类的使用。
以下是减少类数并获得完全相同结果的一些方法:
- 不要设置
pt-4 pb-4
,可以直接使用py-4
。这同样适用于px
、mx
和my
属性。 - 不要使用
flex flex-row justify-between
,可以直接使用flex justify-between
。这是因为在 CSS 中,flex-row
是flex-direction
属性的默认值。通常,记住其他 CSS 属性的一些默认值(例如flex-wrap
)可能对识别这类情况有帮助。 - 不要编写类似
border border-dotted border-2 border-black border-opacity-50
的长类列表,可以设置border-dotted border-2 border-black/50
,也可以达到相同的效果:border-2
表示已设置border
,而border-black/50
表示RGBA
格式的简写。
使用更短的类列表,下次检查应用程序结构时,分析正在进行的操作将变得更加容易。
2. 将设计规则分组并以语义方式命名
在团队开发时,我们可能会有自己团队的编码实践(如清晰的变量命名),这对于长期开发非常重要。定制一些一目了然的规则能达到事半功倍的效果。
使用设计规则是一种很好的做法,但随意粘贴它们可能会导致 tailwind.config.js
文件中的混乱。
为了解决这个问题,在tailwind.config.js
中将相关的规则分组在一起。这意味着断点
、颜色
等的设计规则将位于特定区域,不会互相干扰:
module.exports = { theme: { colors: { primary: "oklch(75% 0.18 154)", secondary: "oklch(40% 0.23 283)", error: "oklch(54% 0.22 29)", }, spacing: { sm: "4px", md: "8px", lg: "12px", }, screens: { sm: "640px", md: "768px", }, }, //... };
然后我们就可以在我们组件中肆无忌惮的使用这些规则了。
<h1 className="bg-primary m-lg"> <div className="text-secondary m-sm">前端柒八九</div> <div className="p-md">关注走一波</div> </h1>
还有另一点很重要:为我们的规则保持单一的语义命名约定将使查找必要的令牌并随着应用程序的增长扩展系统变得更加容易。
3. 保持类的顺序
这是另一种清晰的编码规范:使用一致的顺序使类更易阅读和理解。为了说明这一点,让我们看一下一些具有未排序类的 HTML 元素:
<div class="p-2 w-1/2 flex bg-black h-2 font-bold">前端柒八九</div> <div class="italic font-mono bg-white p-4 h-2 w-3 flex">关注走一波</div>
在上面的块中,有不同类别的类:处理框模型、显示、排版等,但它们没有任何形式的呈现顺序。我们可以应用一个统一的顺序,按类别对类进行排序:
<div class="flex h-2 w-1/2 bg-black p-2 font-bold">前端柒八九</div> <div class="flex h-2 w-3 bg-white p-4 font-mono italic">关注走一波</div>
由于手动维护类的顺序需要很多时间和注意力,最好使用 Tailwind CSS
的官方 Prettier 插件来自动化这项工作。
注意:在使用prettier-plugin-tailwindcss
时,会和prettier
有版本兼容性问题。
4. 最小化构建大小
保持打包资源尽可能小是非常重要的,庞大的构建会导致页面加载缓慢、性能不佳,用户体验差。
Tailwind
为我们提供了数千个实用类,但在单个项目中几乎不可能使用所有这些类。
如果我们使用的是 Tailwind 3.0
或更高版本,则默认情况下启用了项目中的即时(Just-in-Time
,JIT)引擎——它确保在需要时生成 CSS 样式,我们无需为生产构建清理未使用的样式。
但是,如果我们使用的是 Tailwind
的旧版本,则需要为构建执行额外的优化——可以使用 PurgeCSS
,这是一种用于删除未使用 CSS 的工具。我们还可以在tailwind.config.js
文件中手动启用 JIT 模式,如下所示:
module.exports = { mode: "jit", //... };
这将确保我们只在我们的打包中包含必要的样式。
还有另一件重要的事情要考虑:始终对生产构建的最终 CSS 进行缩小。压缩会删除所有不必要的字符(如空格、注释等),这将明显减小文件大小。
使用 Tailwind CLI
,可以通过设置--minify
标志来实现:
npx tailwindcss -o build.css --minify
或者,我们可以将 Tailwind
安装为 PostCSS
插件,可以通过将其添加到插件列表中来使用 cssnano
工具进行缩小。
如果我们不考虑优化,我们的 CSS 大小可能会变得非常大(超过几十千字节)。即使在一个只有几个带有样式的组件的小项目中,启用 CSS 压缩和 JIT 模式后,大小差异也可能超过 30%。要实现这一点,只需按照上述说明添加缩小标志并启用 jit 模式。
我们可以在postcss.config.js
中配置cssnano
的处理。
export default { plugins: { tailwindcss: {}, autoprefixer: {}, ...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {}), }, };
上图中,第一个build
使用了cssnano
,第二个没有。虽然,在资源大小上可以忽略不计,但是由于我们的项目比较小,但是如果是一个大项目的话,那优化是显而易见的。
5. 在覆盖和扩展样式时避免不一致性
假设,我们在页面上使用了一个带有自定义按钮的组件:
jsx
复制代码
<Button className="bg-black" />
并且我们有一个具有一些默认样式的 Button
组件:
export const Button = () => { return <button className="bg-white">前端柒八九</button>; };
在这种情况下,按钮将保持为白色。Tailwind
不会自动覆盖样式并应用黑色,因此我们需要在 Button
组件中指定它:
export const Button = ({ className = "bg-white" }) => { return <button className={className}>前端柒八九</button>; };
这个 Tailwind
的特性本身并没有什么问题,但是如果我们想通过覆盖或扩展大量样式来自定义某些外观,每次通过 props
传递类可能会很繁琐。
而且,这种方法还有另一个缺点:通过 props
接受工具类可能会使确保一致的组件视图变得更加困难。这种方法鼓励在应用程序中为相同组件使用任何工具类组合,这可能导致视觉一致性的缺失。
那么,我们该如何解决呢?
与其允许通过 props
传递任意的工具类,不如定义一组预定义的变体:
const BUTTON_VARIANTS = { primary: "bg-blue-500 hover:bg-blue-600 text-white", secondary: "bg-gray-500 hover:bg-gray-600 text-white", danger: "bg-red-500 hover:bg-red-600 text-white", };
然后,修改 Button
组件以接受一个prop
。为了更方便地构造 className
,我们可以使用 clsx:
export const Button = ({ className, variant = BUTTON_VARIANTS.primary }) => { return <button className={clsx(className, variant)}>前端柒八九</button>; };
使用
clsx
在需要条件地构造类时尤其方便。
<Button variant="secondary" />
现在,确保了一致性,尽管我们对完全自定义添加了限制,但灵活性仍然存在;我们可以为组件添加任何新的变体或编辑现有变体。
这种方法的另一个好处是,它使维护变得更加简单:对工具类的更改可以在一个地方进行,然后传播到应用程序中每个该变体的组件。
上面的建议,总结一下就是:
- 在可能的情况下,最小化实用类的数量
- 在团队中制定代码约定,例如通过分组设计规则并以语义方式命名
- 同样,实施一致的类排序并设置检查器以确保代码清洁
- 压缩捆绑包大小:确保只包含所需的样式,并始终对生产构建的最终 CSS 进行缩小
- 在适当的情况下,尝试为组件定义一组预定义的变体;这将有助于避免不一致性和样式覆盖的问题
后记
分享是一种态度。
全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。