eslint-plugin-classname-arg-last:让外部 className 永远拥有最高优先级

img_2.webp在 React 组件封装中,有一个非常常见的模式:

cn("base-style", props.className)

className 是组件的对外扩展口。

它来自外部,通常代表使用者的“最终意图”。

因此在大多数设计约定中:

外部传入的 className 应该拥有最高优先级。

而在 Tailwind CSS 或 twMerge 的场景下,参数越靠后,覆盖优先级越高

这就意味着:

cn("text-red-500", "text-blue-500") 
// 最终是 text-blue-500

所以正确的组件写法应该是:

cn("internal-style", props.className)

而不是:

cn(props.className, "internal-style")

否则组件内部样式可能反而把外部样式覆盖掉。


为什么需要 ESLint 规则?

在团队协作中,这种顺序很容易被忽略:

cn(className, "base")
twMerge(className, "rounded-md")

代码不会报错,但语义已经错了。

当组件越来越多时,这种不一致会带来:

  • 外部样式无法正确覆盖内部样式

  • 调试样式时出现“为什么改不生效”的问题

  • Code Review 反复纠正参数顺序

为了解决这个问题,我写了一个 ESLint 插件:

eslint-plugin-classname-arg-last


它做的事情非常简单

它会检查:

  • cn(...)

  • twMerge(...)

如果发现 className 不在最后一个参数位置,就报错。

错误示例:

cn("base", className, "active")
twMerge(className, "base")

正确示例:

cn("base", "active", className)
twMerge("base", className)

其他函数不会被检查。


安装

npm install eslint-plugin-classname-arg-last --save-dev

配置

ESLint v7 / v8

{
  "plugins": ["classname-arg-last"],
  "rules": {
    "classname-arg-last/classname-arg-last": "error"
  }
}

ESLint v9 Flat Config

import classnameArgLast from "eslint-plugin-classname-arg-last";

export default [
  {
    plugins: {
      "classname-arg-last": classnameArgLast
    },
    rules: {
      "classname-arg-last/classname-arg-last": "error"
    }
  }
];

这条规则的真正价值

这不是一个“代码风格”规则。

它本质上是在强制一个组件设计原则:

组件内部提供默认样式

外部 className 负责覆盖与扩展

外部优先级永远最高

当这种约定被 lint 固化之后,你不再需要在 code review 里反复强调 “className 放最后”。

组件的扩展性也会更加稳定。

如果你在做组件库,或者项目里大量使用 cn / twMerge,这条规则值得加进团队规范。