本文本来只是一个 tailwind 的入门教程,但是最终变成了对于 CSS 前端样式的原理和发展的一个整体分享,希望带大家回顾一下 css 的演进过程,让大家从更深的层面理解样式系统的发展。
背景
从原生 CSS 到 Tailwind CSS 的发展过程中,社区中出现了诸如 Sass、Less、Bootstrap、BEM 等众多熟知的解决方案。这些方案的出现,是为了解决什么问题?它们的基本原理是什么?为什么我们会选择使用 Tailwind CSS?本次分享将全面解答这些问题。
通过深入探讨这些流行的 CSS 工具和框架,我们可以理解它们各自独特的设计理念和应用场景。从 Sass 和 Less 这样的预处理器,到 Bootstrap 这种全面的前端框架,再到 BEM 这种命名方法论,每一种技术都有其特定的优势和适用条件。尤其是 Tailwind CSS,作为一种实用主义的 CSS 框架,它通过提供低级的工具类来实现更高效的样式编写方式。在本次分享中,我们将详细分析这些技术的核心原理,探讨它们是如何帮助前端开发者更有效地编写和管理样式代码的,以及为什么 Tailwind CSS 在当前的前端开发领域中脱颖而出。
CSS 的基本概念
首先, 我们需要了解 CSS 的基本概念,实际上,CSS 的概念非常的简单,简单到
引入方式
在开发网页时,有几种不同的方式可以引入 CSS 来设置元素的样式。这些方法包括内联样式、外部样式表、内部样式表和用户样式表。下面是这些方法的详细说明和示例:
1. 内联样式 (Inline Styles)
内联样式是直接在 HTML 元素中使用
style
属性来定义样式的方法。这种方式适用于单个元素的快速样式设置,但不利于样式的重用和维护。示例:
<p style="color: blue; font-size: 14px;">这是一个带有内联样式的段落。</p>
2. 外部样式表 (External Stylesheet)
外部样式表是将 CSS 写在一个单独的
.css
文件中,然后通过 HTML 的 <link>
标签引入这个文件。这是最常用的方法,有利于样式的复用和维护。示例:
<!-- 在 HTML 文档的 <head> 部分引入外部样式表 --> <head> <link rel="stylesheet" type="text/css" href="styles.css"> </head>
在 "styles.css" 文件中:
body { background-color: lightgrey; } p { color: green; }
3. 内部样式表 (Internal Stylesheet)
内部样式表是在 HTML 文档内部的
<style>
标签中编写 CSS。这种方法适用于单个页面的样式设置,但不适合多个页面的样式共享。示例:
<head> <style> body { background-color: lightblue; } p { color: navy; } </style> </head>
4. 用户样式表 (User Stylesheet)
用户样式表是用户自己定义的 CSS 规则,用于修改或覆盖网页的默认样式。这通常在用户的浏览器设置中完成,而不是由网页开发者控制。
示例:
用户可以在浏览器的设置中定义自己的样式表,比如改变所有网页的默认字体或颜色。不过,这种方式的具体实现依赖于用户使用的浏览器和其配置
选择器
选择器是 CSS 中的一种模式,用于选择要设置样式的 HTML 元素。例如:
p { color: red; }
这里,
p
是一个选择器,表示选择所有的段落(<p>
标签),并将它们的文本颜色设置为红色。基本选择器
- 元素选择器:直接通过元素标签名选择,如
p
、div
。
- 类选择器:通过类名选择,以点(
.
)开始,如.classname
。
- ID 选择器:通过元素的 ID 选择,以井号(
#
)开始,如#idname
。
- 通用选择器:使用星号(
),选择页面上的所有元素。
2. 组合选择器
- 后代选择器:通过空格分隔,选择某个元素内的后代元素,如
div p
选择所有位于div
元素内的p
元素。
- 子选择器:通过大于号(
>
),只选择直接子元素,如div > p
。
- 相邻兄弟选择器:使用加号(
+
),选择紧接在另一元素之后的元素,如h1 + p
。
- 通用兄弟选择器:使用波浪号(
~
),选择某个元素之后的所有兄弟元素,如h1 ~ p
。
3. 属性选择器
- 存在和值属性选择器:如
[type="text"]
选择所有type
属性值为text
的元素。
- 部分值匹配选择器:如
[class^="btn"]
选择类名以btn
开头的所有元素。
4. 伪类选择器
- 动态伪类:如
:hover
、:active
,根据用户行为来选择元素。
- 结构伪类:如
:first-child
、:last-child
,根据文档结构来选择元素。
- 目标伪类:如
:target
,选择文档中的目标元素。
5. 伪元素选择器
- 用于选择元素的特定部分:如
::first-line
、::before
、::after
。
6. 分组选择器
- 多选择器分组:通过逗号分隔,同时应用一个样式规则到多个选择器,如
h1, h2, h3
。
变量
CSS 变量(或“自定义属性”)允许您存储一个值,然后在整个文档中重复使用这个值。例如:
:root { --main-bg-color: coral; } body { background-color: var(--main-bg-color); }
这里,
--main-bg-color
是一个变量,代表主背景颜色。在 body
选择器中,我们通过 var(--main-bg-color)
来使用这个颜色值。层叠
CSS 的全程,层叠样式表,其中层叠是他的核心概念
CSS 允许多个样式表影响同一个页面。当多个样式规则应用于同一个元素时,这些规则将“层叠”在一起,形成一组综合的样式。这种层叠机制允许不同来源的样式(如用户代理样式、用户定义的样式、作者定义的样式)合并,形成最终的样式。
优先级和继承
在这个层叠过程中,CSS 遵循特定的优先级规则来决定哪些样式是最终应用于元素的。如果有冲突,比如两个规则都试图设置同一个元素的颜色,CSS 的优先级规则(基于选择器的特异性、重要性和源代码中的顺序)将决定哪个规则胜出。此外,某些样式可以从父元素继承到子元素,这也是层叠过程的一部分。
小结
CSS 的概念并不多,引入方式,选择器,变量,层叠特性,优先级和继承。了解这些基本 CSS 已经没有什么秘密了,就只需要根据需要去选择样式属性了。但是正是由于 CSS 过于简单,导致他基本无法在大的项目中直接使用,也就有了一系列工具的出现,他们的出现则是为了解决 CSS 的若干问题。但是其最终都无法逃过上面的概念
接下来我们就逐一了解他们到底解决了什么问题,以及如何解决的。
Bootstrap
预定义 CSS 的鼻祖
Bootstrap 提供 CSS 样式主要是通过预定义的类名,这些类名可以直接应用于 HTML 元素来实现特定的样式和布局。使用 Bootstrap 的项目中,通常需要首先将 Bootstrap 的 CSS 文件包含进去,然后通过添加相应的类名到 HTML 标签来应用样式。
引入 Bootstrap
在项目中使用 Bootstrap 之前,需要先将其 CSS 文件引入到你的 HTML 中。这可以通过链接到 Bootstrap 的 CDN(内容分发网络)或下载 Bootstrap 文件到本地来实现。
通过 CDN 引入:
htmlCopy code <!DOCTYPE html> <html lang="en"> <head> <!-- 引入 Bootstrap CSS --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"> </head> <body> <!-- 你的内容在这里 --> </body> </html>
使用预定义的类
Bootstrap 的强大之处在于其丰富的预定义类,可以轻松实现各种布局和样式效果。例如,如果你想创建一个有着基本样式的按钮,你可以使用 Bootstrap 的按钮类。
示例: 创建一个按钮
htmlCopy code <button type="button" class="btn btn-primary">Primary Button</button>
在这个例子中,
btn
和 btn-primary
是 Bootstrap 提供的类。btn
提供了基本的按钮样式,而 btn-primary
则应用了主题颜色。小结
通过这种方式,Bootstrap 允许开发者快速搭建页面结构和样式不过,值得注意的是,虽然 Bootstrap 提供了快速搭建界面的便利,但在一些要求高度定制化设计的项目中,过度依赖 Bootstrap 可能会限制创造性和灵活性。
而且 Bootstrap 的预定义样式无论你用不用得到都会请求过来,在没有进行工程话优化的情况下会影响前端初次渲染的性能
而且 bootstrap 提供的是全局样式类,可能与你的自定义样式或其他第三方库的样式发生冲突。这可能导致意料之外的布局和样式问题。
BEM
由于 CSS 是一种全局命名方案,为了解决 CSS 命名互相冲突的问题
BEM(Block Element Modifier)是一种命名约定,用于在前端开发中创建可重用组件和代码共享。它的目的是通过明确的命名规则来帮助开发者快速理解一个元素的作用和关系,提高 CSS 的可维护性和可读性。BEM 由 Yandex 团队开发,现已广泛应用于各种网页和应用程序的开发中。
BEM 的组成
BEM 的命名约定由三个主要部分组成:Block(块)、Element(元素)和 Modifier(修饰符)。
- Block(块): 定义一个高级别的抽象或组件,如
button
、menu
、header
。它是一个独立的实体,可以在不同的上下文中重复使用。
- Element(元素): 定义 Block 内部的一部分,与 Block 紧密相关,如
menu item
、list item
。在命名时,元素名称与块名称之间通常使用两个下划线连接(__
),例如block__element
。
- Modifier(修饰符): 描述 Block 或 Element 的一个状态或变化,如
disabled
、highlighted
、size-big
。修饰符与它们修改的块或元素名称之间使用两个短横线连接(-
),例如block--modifier
或block__element--modifier
。
示例
假设我们有一个简单的按钮组件,它可以是大的或小的,并且有不同的颜色。
HTML:
htmlCopy code <button class="button button--large button--success">Large Success Button</button> <button class="button button--small button--danger">Small Danger Button</button>
CSS:
cssCopy code /* Block */ .button { /* ... */ } /* Element */ .button__icon { /* ... */ } /* Modifier */ .button--large { /* ... */ } .button--success { /* ... */ } .button--small { /* ... */ } .button--danger { /* ... */ }
优点
- 可读性和可理解性:通过明确的命名,可以快速了解元素的功能和相互关系。
- 重用性和组件化:BEM 有助于创建可重用的组件和模块。
- 降低样式冲突:通过命名规则限制样式的作用域,减少全局冲突。
- 方便维护:使代码更整洁和组织化,便于后期维护和更新。
缺点
- 冗长的类名:BEM 的命名可能导致类名较长。
- 学习曲线:对于初学者来说,理解和正确应用 BEM 命名规则可能需要一定的学习。
总的来说,BEM 是一种非常实用的 CSS 命名方法论,尤其适用于大型项目和团队协作,有助于保持代码的清晰性和一致性。但是很麻烦。
在 MUI 中可以看到类似这种命名方法的实现
<button class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium css-1hw9j7s" tabindex="0" type="button" > Contained<span class="MuiTouchRipple-root css-w0pj6f"></span> </button>
构建 css
上面说了那么多,但是实际上,我们现在的开发从来不会用 link 方式来引入代码,而是会在代码中使用 import 方式的方式引入 css ,这中间发生了什么呢?
import './style.css';
首先需要明确的一点,JS 模块系统是不会处理 css 文件的引入的。如果你直接在 js 中 import css 文件,会直接报错,告诉你无法处理 css 后缀的文件
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".css" for /Users/acring/Workspace/demo/test/style.css
在我们实际使用
import ‘./style.css’
需要依赖我们的构建系统,通常来说指的就是 webpack在 webpack 中配置对于 css 的处理很简单,首先下载 css 的 loader
css-loader
和 style-loader
npm install --save-dev css-loader style-loader
然后在
webpack
的规则配置中,增加对于 css 文件的处理规则,... module.exports = { ... module: { rules: [ ... { test: /\.(css)$/, use: ['style-loader', 'css-loader'], }, ], }, ... };
处理流程也非常简单
- Webpack 读取入口文件(通常是 JavaScript 文件)。
- 当遇到
import './style.css'
这样的语句时,Webpack 使用css-loader
来处理 CSS 文件。
css-loader
解析 CSS 文件中的所有依赖(如@import
和url()
)。
- style-loader:将 CSS 注入到 DOM 中,通过创建
<style>
标签并附加到 HTML 文件的头部。
而有了 webpack 这类构建工具之后,明显打开了大家处理 css 的思路,于是在 15 年左右, css module 被提出来了
CSS Module
我们在开发的时候应该写过这种代码
Button.module.css
:cssCopy code /* Button.module.css */ .button { background-color: blue; color: white; }
在 JavaScript 文件中使用:
javascriptCopy code import styles from './Button.module.css'; console.log(styles.button); // 输出:_23_aKvs-b8bW2Vg3fwHozO
在 HTML 中应用:
htmlCopy code <button class={styles.button}>Click me</button>
在这个例子中,
.button
类在构建时会被转换成一个唯一的标识符,如 _23_aKvs-b8bW2Vg3fwHozO
。这就是 css module 的思想,通过将 className 生成为唯一标志符来让 css 的 className 不再出现冲突。
而实现原理则是依赖于 webpack 的 css loader
module.exports = { // ... 其他配置 ... module: { rules: [ { test: /\.css$/, // 匹配 CSS 文件 use: [ 'style-loader', { loader: 'css-loader', options: { modules: true, // 启用 CSS Modules localIdentName: '[name]__[local]___[hash:base64:5]', // 定义生成的类名格式 } } ] } ] } };
虽然 CSS Modules 解决了命名空间的问题,但是 CSS 和 HTML 依然是分开的,由于 React 中将 JS 和 HTML 写在一起的习惯,在 React 中,更加流行的一种解决方案是 CSS IN JS
CSS IN JS
CSS in JS 代表性的库叫 @emotion ,他的使用例子如下
/** @jsxImportSource @emotion/react */ import { css } from '@emotion/react'; // 创建一个简单的样式 const buttonStyle = css` padding: 10px; background-color: hotpink; font-size: 14px; border-radius: 8px; color: white; &:hover { color: black; } `; function App() { return ( <div> <button css={buttonStyle}>点击我</button> </div> ); }
CSS-in-JS 的优点包括:
- 局部作用域:样式是局部的而不是全局的,这意味着它们只应用于特定的组件,减少了样式冲突的可能性。
- 动态样式:可以根据组件的状态或属性动态生成样式。
- 维护性和可重用性:将样式与组件逻辑捆绑在一起,有助于组织代码和提高可重用性。
- 依赖管理:通过 JavaScript 的模块系统来管理样式依赖,使得样式模块化和易于管理。
CSS IN JS 的实现原理相对来说复杂一点。
简单来说有几个方面
- 运行时插入:Emotion 在运行时将生成的 CSS 插入到 DOM 中。它使用
<style>
标签动态地将 CSS 规则添加到文档的<head>
部分。这使得样式可以根据组件的渲染和更新动态地改变
- CSS Modules:Emotion 生成的样式类似于 CSS Modules,每个样式都具有唯一的标识符,这减少了全局命名冲突的风险。。
CSS in JS 将组件的状态逻辑和 CSS 关联起来,确实提高了开发的效率,并且也解决了 CSS 作用域的问题,所以 CSS in JS 是 React 项目非常喜欢使用的一种 CSS 解决方案。当然 CSS in JS 也有他的缺点,例如
- 性能影响: 将 CSS 注入 JavaScript 可能会增加运行时的负担,尤其是在大型应用中。这可能导致较慢的加载时间和较高的 JavaScript 文件大小。
- 学习曲线: 对于习惯于传统 CSS 的开发者来说,CSS-in-JS 可能需要一定的学习和适应。它需要对 JavaScript 和特定库的深入理解。
- 工具链依赖: 大多数 CSS-in-JS 解决方案依赖于特定的构建工具和编译步骤(例如 Babel 插件),这可能增加项目的复杂性。
小结
自从前端构建出来之后,CSS Module 和 CSS in JS 成为了 CSS 主流的解决方案,两者都解决了 CSS 全局命名冲突的问题,让 CSS 更好管理,其根本原理都是通过生成唯一的 className ,让 CSS 命名不再冲突。
CSS in JS 相比 CSS Modules 在 React 生态中更受欢迎,他让 CSS 能够和组件结合,根据组件的状态动态的调整 CSS,但是也带来了一定的性能问题
同时我花费时间描述 CSS 的发展史史为了告诉大家 CSS 并没有什么魔法,任何解决方案不过是通过构建系统或者脚本对 CSS变量, className 等进行处理,生成最终的结果,来解决 CSS 无法解决的问题, tailwind 也是如此。了解了这些,可能能让我们更加自信的接触 tailwind css。
Tailwind CSS
假设我们想创建一个带有圆角、阴影、内边距和中心文本的按钮。在传统的 CSS 中,你可能需要写一些类似这样的 CSS 代码:
cssCopy code .button { padding: 8px 12px; border-radius: 4px; background-color: blue; color: white; box-shadow: 0 2px 4px rgba(0,0,0,0.2); text-align: center; }
然后在 HTML 中这样使用:
htmlCopy code <button class="button">点击我</button>
在 Tailwind CSS 中,你不需要写这些 CSS。相反,你可以直接在 HTML 元素上应用工具类:
htmlCopy code <button class="px-3 py-2 rounded bg-blue-500 text-white shadow-md text-center"> 点击我 </button>
在这个例子中,
px-3
和 py-2
分别设置了按钮的水平和垂直内边距,rounded
添加了圆角,bg-blue-500
设置了背景颜色,text-white
设置了文本颜色,shadow-md
添加了中等大小的阴影,而 text-center
则确保了文本居中。从这个例子我们可以看出来, tailwind 和 bootstrap 还有点类似,都是预定义样式,但是理念完全不同,tailwind css 的样式并不是通过组件来组织,而是根据项目中会用到的一些原子样式来定义样式规则。
虽然 tailwind 乍一看好像也差不了多少,而且可以预见的是,当一个组件的样式越来越多,他的 className 会非常非常的长,那不是更加难看吗?没关系,在后面我们会慢慢解决这些疑问。
优点一: 基于样式系统
可能乍一看 tailwind 会让人觉得这不过就是将内联样式的代码写到了 className 中,但是实际上这有一个根本性的不同,tailwind 的 className 是通常事先确定好的,而不是可以随意定制的变量,而根本原因就是 tailwind 是样式系统优先的。
tailwind css 定义了一套默认的样式系统,包括颜色、字号、字体、布局、圆角、阴影等一系列规则,我们也可以定制这些规则,例如我们,我们的主色是 紫色,那我们可以在 tailwind 配置中声明,
primary
对应的颜色为紫色,之后我们才可以在 className
中使用 text-primary
来设置文字的颜色,或者使用 bg-primary
设置背景的颜色为theme: { extend: { colors: { primary: 'rgba(120, 85, 250,1)', },
<div className="text-primary bg-primary" ></div>
也就是先定义我要使用哪些值,再使用。
在 pluto 中,我们使用的是 mui 提供的样式系统方案,而如果需要在组件内使用某一个样式系统的变量,我们需要先导入样式系统对象,再获取对应的值,虽然做到了同样的结果,但是这样的方式明显更加麻烦
import themeCSSObj from "~/styles/theme.module.scss"; <Typography key={index} css={{ padding: "6px 16px", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", background: themeCSSObj.grey2, }} type="t1" > {menuTitle} </Typography>;
相比 mui 或者组件库提供的样式系统, tailwind 的样式系统没有侵入性,所有的样式都是基于 css 样式,不会影响 js 代码,或者依赖某一种框架,在html,jsx, react, vue 中。
优点二: 原子化带来的体积优化
在过去的的 css 开发中,还有个非常突出的问题,就是当项目的的体积扩大的时候,css 样式表的体积会成倍的增加,不同于业务开发时可以复用组件,在业务开发中的 css 只要定义了一个新的页面就无法避免的会引入新的 css。
而当使用 tailwind 时,一个
text-primary
className 可以被使用不同的组件,不同的业务代码,而其背后的样式表代码只有一个 .text-primary{ color: xxx }
所以哪怕项目不断的扩张, tailwind 的 css 样式代码的扩张相对来说是可控的。
优点三: 支持响应式设计,支持状态样式
每个Tailwind中的实用类都可以在不同的断点条件下应用,这使得构建复杂的响应式界面变得轻而易举,而无需离开HTML。
下面这个是在 md 和 lg 断点下图像的宽度设置
<img class="w-16 md:w-32 lg:w-48" src="...">
Breakpoint prefix 断点前缀 | Minimum width 最小宽度 | CSS |
sm | 640px 640像素 | @media (min-width: 640px) { ... } |
md | 768px 768像素 | @media (min-width: 768px) { ... } |
lg | 1024px 1024像素 | @media (min-width: 1024px) { ... } |
xl | 1280px 1280像素 | @media (min-width: 1280px) { ... } |
2xl | 1536px 1536像素 | @media (min-width: 1536px) { ... } |
当然,断点也可以根据我们的需要去自己定义,这样就不需要手动修改媒体查询的变量了,只需要知道 sm md lg 这些断点名称
而对于组件状态的样式定义,在 tailwind css 也非常简单
这样就是将当前元素的 hover 状态下的背景色设置为
sky-700
,<button class="bg-sky-500 hover:bg-sky-700 ..."> Save changes </button>
当然每一个 hover 后的样式必须重复写,例如想要在 hover 时同时应用背景色和文本颜色,需要写两遍 hover ,这也是稍微有些麻烦的地饭
<button class="bg-sky-500 hover:bg-sky-700 hover:text-primary"> Save changes </button>
基本使用
当我们刚开始使用 tailwind css 时,肯定会遇到不知道怎么设置某个样式的时候,这时候最快的方法就是在官方文档中搜索
例如我想知道 tailwind 中怎么设置字号,font-size,那我们可以在 https://tailwindcss.com/docs/installation 页面下按
command + k
搜索 font-size
然后回车进入页面,就可以看到对应的 className 和对应的变量值。以及一些示例。
例如这里可以看到
text-xs
不仅设置字号还会同时设置行高。下面则会告诉你如何在设置了字号之后修改行高这里的
text-xl/8
会把行高设为 2rem
,为什么呢,这在 line-height
的介绍页面会告诉你所以其实所有不知道该怎么写的样式都可以在 tailwind 官方文档中搜索到相应的使用方式。
group-{modifier}
在 tailwind 中,有些样式规则看上去是不太好实现的,比如我希望 hover 父元素时,子元素的样式发生变化,好在 tailwind 都提供了对应的解决方案。这里以 group 为例,说明如何实现基于父元素的状态修改子元素的样式。
假如我们希望 hover 父元素的时候修改父元素的背景色,同时修改子元素的文字颜色,首先需要在父元素上加上
group
class ,然后在子元素上加上 group-hover:text-white
,就可以将让父元素被 hover
到时候修改子元素的文字颜色为白色<a href="#" class="group hover:bg-sky-500"> <div class="flex items-center space-x-3"> <svg class="group-hover:stroke-white" fill="none" viewBox="0 0 24 24"><!-- ... --></svg> <h3 class="group-hover:text-white text-sm font-semibold">New project</h3> </div> <p class="text-slate-500 group-hover:text-white text-sm">Create a new project from a variety of starting templates.</p> </a>
注意,这里并没有所谓的什么作用域存在,有的只是单纯的样式规则,也就是说如果我在 React 写成
function Text() { return ( <div className="group mt-2 h-5 w-5 bg-blue-500"> <div className="group-hover:text-white">测试2</div> </div> ); } <div className="group h-10 w-10 bg-red-500"> <div className="group-hover:text-white">测试</div> <Text></Text> </div>;
本意上我或许希望的子组件能有自己的作用域,但实际上并不会,当 hover 最外层的 group 时,所有的写作
group-hover
的元素都会受到影响记住, tailwind 并没有引入太多的神奇概念,只是根据规则生成
class
和样式。例如上面的例子中
group-hover:text-white
生成的样式为.group:hover .group-hover\:text-white { --tw-text-opacity: 1; color: rgb(255 255 255 / var(--tw-text-opacity)); }
通过生成
.group:hover .group-hover\:text-white
的规则来达到 hover 父元素时修改子元素的结果。 group
这个关键字只是为了告诉 tailwind css ,这里有一个需要特殊对待的规则。当然对于上面这种情况, tailwind 也提供了解法,就是给
group
增加额外的标志function Text() { return ( <div className="group/text mt-2 h-5 w-5 bg-blue-500"> <div className="group-hover/text:text-white">测试2</div> </div> ); } <div className="group/container h-10 w-10 bg-red-500"> <div className="group-hover/container:text-white">测试</div> <Text></Text> </div>;
在上面的代码中,在
group
后面用 /text
和 /container
区分了不同的 组group
是必须的,需要给 tailwind
来识别规则,而后面的标志可以自己定。这种情况下生成的样式规则如下
.group\/text:hover .group-hover\/text\:text-white { --tw-text-opacity: 1; color: rgb(255 255 255 / var(--tw-text-opacity)); }
因此我们最好不要在 业务中直接使用 group ,最好在组件级别,业务组件级别使用。并最好给一个额外的标志来区分
除了
group
还有 peer
(处理同级之间的样式) , has-modifier
直接修改子元素样式等,如果遇到类似的问题都可以通过 tailwind 官方文档找到答案。这里就不多赘述了。基本原理
为了让大家更加理解 tailwind 的工作,防止大家在学习时因为不懂原理而造成困惑,气馁。
生成 CSS 类的过程
- 基于配置的类生成:
- 配置文件:Tailwind CSS 的核心是其配置文件(
tailwind.config.js
)。这个文件包含了定义颜色、间距、字体大小、断点和其他各种设计令牌的默认设置。 - 实用程序类:基于这些配置,Tailwind 自动生成一系列的实用程序类。例如,对于间距(margin 和 padding),Tailwind 会生成如
m-1
,p-2
等类,其中数字代表配置中定义的间距大小。
- PostCSS 插件:
- PostCSS 的功能是读取当前的 CSS 文件,输出新的 CSS 文件,一个最常见的用法是
autoprefixer
,也就是自动给 CSS 的字段加上兼容性前缀。 - Tailwind 作为 PostCSS 插件运行。当你在 CSS 文件中使用
@tailwind
指令时,Tailwind 会将这些指令替换为一组实用程序类。 - 这个转换过程发生在构建时,意味着最终的 CSS 文件包含了所有这些生成的类。
- 响应式和状态变量:
- Tailwind 还根据配置文件中的断点生成响应式版本的实用程序类,如
md:text-center
表示在中型设备上文本居中。 - 类似地,也支持状态变量类(如悬停或聚焦状态),例如
hover:bg-blue-500
。
集成清理工具(如 PurgeCSS)
由于 Tailwind 自动生成大量的实用程序类,这可能导致最终的 CSS 文件非常庞大。为了解决这个问题,Tailwind 集成了清理工具,比如 PurgeCSS,以优化生产环境中的 CSS 文件大小。
- 分析项目文件:
- 根据 tailwind.config.js 中的 content 字段, PurgeCSS 分析项目的 HTML、JavaScript(或任何标记语言文件)。
- 它检查这些文件中实际使用的 CSS 类,并创建一个使用的类列表。
- 移除未使用的类:
- 然后,PurgeCSS 会比较这个列表与 Tailwind 生成的 CSS 文件。
- 它移除那些在项目中没有使用的类,这大大减少了最终 CSS 文件的大小。
- 优化生产环境:
- 在开发环境中,为了方便调试和快速开发,通常会保留所有生成的类。
- 在生产环境的构建过程中,通过使用 PurgeCSS 或 Tailwind CSS v2.0 及更高版本中的内置功能(JIT 模式),可以自动执行这个清理过程。
可以看到 tailwind 的基本原理并不难, 最终的结果就是生成对应的 className ,所以当一个样式不生效,只可能是 tailwind 没有生成这个样式规则,或者你的 className 写错了。
小结
从 CSS Module 到 CSS In JS 再到 Tailwind ,会发现确实换一种编写样式的思路给我们打开了一个新的大门, Tailwind CSS 解决了 CSS 样式规则再大项目中无限扩张的问题,并且以样式系统优先,让我们定义整个项目的样式的方式更加规范。当你在一个新的项目中使用 tailwind 去开发的时候你会立刻体会到开发效率的快速提升。
当然,我们还没提到 tailwind 的问题,接下来我会从最佳实践的角度指出 tailwind 在实践中的问题以及对应的解决思路。
Tailwind 最佳实践
使用 Tailwind CSS IntelliSense 增加语法提示
在使用 JS 的时候我们已经习惯了语法提示,如果编写
tailwind
的 className
没有语法提示,那编写 tailwind
的效率确实很难提示,因为你不知道到底写到对不对。好在由于
tailwind
的原理就会根据配置生成样式规则的列表,所以有哪些样式规则都是已知的,我们只需要在 vscode
中下载 Tailwind CSS IntelliSense
插件,他就可以在你输入 className
时自动提示和补全同时他还能在你
hover
对应的 class
时告诉你对应的样式所以这个插件是使用
tailwind
时必装的插件Tailwind merge
有聪明的同学应该会发现一个问题,那我如果将两个设置文字颜色的
class
写到一起会怎么样,是放在后面的会生效吗?还是前面的会生效?<div className='text-red-500 text-blue-500'>测试</div>
答案是,不一定,
根据 CSS 层叠样式表的规则,当优先级相同时,在后面的规则会优先
.text-blue-500{ color: blue; } .text-red-500{ color: red; } <div class="text-red-500 text-blue-500">测试</div>
上面这种情况的文本颜色是红色, 而如果样式表的 className 上下顺序反过来,则会是蓝色。
而这个生成的样式表的顺序,完全取决于 tailwind 的喜好,或许是基于配置的顺序,总之没办法控制。
所以我们需要额外的处理方法,解决冲突样式规则的问题。
这就是 tailwind-merge ,他可以高效的合并
tailwind
的样式,让后面的样式规则生效(其实是删除掉了前面的样式规则 class)clsx
当我们要在动态的修改 className 时总会比较麻烦,尤其是在 React 中根据状态改变样式时
不使用 clsx
不使用
clsx
时,你可能需要手动拼接字符串来处理类名的变化。这在条件较多或类名较复杂时可能会变得笨拙和容易出错。jsxCopy code import React from 'react'; function TextComponent({ isRed }) { const textColorClass = isRed ? 'text-red' : 'text-blue'; return <div className={textColorClass}>这是一段文本</div>; }
在这个例子中,
text-red
和 text-blue
是预先定义在 CSS 中的类。根据 isRed
的值,组件会选择相应的类。使用 clsx
使用
clsx
,你可以更简洁、更清晰地处理类名。clsx
允许你以更声明式的方式组合类名,使代码更易于阅读和维护。jsxCopy code import React from 'react'; import clsx from 'clsx'; function TextComponent({ isRed }) { return <div className={clsx({ 'text-red': isRed, 'text-blue': !isRed })}>这是一段文本</div>; }
在这个例子中,
clsx
根据 isRed
的值动态地添加 text-red
或 text-blue
类。这种方式使得代码更简洁,且当处理更复杂的类名组合时,它的优势更加明显。cn
在行业中,通常我们会把这两个函数组合起来,命名为 cn,方便在项目中使用
export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }
import React from 'react'; import clsx from 'clsx'; function TextComponent({ isRed }) { return <div className={cn({ 'text-red': isRed, 'text-blue': !isRed })}>这是一段文本</div>; }
这样我们就不需要自己处理 tailwind 的冲突和名称拼接的问题了。
这个函数可以从 @eris/ui 中导出 `import { cn } from '@eris/ui';`
cva
其实有了上面两个工具,基本可以应付大部份的业务开发了,但是我还是介绍一个在组件开发时更加合适的工具。他可以用更加结构化的方法来定义和应用样式类的变体
下面是使用 cva 实现的
button
组件import React from 'react'; import { cva } from 'cva'; // 定义按钮的基本类和变体 const buttonCva = cva('btn', { variants: { size: { small: 'btn-small', medium: 'btn-medium', large: 'btn-large' }, status: { default: 'btn-default', active: 'btn-active', disabled: 'btn-disabled' } }, defaultVariants: { size: 'medium', status: 'default' } }); function Button({ size, status, children }) { // 使用 cva 生成类名 const className = buttonCva({ size, status }); return <button className={className} disabled={status === 'disabled'}>{children}</button>; }
可以看到相对来说定义的逻辑非常的清晰,有哪些变量,以及变量对应的样式类都很容易看出来,相比之下用 clsx 的实现的例子
import React from 'react'; import clsx from 'clsx'; function Button({ size, status, children }) { // 根据 size 和 status 属性计算类名 const className = clsx( 'btn', { 'btn-small': size === 'small', 'btn-medium': size === 'medium' || !size, // 默认值 'btn-large': size === 'large', 'btn-default': status === 'default' || !status, // 默认值 'btn-active': status === 'active', 'btn-disabled': status === 'disabled' } ); return <button className={className} disabled={status === 'disabled'}>{children}</button>; }
可以看到,相比之下很难看出状态和样式的关系,并且相比之下更不好维护。
所以推荐在开发复杂的业务组件时采用 cva 的写法,将样式与状态的关系单独维护
className 太长
当一个元素的样式太多时, classname 会变得非常的长,对于强迫症来说,很难接受
cn( 'text-body inline-flex cursor-pointer select-none items-center rounded-sm border border-solid font-normal outline-none transition disabled:cursor-not-allowed disabled:shadow-none', ),
我个人比较推荐的写法是将 className 按分类写在不同的字符串中,这时候 prettier 就会帮我们自动换行,让 classname 能够在一个屏幕中显示
cn( 'text-body inline-flex cursor-pointer select-none items-center rounded-sm ', 'border border-solid font-normal outline-none transition', 'disabled:cursor-not-allowed disabled:shadow-none', ),
其他
还有一些细节内容,例如 可以安装 prettier 的 tailwind 插件,让 prettier 帮助我们自动对 classname 进行排序分类,配置 vscode settings ,让 Tailwind CSS IntelliSense 能够在 cn 函数 中也实现自动补全。这些就不多赘述了,感兴趣的可以自己了解一下。
设计与代码协作
接下来是最重要的一点,关系到业务中的
className
编写,大家认真听一下。我们的
tailwind
配置是经过自定义的,也就是说 tailwind 文档中的 className 并不完全适用,那我们如何根据设计图,知道对应的我们需要使用的 className
呢其实非常简单,目前 eris 和 设计的变量系统是对齐的,也就是说,在设计系统看到的变量直接对应的就是 tailwind 中自定义配置的变量。
首先我们需要使用
Dev Mode
来查看设计图,这样能够更快的查看到元素的样式。以一个文本为例,用
Dev Mode
查看一个文本的 CSS 样式,会看到下面的 CSS 代码,color: var(--text-2, #505774); text-align: center; /* Regular/14 Body-段落间距4px */ font-family: PingFang SC; font-size: 14px; font-style: normal; font-weight: 400; line-height: 22px; /* 157.143% */
其中关键的有几个部份
- 文本的颜色
- 文本的字号和行高
- 文本的字重
颜色
颜色非常好识别,这里的
—text-2
说明对应的变量名称为 text-2
,那么他在 tailwind 中对应的写法是 text-text-2
,注意不是 text-2
,第一个 text
是 tailwind
用来识别需要设置文本的颜色或者字号,第二个才是颜色的变量名称,合起来才是最终的 className
,也就是 text-text-2
字号和行高
比较特殊的是文本的字号和行高,由于 Figma 中暂时不支持将字号和行高设为变量,所以只能通过看注释的方式,和设计约定了常用的文字用法,我们可以查看注释中 红色背景的部份
/* Regular/14 Body-段落间距4px */
假如是
body
,则对应的 tailwind className
为 text-body
,如果是 caption
则对应的是 text-caption
下面是 tailwind 配置中具体的名称和数值,第一个字号,第二个是行高
fontSize: { caption: ['12px', '20px'], body: ['14px', '22px'], subhead: ['16px', '24px'], title: ['20px', '28px'], number: ['24px', '32px'], bignumber: ['32px', '40px'], extralarge: ['40px', '48px'], },
字重
字重还没有绑定变量,用的是
tailwind
默认的配置, 400
对应的是 tailwind
中的 font-normal
其他的可以根据 tailwind
官方文档找到对于的值Class | Properties |
font-thin | font-weight: 100; |
font-extralight | font-weight: 200; |
font-light | font-weight: 300; |
font-normal | font-weight: 400; |
font-medium | font-weight: 500; |
font-semibold | font-weight: 600; |
font-bold | font-weight: 700; |
font-extrabold | font-weight: 800; |
font-black | font-weight: 900; |
阴影
由于 figma 不支持阴影作为变量,阴影也是需要特殊对待的样式,例如下面的 阴影,我们也需要根据注释来找到对应的
tailwind
值/* Elevation 3/bottom */ box-shadow: 0px 6px 20px 0px rgba(0, 0, 0, 0.06), 0px 4px 15px 5px rgba(0, 0, 0, 0.03), 0px 4px 10px 2px rgba(0, 0, 0, 0.05);
上面的
/* Elevation 3/bottom */
对应的 tailwind className
是将 空格
和 /
替换为 -
之后的结果,然后在最前面加上 shadow
表示要设置为阴影<div className="shadow-elevation-3-bottom"></div>
在从设计图到 tailwind className 基本可以遵照两个原则,如果看到变量,则使用【前缀】+【变量名称】 的方式设置 className,否则尝试将注释转成合法的 className。
小结
通过将设计系统的变量和 tailwind 样式系统的变量统一名称,设计和开发拥有了一套统一的样式系统规范,我们可以快速通过设计图知道对应的
tailwind className
应该是什么,大大降低了设计和开发的沟通成本,开发也不需要拿着变量的值去代码里面找变量名了。减轻了开发的负担。但由于 figma 变量系统的不完善,在某些情况下还需要自己去拼接对应的 tailwind className
总结
CSS 最初的设计非常简单,样式选择器,层叠规则,优先级,变量,知道这几个基本概念就能上手。但是在大项目的开发中,由于 CSS 的设计过于简单,就出现了一系列问题,缺乏命名空间,缺乏样式规范,编写麻烦,容易编写重复代码,大项目中 CSS 会不断增长等等。而为了解决这些问题,社区中提出了一系列的解决方案,而目前最流行的就是 tailwind css ,他从样式系统,到命名空间,到性能优化,都利用实用主义优先的思想,通过原子化的 CSS 类得到了一定程度的解决。
所以我们提倡将 tailwind 作为我们新的样式编写规范,在新项目中尽量尝试使用 tailwind 进行样式开发。这会大大提高业务开发,以及设计协作的效率。
当然,编程领域没有“银弹”, tailwind 也不过是解决方案中的一种,更重要的是我们需要理解其中的原理,从不同的解决方案中吸收思想,才能更好的进行开发。