ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

用 ponytail 自动整理 Tailwind CSS 类名顺序的实践指南

用 ponytail 自动整理 Tailwind CSS 类名顺序的实践指南

前段时间重构博客后台的时候,我把整个页面的样式全切成了 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” 上做文章:给你项目里的“尾巴”扎个马尾。它的工作流程可以拆成三步:

  1. 扫描:读取项目里所有模板文件(HTML、Vue、JSX、TSX、astro 等),通过正则和 AST 解析找出所有class或className属性;
  2. 分类:将每个 class 值按 Tailwind 官方类别拆分,比如flex属于 display,px-4属于 padding,md:flex-row属于 responsive,hover:bg-gray-50属于 pseudo-class;
  3. 重排:按预设的组顺序输出新的 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 的先后顺序和同事讨论半天,也不用担心重构时某个类名被重复写了两遍。这样的小工具看起来不起眼,但长期省下来的时间,比我写这篇文章投入的精力多得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进