tailwind 学习笔记
什么是tailwind
上一张图,tailwind 官网对自己的定位分析。
Tailwind 是一个 CSS UI 框架,它最大的特点是遵循原子类优先 utility-first 的原则,只提供了基础的原子类(如内边距 padding、字体 text 和 font、动效 transition 等预设类),可以直接在 HTML 源码上使用这些基础类,通过编译器的编译,生成相应的样式表,从而为页面设置相应的外观样式。
从 NPM.DEVTOOL 的标签中可以看出: 每个月 npm 下载量高达 300 万次,jsDelivr 下载量更是高达 1800 万次,Star 数也即将突破 80 K,依赖于它的 Packge 及 Github Repo 更是成千,足见其受欢迎程度。
许多企业,诸如 网飞 Netflix、 脸书 Facebook、OpenAI、Shopify、字节等均有使用tailwind css。
四大特性: 原子化、语义化、统一性、响应式
原子化类
先谈谈前端css颗粒度,通常划分为以下四层
<div style="{ borderRadius: '0.5rem', padding: '1rem' }">Click</div>
<div class="rounded-lg p-4"> Click </div>
<div class="button"> Click </div>
<Button> Click </Button>
越往下走,颗粒度越来越大,约束性变高,自由性不足。而 tailwindcss 位于第二层
组合Tailwind 提供的原子类,无需离开 HTML 文档,就实现复杂的页面样式设计。
在正常的开发中,你需要给一个元素加左边的边距是这样的:
.class{
margin-left: 10px
}
或者是这样的:
<div style="margin-left:10px" ></div>
但是在Tailwind里面,它是这样的
css
<div class="ml-10" ></div>
原子类最大的好处:
- 不浪费精力去取类名。
- CSS 文件停止增长。
- 全局修改变得更容易。
语义性强
text-lg、text-white、ring、animate-spin 这些类名望文生义,一目了然。
统一性
对于手写 CSS 一大痛点就是命名,即要考虑到语义化,还要考虑到复用,还要去想 HTML 结构。取名不规范甚至会导致样式冲突,比如我习惯
.center { align-content: center }
但别的项目可能定义为 .text-center
好比 一千个读者,就有一千个哈姆雷特,一千个前端,就写过一千个 .center 的类。
响应式设计
Tailwind 提供五个默认的断点
sm640pxmd768pxlg1024pxxl1280px2xl1536px
断点都是基于 min-width 最小宽度的,这是mobile first 原则,即默认样式就是移动端小屏的样式,然后各断点是指当页面宽度大于特定值时,才将样式响应式地进行变化。
举个栗子
<div class="container">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
如果通过 grid 布局很容易实现,但未免繁琐
@media (min-width: 1024px) {
.container {
grid-template-columns: repeat(3,minmax(0,1fr));
}
}
@media (min-width: 768px) {
.container {
grid-template-columns: repeat(2,minmax(0,1fr));
}
}
.conainer {
display: grid;
gap: 1rem;
}
那使用 tailwind 呢? 只要一行,直接高效
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"></div>
为了更好的理解tailwindcss 的特点 ,我们再来看个完整的栗子,实现如下消息通知效果:
一般的写法是:
<div class="chat-notification">
<div class="chat-notification-logo-wrapper">
<img class="chat-notification-logo" src="/img/logo.svg" alt="ChitChat Logo">
</div>
<div class="chat-notification-content">
<h4 class="chat-notification-title">ChitChat</h4>
<p class="chat-notification-message">You have a new message!</p>
</div>
</div>
<style>
.chat-notification {
display: flex;
max-width: 24rem;
margin: 0 auto;
padding: 1.5rem;
border-radius: 0.5rem;
background-color: #fff;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.chat-notification-logo-wrapper {
flex-shrink: 0;
}
.chat-notification-logo {
height: 3rem;
width: 3rem;
}
.chat-notification-content {
margin-left: 1.5rem;
padding-top: 0.25rem;
}
.chat-notification-title {
color: #1a202c;
font-size: 1.25rem;
line-height: 1.25;
}
.chat-notification-message {
color: #718096;
font-size: 1rem;
line-height: 1.5;
}
</style>
这就是一个典型的 「Semantic CSS」 命名方式:为不同的 html 标签定义语义化的 class 名字,然后每个 class 中包含应用到对应 html 标签上的所有 css 样式。
但是,随着项目的开发过程, 这种 css 规范会让 css 的维护成本越来越高:
- 命名困难。越来越多的相似语义化场景,会导致越来越多形如 aa-title、bb-title、bb-b1-title、aa-content、bb-content 的 class 命名。开发人员一边需要保证 aa、bb、bb-b1 这样的名称能准确表达语义,一边需要避免 css 全局作用域带来的冲突问题。
- 难以复用。css 样式很难通过语义化命名的 class 进行复用,因为一个 class 中包含了多条 css 样式,而多条 css 样式即使在同一语义环境下,也会因受到更大的上下文的影响,导致部分样式的差异化而无法直接复用 class。例如,企图通过 title、header-title 这类 class 命名来实现 「标题」语义下的 css 复用肯定是行不通的。长期以往,势必会出现更多的类似名称的 class :nav-title、nav-min-title、sider-title ... 而这些 class 很可能只是其中一条 css 规则不同,例如 font-size。
- 重构成本高。即使是将所有字号增加 2px 这类需求,在「Semantic CSS」规范下,都需要修改大量文件才能实现。
- css 文件大小膨胀。每个 class 都包含大量重复的 css 样式,无法解决复用性,这些问题都会导致随着项目需求的增加, css 文件变得越来越大。
我们来看下,使用 Tailwind CSS 实现同样的示例:
<div class="p-6 max-w-sm mx-auto bg-white rounded-xl shadow-md flex items-center space-x-4">
<div class="flex-shrink-0">
<img class="h-12 w-12" src="/img/logo.svg" alt="ChitChat Logo">
</div>
<div>
<div class="text-xl font-medium text-black">ChitChat</div>
<p class="text-gray-500">You have a new message!</p>
</div>
</div>
对比第一版,有以下区别:
- 没有自定义任何的 css class,使用的所有的 css class 都直接来源于 Tailwind CSS,这样就没有了命名的困扰问题,同时也解决了 css 膨胀的问题。当然 html 体积也变大了,但是因为 class 中使用的是有限集合内的、高度重复的 class 名称,在 Gzip、Brotli 这些压缩算法的作用下,是可以基本忽略的。
- 每一个 class 一般只对应一条 css 规则,如 p-6 对应 padding: 1.5rem,h-12对应 height: 3rem,原子类的颗粒度自然更容易在其他地方复用,而且原子化的 css 规范/思想,强制开发人员在为 html 标签定义样式时,写全所有需要的 class ,大大减少了不同 html 标签的 class 之间的相互影响。
- 「Atomic/Utility-First CSS」的使用,让样式重构/整体修改变得更加容易。我们可以通过覆盖原子颗粒度的 class ,变更应用的整体样式,例如,覆盖 text-xl 为 2rem,这样所以使用到 text-xl class 的字体大小都会变成 2rem。
Q1: “这跟在项目中直接全局写类名然后使用,有什么区别呢?”
其实我们在搭建tailwind的项目过程中就可以发现,tailwind存在JIT引擎(Just-In-Time),就是在编译过程才去扫描html文件,在这个过程中去识别使用了哪些类名,然后才生成对应的样式。
相比于预先直接全局写好大量的类名,JIT机制的优点在于精简紧凑,样式所占用的空间较小,因为只有用到了才会生成相应的文件。
同时tailwind对于类名的规则约定,也可以说是一种写类名的规范与统一,统一了团队内不同的成员甚至是不同的团队之间对于类名的书写与阅读。这给团队带来的效益,从长远来看利是远远大于弊的。
Q2:「原子级别」类 导致页面充斥大量相同类如何解决?
为了避免代码冗余以及后续维护方便,Tailwind 提供 @apply指令。
可以将这些相同的一系列基础类提取出来,「汇总」在一个新的更抽象的类名下 。
html
<!-- 原始的按钮元素 -->
<!-- 📄 index.html -->
<!-- Before extracting a custom class -->
<button class="py-2 px-4 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75">
Save changes
</button>
css
/* 📄 styles.css */
@layer components {
/* 将基础类组合抽取出来 */
.btn-primary {
@apply py-2 px-4 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75;
}
}
html
<button class="btn-primary">
Save changes
</button>
Q3: 预设类如何实现个性化需求?
tailwind 支持多种自定义样式的方法。
自定义基础类
可以在配置文件 tailwind.config.js 的主题属性 theme中定制颜色、间隔、字体、断点等基础类。而且可以通过覆写默预设值和扩展添加新值两种方式。
js
module.exports = {
theme: {
// 直接在 theme 属性中设置的基础类会完全覆盖原有的预设基础类
screens: {
sm: '480px',
md: '768px',
lg: '976px',
},
colors: {
'blue': '#1fb6ff',
'pink': '#ff49db',
'orange': '#ff7849',
},
fontFamily: {
sans: ['Graphik', 'sans-serif'],
serif: ['Merriweather', 'serif'],
},
// 在 theme 属性的 extend 属性下添加的基础类会以扩展添加新值的方式来添加自定义基础类
extend: {
spacing: {
'128': '32rem',
'144': '36rem',
},
borderRadius: {
'4xl': '2rem',
}
}
}
}
使用任意值
Tailwind 支持以 utility-[value] 的形式来临时使用任意样式值,其中 utility 表示基础类,后面用 - 连字符相连的,在方括号 [] 中的 value 就是该样式的值。而且这种形式也支持使用状态变量,例如 hover:utility-[value] 设置在鼠标悬停时元素的样式.
html
<!-- 将字体大小设置为 22px -->
<div class="text-[22px] lg:top-[34px]">Hello World</div>
插件添加 CSS
还可以通过编写插件方式来添加自定义样式(而不是使用样式表)。在 Tailwind 的配置文件 tailwind.config.js 中引入 tailwindcss/plugin 模块的 plugin 方法创建插件。它提供了一些钩子函数,用以将自定的样式添加到 base、components 或 utilities 中
js
// 📄 tailwind.config.js
const plugin = require('tailwindcss/plugin')
module.exports = {
// ...
plugins: [
plugin(function ({ addBase, addComponents, addUtilities, theme }) {
addBase({
'h1': {
fontSize: theme('fontSize.2xl'),
},
'h2': {
fontSize: theme('fontSize.xl'),
},
})
addComponents({
'.card': {
backgroundColor: theme('colors.white'),
borderRadius: theme('borderRadius.lg'),
padding: theme('spacing.6'),
boxShadow: theme('boxShadow.xl'),
}
})
addUtilities({
'.content-auto': {
contentVisibility: 'auto',
}
})
})
]
}
Q4: 使用tailwind项目体积变化?
Facebook 经过重构后 CSS 体积已经从 413Kb 减至 74Kb,因为同样的class还是只生成一份样式,减少了打包后的样式文件大小。此外Tailwind 提供了 purge 的选项,以此确定项目使用到了哪些类名,最后在 NODE_ENV 为 production 的情况下,构建生成的样式表只会留下用到的样式。
由于HTML因为类名过多会造成体积增大,但也不必担心,由于 class 高度相似,gzip 也将会得到一个很大的压缩比例(gzip 的核心是 Deflate,而它使用了 LZ77 算法与 Huffman 编码来压缩文件,重复度越高的文件可压缩的空间就越大。)。
安装
1、npm install -D tailwindcss postcss autoprefixer
2、npx tailwindcss init -p 创建 两个文件
postcss.config.js
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
}
3、创建 tailwind.css 文件 并在 main.js 中导入
tailwind.css
@tailwind base;
@tailwind components;
@tailwind utilities;
注:防止冲突,需在 Element Plus 组件之前导入 tailwind.css
4、vite.config.js 新增配置
vite.config.js
import tailwindcss from 'tailwindcss';
import autoprefixer from 'autoprefixer';
export default defineConfig({
css: {
postcss: {
plugins: [tailwindcss, autoprefixer]
}
}
})
思考
每个 class 中要声明多个 css class 的写法,会让人感觉代码丑陋,尤其是对于刚接触 Tailwind 的用户。然而,一段时间过后(对我而言大概1周时间),对于绝大部分开发者来说,都是可以习惯这种写法的。
个人认为tailwind css,其价值在于「可定制化的css」和「缩短从想写一个样式到写好这个样式的中间所省去的步骤」,并且做到团队的统一,能够提升开发效率。