用 TailwindCSS 重构待办事项项目:从手写 CSS 到原子类

1793182377141

一篇写给自己的学习笔记:把课程里的待办事项项目从头到尾用 Tailwind 重写了一遍,记下改了什么、踩了什么坑、以及我为什么最后觉得它值得。

一、起因

待办事项项目功能很简单:输入、添加、勾选、删除、过滤、统计。但停下来看 style.css 的时候,我发现一件很尴尬的事——功能越加越多,CSS 越来越不敢动。

大致是这么几个症状:

  • 类名越起越敷衍。.item、.item2、.item-active、.item-active-new……同一个列表项,四个类名,谁在用什么全靠记忆。
  • 想改一个"删除按钮只在 hover 时出现"的效果,得在三处加样式:.item:hover .del、.del、.del:hover,还要考虑触屏设备没有 hover。
  • 响应式是一堆重复。同一个卡片,桌面一套、手机一套,改个圆角要改两个地方。
  • 最要命的是:我打开 style.css 找样式的时间,比写业务逻辑还长。

于是我决定用 Tailwind 重写一遍。不是为了追新,是想验证一个说法:样式应该待在它该待的地方,也就是模板里。

二、重构前长什么样

旧版本的结构大概是这样的:

点击查看代码
<div class="todo-box">
  <div class="todo-input-wrap">
    <input class="todo-input" />
    <button class="todo-btn">添加</button>
  </div>
  <ul class="todo-list">
    <li class="todo-item">
      <input type="checkbox" class="todo-check" />
      <span class="todo-text">买牛奶</span>
      <button class="todo-del">删除</button>
    </li>
  </ul>
</div>

配套的 CSS 里,.todo-item 有 11 条声明,.todo-del 有 7 条,加上 hover、focus、响应式,一个列表项占了 40 多行。

三、Tailwind 到底解决了什么

用了两周,我把它解决的问题归纳成三句人话:

  1. 不用起名了。 以前最痛苦的是"这个 div 该叫什么"。现在它叫什么不重要,长什么样写在 class 里。
  2. 改了就走,不用全局搜。 改一个按钮的圆角,只改用到它的那一行模板,不会牵连别处。
  3. 删代码是安全的。 删掉一个组件,它的样式跟着消失,不会在 CSS 里留下"不知道还有没有人用"的死规则。

代价也很明显:类名会很长,模板会变"吵"。这一点后面讲我怎么处理的。

四、把环境搭起来(Tailwind v4)

这一步比 v3 时代简单很多,因为 v4 不再需要 tailwind.config.js 和 postcss.config.js。

点击查看代码
npm install tailwindcss @tailwindcss/vite
点击查看代码
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [vue(), tailwindcss()],
})
点击查看代码
/* src/style.css —— 就这一行 */
@import "tailwindcss";

三处改动就能跑。这是我第一次觉得"配置比想象中少"的前端工具。

顺带记一个新东西:v4 的主题变量直接写在 CSS 里,不用开 JS 配置文件。

点击查看代码
@import "tailwindcss";

@theme {
  --color-brand: #4a90e2;
  --radius-card: 12px;
}

之后就能用 bg-brand、rounded-card 这样的类名,跟内置工具类混着写,没有区别。

五、开始重构,一个组件一个组件改

5.1 外壳:从 .todo-box 到一行工具类

点击查看代码
<main class="mx-auto flex min-h-screen w-full max-w-xl flex-col gap-4 bg-slate-50 px-4 py-10">

原来这段对应 6 条 CSS(宽度、最小高度、flex 方向、间距、背景、内边距),现在一行读完就知道它长什么样——这就是原子类最大的好处:代码即效果图。

5.2 输入区

点击查看代码
<form class="flex gap-2" @submit.prevent="add">
  <input
    v-model="text"
    placeholder="今天要做什么?"
    class="flex-1 rounded-xl border border-slate-200 bg-white px-4 py-2.5 text-slate-700
           outline-none transition placeholder:text-slate-400
           focus:border-blue-400 focus:ring-4 focus:ring-blue-100" />
  <button
    class="rounded-xl bg-blue-500 px-4 py-2.5 font-medium text-white
           transition hover:bg-blue-600 active:scale-95">
    添加
  </button>
</form>

focus:ring-4 这种写法替代了我以前手写的 box-shadow: 0 0 0 4px rgba(...),而且焦点态和边框色是同时声明的,不会像以前那样漏掉一个。

5.3 列表项:一个 group 解决 hover 联动

这是重构里我最喜欢的部分。以前"鼠标移到整行,删除按钮才出现",要靠后代选择器:

点击查看代码
.todo-item .todo-del { opacity: 0; transition: opacity .2s; }
.todo-item:hover .todo-del { opacity: 1; }

Tailwind 里给父级加 group,子元素用 group-hover:

