前段时间重构博客后台的时候,我把整个页面的样式全切成了 Tailwind CSS。刚开始那叫一个爽,写个按钮都不用想类名,直接px-4 py-2 rounded往上堆。可等页面越来越多,问题就来了:同一个 tab 导航,有人写flex items-center justify-between px-4 py-2,有人写px-4 py-2 flex items-center justify-between,还有人把 hover 状态放在中间,搞得代码 Review 的时候大部分时间都在争论“顺序到底该按什么规范”。直到我找到ponytail这个插件,它本质上就是一个针对 Tailwind CSS 的类名整理工具,能把散落在 class 里的 utility class 按固定规则排序、分组、去重,像把散乱的头发扎成一个马尾辫一样规整。这篇文章我就把从安装、配置到日常使用的完整经验写出来,适合正在被 Tailwind 类名顺序折磨的前端开发者参考。
1. 为什么需要 ponytail:类名失控的痛点与设计思路
1.1 类名一多就失控的真实场景
我们先还原一个我每天都在犯错的场景。一个卡片组件大概长这样:
<div class="bg-white hover:bg-gray-50 shadow rounded-lg p-6 mt-4 flex flex-col md:flex-row items-center justify-between max-w-2xl mx-auto">你能一眼看出这个容器的大致结构吗?很难。bg-white和hover:bg-gray-50属于背景色,应该放一起;shadow和rounded-lg是盒模型外观;p-6 mt-4是间距;flex flex-col md:flex-row是布局;items-center justify-between是弹性布局的对齐方式;max-w-2xl mx-auto是尺寸和水平居中。如果每次都靠人工理清这十几个类名的优先级,迟早会出错,而且在新同事加入项目后,类名排序的“隐性规则”根本传递不下去。
这个问题不只是美观问题。类名顺序不一致会导致 Git diff 变得极其难看:你只是改了一个颜色,结果整行 class 属性都被调整了位置,Code Review 的人必须逐个字符去对比才能发现真正的变更。时间长了,大家对代码整洁度就会失去信心。
1.2 人工排序 vs 插件强制规范
有人可能会说:“我们约定一个顺序不就行了?”问题是约定是软性的,只要有人不遵守,就会慢慢变成“新的约定”。在我过往的项目里,Try to enforce 类名顺序的方式无非几种:代码 Review 时口头建议、用 ESLint 规则提醒、或者干脆使用 Prettier 插件。但这些方案要么依赖个人自觉,要么只能提示不能自动修复,要么对动态拼接的类名判断力很弱。
ponytail 的思路则是完全不同的:它在构建流程里介入,把class属性当作一个整体,解析出内部的 Tailwind utility class,然后根据一套可配置的优先组顺序重新排列。这不只是代码风格层面的“偏好”,而是把“类名顺序”变成一种可以被机器执行、自动修复的工程标准。一旦接入,你不需要再去背“先写布局再写间距还是先写间距再写布局”,因为每次保存代码时,插件已经帮你把顺序固定了。
1.3 ponytail 的核心设计思路
ponytail 这个名字本身就有点意思,它是在“Tailwind”的 “tail” 上做文章:给你项目里的“尾巴”扎个马尾。它的工作流程可以拆成三步:
- 扫描:读取项目里所有模板文件(HTML、Vue、JSX、TSX、astro 等),通过正则和 AST 解析找出所有
class或className属性; - 分类:将每个 class 值按 Tailwind 官方类别拆分,比如
flex属于 display,px-4属于 padding,md:flex-row属于 responsive,hover:bg-gray-50属于 pseudo-class; - 重排:按预设的组顺序输出新的 class 值,同时把重复项合并,最后写回原文件。
这种设计最大的好处是规则透明。你不需要让插件理解每个类的具体样式含义,它只需要知道类名属于哪一组、组的优先级是多少,就能在毫秒级完成排序。因为 Tailwind 的类名命名高度规范,所以基于前缀和模式来分类是可靠的,不需要跑一次浏览器来获取实际计算样式,这也让 ponytail 的运行速度非常快。
2. 安装与环境准备工作
2.1 环境要求:Node 版本与 Tailwind 依赖
在动手之前,你最好先确认自己的项目环境。ponytail 是一个基于 Node.js 和 PostCSS 生态开发的插件,所以它需要:
- Node.js 16 及以上版本(如果你用的还是 Node 14,建议先升级,否则某些新语法会直接报错);
- 项目中已经安装并配置了 Tailwind CSS(正式版 v3 或 v4,v4 的使用方式稍微不同,但这个插件兼容得很好);
- 期望被整理的文件类型需要能被 PostCSS 的解析器识别。纯 HTML 文件其实不经过 PostCSS,所以 ponytail 在实际使用时会有两种模式:一种是作为 PostCSS 插件直接接在 Tailwind 的构建链里,另一种是作为独立的 CLI 工具扫描模板文件。安装时可以一起装上。
如果你是在 Vite + React 项目里使用,那么postcss.config.js和tailwind.config.js应该已经存在了;如果是 Next.js 项目,也同样支持。老项目用 webpack 的也不太担心,只要是 PostCSS 链上的都能接入。
2.2 用 npm 安装 ponytail 及配套包
我的建议是先把 ponytail 作为开发依赖安装,同时安装它需要的插件依赖。打开项目根目录,执行:
npm install -D ponytail postcss postcss-cli如果你打算用命令行模式快速整理整个项目,那就需要postcss-cli。装完以后,你可以顺手检查一下package.json,确认 devDependencies 里出现了这三个包。如果你用的是 pnpm 或 yarn,对应命令换成pnpm add -D或yarn add -D即可。
这里有一个细节:如果你在项目里已经单独配置了 Tailwind 的 PostCSS 插件,那么 ponytail 建议放在 Tailwind 插件的后面。因为 ponytail 处理的是已经生成的类名,而不是生成类名本身,它需要在 Tailwind 处理完@tailwind utilities之后再对模板文件做文本层面的整理,顺序反了可能会扫描到的是压缩混淆后的代码。
2.3 验证安装是否成功
装完先别急着配置,我们可以先跑一个小命令验证插件是否正常识别。在项目根目录创建一个临时测试文件test.html,里面随便写一段样式:
<div class="p-4 mt-2 flex shadow-lg rounded bg-white hover:bg-gray-100"></div>然后在终端执行:
npx ponytail test.html如果安装成功,你会发现test.html里的 class 顺序变成了一个相对统一的结构,比如:
<div class="flex rounded bg-white p-4 mt-2 shadow-lg hover:bg-gray-100"></div>不同版本的默认分组顺序可能略有差异,但关键点是文件被修改了,而且没有破坏类名本身。如果这一步就报错,先检查 Node 版本和 PostCSS 版本,最常见的坑是 Node 版本太低导致 AST 解析失败。
3. 配置文件与自定义规则
3.1 在 postcss.config.js 里接入 ponytail
大部分 Tailwind 项目都会有一个postcss.config.js,我们就在这里接入 ponytail。我的配置长这样:
module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, ponytail: { // 扫描范围 include: ['src/**/*.{html,jsx,tsx,vue,astro}'], // 忽略目录 exclude: ['dist', 'node_modules', 'coverage'], // 类名分组顺序可定制 groups: { layout: ['flex', 'grid', 'block', 'inline-block', 'table', 'hidden'], position: ['relative', 'absolute', 'fixed', 'sticky', 'inset', 'top', 'right', 'bottom', 'left', 'z'], display: ['p', 'm', 'space', 'gap', 'w', 'h', 'max-w', 'min-w', 'max-h', 'min-h'], // 注意:这里只是示意,实际默认规则会更细 }, }, }, };其实如果你不做任何自定义,ponytail 也会自带一套非常合理的默认分组规则。官方默认的顺序是按照“布局 → 盒模型 → 间距 → 尺寸 → 背景 → 边框 → 文本 → 交互状态 → 其他”来排列,基本上符合我们从外到内的阅读习惯。我强烈建议你先用默认规则跑一段时间,体验一下,再根据团队习惯微调。
3.2 自定义分组顺序与优先级
为什么分组顺序那么重要?因为 Tailwind 的类名是有层级关系的,比如flex决定了子元素的排列方式,如果被排在p-4后面,视觉上并不会变错,但人在读代码时就要在多行之间来回跳。我们团队后来约定的核心原则是:先结构,后外观,再状态。
具体来说:
- 第一优先级:布局类(display、flex、grid、position);
- 第二优先级:盒子模型(width、height、padding、margin、border);
- 第三优先级:视觉类(background、color、shadow、rounded、font);
- 第四优先级:交互与状态类(hover、focus、active、disabled、dark)。
这个顺序的好处是符合 CSS 的书写直觉:先想好这个元素是什么布局,再想它多大、间距多少,最后才关心它长什么样、鼠标移上去有什么变化。如果你的团队已经习惯另一种顺序,比如“颜色放前面方便做主题切换”,那也可以直接修改配置里的 group 顺序。
ponytail 的配置基于数组,数组项的先后顺序就是最终输出的顺序。你还可以细分前缀,比如把flex和grid都归入 layout 组,但让flex排在grid前面。组内部还有二级排序,默认是字母序,但可以通过sort: 'alpha' | 'custom'调整。
3.3 忽略文件夹和单行禁用
在大型项目里,并不是所有文件都希望被 ponytail 处理。比如你的dist目录、node_modules肯定要忽略,否则构建产物和依赖包会被改得面目全非。配置里用 exclude 数组就行:
ponytail: { exclude: ['dist/**', '.next/**', 'out/**'], }另外还有一种场景:某些第三方组件的模板里用了很诡异的类名拼接,比如class={condition ? 'p-4' : 'p-2'} ${extraClass},这种动态拼接的类名有时候会被插件误伤。为了避免这种情况,你可以在那一行加一个注释标记,让 ponytail 跳过。具体语法通常是<!-- ponytail:off -->,如果是在 JSX 里,则用// ponytail:off。不同版本语法略有不同,我建议查看一下你安装版本的 README。实测下来,单行禁用非常有必要,因为你不可能为了满足插件去改第三方库作者的代码习惯。
4. 实操:在项目里把 ponytail 跑起来
4.1 一键整理整个项目的类名
配置好之后,最直接的使用方式就是通过命令行对整个项目跑一次。我会在package.json里加一个 script:
{ "scripts": { "pony:format": "ponytail \"src/**/*.{html,jsx,tsx,vue,astro}\" --write" } }然后执行:
npm run pony:format首次运行会有一种“看着它扫描几百个文件,逐个重写 class 顺序”的快感。我的建议是第一次先在分支上跑,然后立刻打开 Git diff 检查。你可能会发现两个问题:一是有些文件里的类名顺序和你平时习惯不一样,二是某些类名因为插件识别问题被合并或拆分。这些都是正常现象,你可以根据自己的需要修改 include 或 exclude。
我习惯把这条命令加入lint流程里,比如在 CI 上跑ponytail --check模式,它只会检查顺序不对的文件,并输出文件名,不会直接修改文件。这样能阻止那些没有格式化过的代码合并进主分支。
4.2 接入编辑器保存时自动整理
命令行适合批量处理,但日常开发最舒服的还是在保存文件时自动触发。ponytail 官方提供了 VS Code 扩展,安装后会在语言服务器里绑定保存事件。你只需要在设置里开启:
{ "ponytail.enableOnSave": true, "ponytail.include": ["src/**/*.{html,jsx,tsx,vue,astro}"] }保存的时候,扩展会自动执行和命令行一样的格式化,但只处理当前文件。这个体验和 Prettier 的 format on save 很像,不会有明显的卡顿感。
如果你不用 VS Code,而是用 Neovim 或 JetBrains 系 IDE,也可以在保存时挂钩子调用 CLI 命令,只是稍微麻烦一点。我目前是 VS Code 加终端的组合,保存后偶尔会看一眼改动,基本没有再手动调整过类名顺序。
4.3 与 Tailwind 的 class 自动补全插件配合
很多前端工程师都装了 Tailwind CSS IntelliSense 扩展来提示类名,这和你用 ponytail 并不冲突。IntelliSense 负责输入时的补全和排序建议,ponytail 负责保存时的强制整理,配合起来非常顺手。不过有个小坑:IntelliSense 默认会按照自己的算法给类名排序,而你不小心按了 Tab 接受它的顺序,保存后 ponytail 又会把你改成另一套顺序。对于这种情况,我建议直接在设置里把 IntelliSense 的排序规则也改成和 ponytail 的 group 一致,或者干脆把它的tailwindCSS.experimental.classRegex关闭,只保留补全功能,把排序交给 ponytail。
4.4 用 pony lint 检查类名顺序
如果你想在代码审查前就发现问题,ponytail 还提供了轻量级的 lint 命令。用法很简单:
npx ponytail lint "src/**/*.tsx" --quiet它只会列出包含乱序类名的文件和具体行号,不会修改文件。这在 CI 里特别有用,可以配合--max-warnings 0强制不合格代码不能通过构建。我们团队在接入 ponytail 之后的第三周,就把lint命令加到了 CI 里,从那以后类名顺序就再也没有被当做过 Review 话题。
5. 常见问题与排查技巧
5.1 插件没生效,文件没有被修改
这是接入时遇到最多的问题。如果你执行ponytail之后文件没有任何变化,先按这几个顺序排查:
- 检查
include配置是否覆盖了目标文件路径,注意 glob 通配符的写法,src/**/*.tsx不会匹配src/index.tsx之外的嵌套路径,建议用src/**/*.{tsx,jsx,vue,html}; - 确认是否被
exclude排除了,比如你习惯把组件放在src/components下,但 exclude 里写了src/components的话就会跳过; - 看看文件里是不是用了
class:这种 Vue 语法,或者 CSS module 的style={styles.foo},这类属性默认不处理,需要在配置里加attributes: ['class', 'className']扩展范围。
还有一个容易忽略的问题:如果你的项目是通过 CDN 引入 Tailwind 而不是用 PostCSS 构建的,ponytail 依然可以作为独立 CLI 工具运行。但如果你的 Tailwind 版本太老(比如 v1),类名的前缀规则差异很大,插件解析时可能识别不全,导致结果不可预期,建议先升级 Tailwind 到 v3/v4。
5.2 动态拼接的类名被意外拆分
这类问题多出现在 React 中,比如:
<div className={`p-4 ${condition ? 'bg-red-500' : 'bg-blue-500'} flex`}>ponytail 默认会尝试把模板字符串里的内容也当作 class 列表处理,但它毕竟不是 JS 解释器,无法知道这个变量的最终值。结果就是它可能把静态部分p-4和flex排列好,但保留了变量部分的位置,最后顺序变得不协调。
我的解决办法是把动态条件部分单独抽到一个变量里,或者用 ponytail 的单行禁用注释。不过在大多数情况下,你写的动态拼接类名都只是少数几个状态类,完全可以用clsx或tailwind-merge来管理,让 ponytail 只处理静态字符串,这样冲突更少。
5.3 与 Prettier 冲突怎么办
Prettier 的printWidth如果比较小,会在 class 属性很长时把属性折成多行。ponytail 默认是保留原有换行的,但如果在排序后仍然超过 Prettier 的行宽限制,两者会产生“打架”的现象:Prettier 换行,ponytail 排序,下次保存又换行,来回改。
我的建议是在项目里约定先 p 后 p,具体顺序可以这样:先用 Prettier 格式化 JS/JSX 结构,再让 ponytail 做类名排序,最后再跑一次 Prettier 确保换行位置正确。我一般会把这两个步骤串在一个 Hook 里:
pretty-quick --staged && ponytail --write如果 Prettier 和 ponytail 真的因为个别文件发生循环冲突,最省心的方式是在.prettierignore里忽略这些文件,或者给 ponytail 配置singleLine: false,让它保持多行 class 属性的原样,只调整顺序,减少换行冲突的概率。
5.4 类名去重后发现样式异常
ponytail 默认会去掉重复类名,比如class="p-4 p-4"会变成class="p-4",这在大多数情况下是安全的。但有一种情况可能会出问题:如果你用了dark p-4 dark:bg-red-500这种写法,去重后正常是dark p-4 dark:bg-red-500,不会误删。但如果某个类名既有前缀又是另一个类名的前缀,比如bg-red-500和bg-red-500/50,这两个其实是不同类,插件也会区分。
我在一个老项目里遇到过的问题是:有人在 Tailwind 里又启用了自定义插件,生成了形如btn btn-primary这样的语义类名,而 ponytail 默认不会识别这类非 utility class,结果把btn和btn-primary当成普通文本保留在原位置。这不影响样式,但可能会导致排序不彻底。解决方法是在配置里把自定义类名也归入某一个 group,比如:
customGroup: ['btn', 'btn-primary', 'icon-']这样它们就能被统一放到末尾或指定分组了。
5.5 大型项目性能卡顿
如果你的项目文件特别多,ponytail 在保存时做全量扫描会明显卡顿。我遇到过大几千行的.astro文件,保存时几乎卡了三秒。解决办法有两个:
- 在 VS Code 设置里缩小
include范围,比如只处理src/components、src/pages,不过度扫描; - 使用增量缓存模式。当前版本支持在项目根目录生成
.ponytail-cache文件,第二次运行同一目录时只处理有改动的文件。
如果你在 CI 上跑全量 lint,也可以先把缓存文件提交到仓库,避免每次构建都全量跑一遍。
6. 使用心得与后续扩展
说实话,我一开始只把 ponytail 当个玩具,觉得类名排序不至于需要专门一个插件。但真正接入后,最明显的变化不是代码变好看了,而是团队里的“隐性知识”变少了。以前新人会问:这个按钮的类名为什么这样排?现在没人问了,因为所有人保存代码后出来的顺序都是一样的。
有几个小心得可以分享:
- 第一个建议是,引入 ponytail 一定要和 prettier、husky 一起做。只靠编辑器保存还不够,总有人喜欢直接命令行提交不带格式化。我们后来在
lint-staged里加了ponytail --write,现在所有 commit 之前都会自动格式化,彻底杜绝了不合规内容进入仓库。 - 第二个建议是,把一个规范的可配置
ponytail.config.js放进团队模板里。我自己维护了一个基础配置,里面固定了 layout、spacing、visual、state 的顺序,方便新项目直接复用。如果你用的 UI 组件库有自己的设计系统,比如某些组件要求relative必须跟在overflow-hidden后面,可以在每行注释里写清楚这些例外,效果比口头通知靠谱得多。 - 第三个建议是,先拿一个低频改动的目录跑通流程,再逐步扩大到全项目。我第一次直接对整个 monorepo 跑
ponytail --write,结果改了三千多个文件,导致一个几十人的团队短时间内出现了大量合并冲突。后来改成“按模块分批接入”,就平滑多了。
如果你项目中使用了多个 UI 组件库,或者团队正在从纯 CSS 过渡到 Tailwind,ponytail 也可以作为统一类名书写规范的底子。后续还可以考虑把它集成到一个全局的代码生成器里,比如根据组件属性拼装好的 class 字符串,在生成阶段直接就调用 ponytail,连保存格式化都省了。
至少在我目前的项目里,ponytail 已经成了不可或缺的一部分。我不用再为 class 的先后顺序和同事讨论半天,也不用担心重构时某个类名被重复写了两遍。这样的小工具看起来不起眼,但长期省下来的时间,比我写这篇文章投入的精力多得多。