点击查看代码
<li class="group flex items-center gap-3 rounded-xl border border-slate-200 bg-white
           px-4 py-3 transition hover:border-slate-300 hover:shadow-sm">
  <input type="checkbox" class="h-4 w-4 accent-blue-500" v-model="todo.done" />

  <span
    class="flex-1 text-slate-700 transition"
    :class="todo.done ? 'line-through text-slate-400' : ''">
    {{ todo.text }}
  </span>

  <button
    class="rounded-lg px-2 py-1 text-sm text-slate-400 opacity-0 transition
           group-hover:opacity-100 hover:bg-red-50 hover:text-red-500">
    删除
  </button>
</li>

group-hover 的语义非常明确:"我所在的组被 hover 时",不用再关心 DOM 层级,也就不会出现"多加一层 div 样式就失效"的问题。

这里还顺手用 accent-blue-500 改了原生复选框的颜色——以前这是要写 appearance: none 再自己画一个的。

5.4 空状态与底部统计

点击查看代码
<!-- 空状态 -->
<div class="flex flex-col items-center gap-2 rounded-xl border border-dashed
            border-slate-200 py-12 text-slate-400">
  <span class="text-3xl">🗒️</span>
  <p class="text-sm">还没有待办,先加一条吧</p>
</div>

<!-- 底部 -->
<footer class="flex items-center justify-between text-sm text-slate-500">
  <span>共 {{ total }} 条,已完成 {{ doneCount }}</span>
  <button class="hover:text-blue-500 hover:underline underline-offset-4">
    清除已完成
  </button>
</footer>

六、响应式:一个前缀 vs 一段媒体查询

旧代码:

点击查看代码
.todo-box { max-width: 640px; padding: 40px 16px; }
@media (max-width: 480px) {
  .todo-box { padding: 24px 12px; }
  .todo-title { font-size: 20px; }
  .todo-btn { padding: 8px 12px; }
}

新代码,直接写在原类名旁边:

点击查看代码
<main class="px-4 py-10 sm:px-6">
  <h1 class="text-xl font-semibold sm:text-2xl">待办事项</h1>
</main>

sm: 是"≥640px 生效",默认样式就是移动端。这个方向我觉得比 max-width 更省脑子——先写手机,再往上加。

七、踩过的坑

这部分是我真正花时间的地方,记下来备查。

7.1 动态拼接的类名不生效

点击查看代码
// ❌ 这样写,样式不会生成
:class="`bg-${color}-500`"

Tailwind 是静态扫描源码文本的,它看到的是 bg-${color}-500 这个字符串,不是 bg-blue-500。所以必须写完整类名:

点击查看代码
// ✅ 用映射表,每个值都是完整类名
const colorMap = {
  blue: 'bg-blue-500',
  red: 'bg-red-500',
  green: 'bg-green-500',
}

7.2 v4 里没有 tailwind.config.js

我照着 v3 的教程找了一晚上配置文件。v4 的做法是 @theme 写变量,@source 指定扫描范围(默认自动扫)。找不到那个文件不是装错了。

7.3 @apply 一用就回到老路

点击查看代码
/* 看着很爽,但用多了等于换了个地方写 CSS */
.todo-btn { @apply rounded-xl bg-blue-500 px-4 py-2.5 text-white; }

我的规则是:只有"确实在三个以上组件里重复出现、且语义固定"的东西才抽,其余都留在模板里。

7.4 类名太长,读不下去

我现在的处理方式:

  • 按 布局 → 间距 → 排版 → 颜色 → 状态 的固定顺序写,扫一眼能定位;
  • 装 prettier-plugin-tailwindcss,保存时自动排序,团队里不会出现两种顺序;
  • 超过三行的类名,说明这个块该拆成组件了。

7.5 浏览器兼容

v4 用了新的 CSS 特性,Chrome 111+ / Safari 16.4+ / Firefox 128+ 才支持。如果项目要兼容老浏览器,老老实实用 v3。

八、结果

对比项 重构前 重构后
style.css 行数
自定义类名数量
响应式写法 3 段 @media 类名前的 sm: 前缀
改一个按钮圆角 全局搜 3 处 改 1 处

主观感受上最大的变化不是"代码变短",而是我敢删了。以前删一个组件要想"CSS 里那堆还有没有别处在用",现在删掉模板,样式就一起没了。

九、什么时候我不推荐 Tailwind

诚实说,它不适合所有场景:

  • 静态页面、一页到底的宣传页:手写 CSS 更直接,没必要引入构建链。
  • 已有成熟组件库的项目:Tailwind 的 preflight 会重置基础样式,和 Element Plus / Ant Design 混用要额外处理冲突。
  • 团队没有统一规范:原子类缺乏约束时,五个人能写出五种风格,反而更难维护。

但对我这个待办事项项目——组件少、样式改得勤、就我一个人写——它非常合适。

十、下一步


参考

posted @ 2026-10-28 18:22  寒夏风雨  阅读(7)  评论(0)    收藏  举报