解决Tailwind任意值滥用:规范化CSS开发体验

背景 eslint-plugin-tailwindcss插件的no-unnecessary-arbitrary-value无法对所有的任意值进行校验,比如h-[48px]text-[#f5f5f5]无法校验出来。但tailwindcss的预设值太多了,一个不小心可能就又写了一个没有必要的任意值。为了避免这种情况,我们需要自己实现一个检测任意值的eslint插件。

插件地址:eslint-plugin-tailwind-no-preset-class

首先来看下效果

no-unnecessary-arbitrary-value 无法检测的情况

使用自定义的:eslint-plugin-tailwind-no-preset-class插件,完美完成了校验

创建eslint插件标准目录结构

  • 安装Yeoman
sh 复制代码
npm install -g yo
  • 安装Yeoman generator-eslint
sh 复制代码
npm install -g generator-eslint
  • 创建项目
sh 复制代码
mkdir eslint-plugin-my-plugin
yo eslint:plugin

生成目录结构如下:

perl 复制代码
eslint-plugin-my-plugin/
├── lib/                    # 核心源代码目录
│   ├── index.js           # 插件的入口文件,在这里导出所有规则
│   └── rules/             # 存放所有自定义规则的目录
│       └── my-rule.js     # 生成器为你创建的一条示例规则文件
├── tests/                 # 测试文件目录
│   └── lib/
│       └── rules/
│           └── my-rule.js # 示例规则对应的测试文件
├── package.json           # 项目的 npm 配置文件,依赖和元信息都在这里
└── README.md              # 项目说明文档

根据实际项目的tailwindcss配置文件和tailwindcss默认配置生成全量定制化配置,用于后续eslint插件的校验依据

实现配置文件生成并加载方法:

js 复制代码
// lib/tailwind-config-loader.js
// 配置文件生成
...
...
// 动态加载 Tailwind 预设配置
let tailwindPresetConfig = null;
...
async function generateTailwindConfig(projectRootPath) {
  try {
    // 动态导入tailwindcss
    const resolveConfigModule = await import('tailwindcss/lib/public/resolve-config.js');
    const resolveConfig = resolveConfigModule.default.default
    // 尝试加载项目配置
    let projectConfig = {};
    try {
      const projectConfigPath = join(projectRootPath||process.cwd(), 'tailwind.config.js');
      const projectConfigModule = await import(projectConfigPath);
      projectConfig = projectConfigModule.default || projectConfigModule;
    } catch (error) {
      console.log('⚠️ 未找到项目 tailwind.config.js,使用默认配置');
      throw error;
    }

    // 使用tailwindcss的resolveConfig函数
    const finalConfig = resolveConfig(projectConfig);

    console.log('✅ Tailwind preset config generated successfully!');
    
    return finalConfig;
  } catch (error) {
    console.error('❌ 生成Tailwind配置失败:', error.message);
    throw error;
  }
}


// 加载配置到内存中
async function loadTailwindPresetConfig(projectRootPath) {
  if (configLoading) {
    console.log('⏳ 配置正在加载中,跳过重复请求');
    return;
  }

  configLoading = true;

  try {
    // 直接动态生成配置
    tailwindPresetConfig = await generateTailwindConfig(projectRootPath);
    console.log('✅ Tailwind 预设配置已动态生成并加载');
    onConfigLoaded();
  } catch (error) {
    console.error('❌ 动态生成 Tailwind 预设配置失败:', error.message);
    onConfigLoadFailed(error);
    throw error;
  }
}


...
// 导出配置
export const TailwindConfigLoader = {
  getConfig: () => tailwindPresetConfig,
  isLoaded: () => configLoaded,
  ensureLoaded: ensureConfigLoaded,
  reload: loadTailwindPresetConfig,
  generateConfig: generateTailwindConfig
};
...
...

创建校验规则函数

  • 实现校验规则函数checkAndReport
js 复制代码
...
// 使用 WeakMap 来跟踪每个文件的已报告类名,避免重复报告
const reportedClassesMap = new WeakMap();
...
// 检查并报告
async function checkAndReport(context, node, className) {
  // 如果配置尚未加载,尝试等待加载
  if (!TailwindConfigLoader.isLoaded()) {
    try {
        const projectRootPath = context.getCwd();
        console.log(`正在等待加载配置文件 ${projectRootPath}...`);
      const loaded = await TailwindConfigLoader.ensureLoaded(projectRootPath);
      if (!loaded) {
        console.warn('⚠️ Tailwind 预设配置尚未加载,跳过检查');
        return;
      }
    } catch (error) {
      console.warn('⚠️ 配置加载失败,跳过检查');
      return;
    }
  }

  const filePath = context.getFilename();
  const filePathWrapper = new FilePathWrapper(filePath);

  if (!reportedClassesMap.has(filePathWrapper)) {
    reportedClassesMap.set(filePathWrapper, new Set());
  }
  const reportedClasses = reportedClassesMap.get(filePathWrapper);

  if (reportedClasses.has(className)) {
    return;
  }

  const propertyInfo = extractProperty(className);
  if (!propertyInfo) {
    return;
  }

  const { property, value, originalPrefix } = propertyInfo;

  // 只检查任意值
  if (isArbitraryValue(value)) {
    const arbitraryValue = value.slice(1, -1);
    const presetClass = findPresetClass(property, arbitraryValue);

    if (presetClass) {
      reportedClasses.add(className);
      // 使用原始前缀显示正确的类名格式(如 h-14 而不是 height-14)
      const suggestedClass = `${originalPrefix}${presetClass}`;
      context.report({
        node,
        message: `类名 "${className}" 使用了任意值,但存在对应的预设类名 "${suggestedClass}"。请使用预设类名替代。`,
      });
    }
  }
}
  • 实现属性提取,将classname解析为tailwindcss的property和value
js 复制代码
// 提取属性值
function extractProperty(className) {
  // 处理响应式前缀(如 max-md:, md:, lg: 等)
  const responsivePrefixes = [
    'max-sm:',
    'max-md:',
    'max-lg:',
    'max-xl:',
    'max-2xl:',
    'max-',
    'min-',
    'sm:',
    'md:',
    'lg:',
    'xl:',
    '2xl:',
  ];

  // 移除响应式前缀,保留核心类名
  let coreClassName = className;
  let responsivePrefix = '';

  for (const prefix of responsivePrefixes) {
    if (className.startsWith(prefix)) {
      responsivePrefix = prefix;
      coreClassName = className.slice(prefix.length);
      break;
    }
  }

  // 按前缀长度降序排序,优先匹配更长的前缀
  const sortedPrefixes = Object.keys(prefixToProperty).sort(
    (a, b) => b.length - a.length
  );

  for (const prefix of sortedPrefixes) {
    if (coreClassName.startsWith(prefix)) {
      return {
        property: prefixToProperty[prefix],
        value: coreClassName.slice(prefix.length),
        originalPrefix: responsivePrefix + prefix, // 包含响应式前缀
      };
    }
  }

  return null;
}
  • 将提取的property和前面生成的全量的tailwindcss进行映射
js 复制代码
// 简化属性映射,只保留常用的属性
const prefixToProperty = {
  // 尺寸相关
  "w-": "width",
  "h-": "height",
  "min-w-": "minWidth",
  "min-h-": "minHeight",
  "max-w-": "maxWidth",
  "max-h-": "maxHeight",

  // 间距相关
  "m-": "margin",
  "mt-": "marginTop",
  "mr-": "marginRight",
  "mb-": "marginBottom",
  "ml-": "marginLeft",
  "mx-": "margin",
  "my-": "margin",
  "p-": "padding",
  "pt-": "paddingTop",
  "pr-": "paddingRight",
  "pb-": "paddingBottom",
  "pl-": "paddingLeft",
  "px-": "padding",
  "py-": "padding",

  // 边框相关(新增)
  "border-": "borderWidth;borderColor",
  "border-t-": "borderWidth;borderColor",
  "border-r-": "borderWidth;borderColor",
  "border-b-": "borderWidth;borderColor",
  "border-l-": "borderWidth;borderColor",
  "border-x-": "borderWidth;borderColor",
  "border-y-": "borderWidth;borderColor",

  // 圆角相关(新增)
  "rounded-": "borderRadius",
  "rounded-t-": "borderRadius",
  "rounded-r-": "borderRadius",
  "rounded-b-": "borderRadius",
  "rounded-l-": "borderRadius",
  "rounded-tl-": "borderRadius",
  "rounded-tr-": "borderRadius",
  "rounded-br-": "borderRadius",
  "rounded-bl-": "borderRadius",

  // 文字相关
  "text-": "fontSize;color",
  "leading-": "lineHeight",
  "tracking-": "letterSpacing",
  "font-": "fontWeight",

  // 背景相关
  "bg-": "backgroundColor",

  // SVG相关
  "fill-": "fill",
  "stroke-": "stroke",
  "stroke-w-": "strokeWidth",

  // 定位相关
  "z-": "zIndex",
  "inset-": "inset",
  "top-": "top",
  "right-": "right",
  "bottom-": "bottom",
  "left-": "left",

  // 布局相关(新增)
  "gap-": "gap",
  "gap-x-": "gap",
  "gap-y-": "gap",
  "space-x-": "gap",
  "space-y-": "gap",

  // 透明度
  "opacity-": "opacity",

  // 变换相关(新增)
  "scale-": "scale",
  "scale-x-": "scale",
  "scale-y-": "scale",
  "rotate-": "rotate",
  "translate-x-": "translate",
  "translate-y-": "translate",
  "skew-x-": "skew",
  "skew-y-": "skew",

  // 阴影相关(新增)
  "shadow-": "boxShadow",

  // 网格相关(新增)
  "grid-cols-": "gridTemplateColumns",
  "grid-rows-": "gridTemplateRows",
  "col-": "gridColumn",
  "row-": "gridRow",
  "col-start-": "gridColumnStart",
  "col-end-": "gridColumnEnd",
  "row-start-": "gridRowStart",
  "row-end-": "gridRowEnd",

  // Flexbox相关(新增)
  "flex-": "flex",
  "basis-": "flexBasis",
  "grow-": "flexGrow",
  "shrink-": "flexShrink",
  "order-": "order",

  // 动画相关(新增)
  "duration-": "transitionDuration",
  "delay-": "transitionDelay",
  "ease-": "transitionTimingFunction",

  // 其他(新增)
  "aspect-": "aspectRatio",
  "cursor-": "cursor",
};

// 动态构建支持的 Tailwind 属性映射
function getSupportedProperties() {
  const config = TailwindConfigLoader.getConfig();
  if (!config) {
    return {};
  }

  return {
    width: config.theme.width,
    height: config.theme.height,
    minWidth: config.theme.minWidth,
    minHeight: config.theme.minHeight,
    maxWidth: config.theme.maxWidth,
    maxHeight: config.theme.maxHeight,
    margin: config.theme.margin,
    marginTop: config.theme.margin,
    marginRight: config.theme.margin,
    marginBottom: config.theme.margin,
    marginLeft: config.theme.margin,
    padding: config.theme.padding,
    paddingTop: config.theme.padding,
    paddingRight: config.theme.padding,
    paddingBottom: config.theme.padding,
    paddingLeft: config.theme.padding,
    fontSize: config.theme.fontSize,
    lineHeight: config.theme.lineHeight,
    borderRadius: config.theme.borderRadius,
    color: config.theme.colors,
    backgroundColor: config.theme.backgroundColor,
    borderColor: config.theme.borderColor,
    fill: config.theme.fill,
    stroke: config.theme.stroke,
    borderWidth: config.theme.borderWidth,
    zIndex: config.theme.zIndex,
    gap: config.theme.gap,
    inset: config.theme.inset,
    top: config.theme.spacing,
    right: config.theme.spacing,
    bottom: config.theme.spacing,
    left: config.theme.spacing,
    opacity: config.theme.opacity,
  };
}

整体实现流程

graph TD A[ESLint 执行插件] --> B[遍历代码中的类名] B --> C{是否为 Tailwind 类名?} C -->|否| D[跳过检查] C -->|是| E{是否包含任意值?} E -->|否| F[使用预设值 通过检查] E -->|是| G[提取类名前缀和任意值] G --> H[通过 prefixToProperty 映射到CSS属性] H --> I[检查Tailwind配置是否已加载] I -->|已加载| J[获取支持的属性预设值] I -->|未加载| K[加载项目Tailwind配置] K --> L[读取项目tailwind.config.js] L --> M{配置是否存在?} M -->|不存在| N[使用Tailwind默认配置] M -->|存在| O[解析项目配置] O --> P[合并默认配置和项目配置] N --> P P --> Q[生成全量Tailwind配置] Q --> R[缓存配置到内存] R --> J J --> S{判断属性类型} S -->|颜色相关| T[调用 findColorPreset] S -->|数值相关| U[调用 findNumericPreset] T --> V{是否匹配预设?} U --> V V -->|是| W[找到对应预设类名] V -->|否| X[未找到预设类名] W --> Y[生成建议消息] X --> Z[通过检查 无匹配预设] Y --> AA[报告建议] Z --> BB[检查完成] AA --> BB
相关推荐
用户54277848515402 小时前
Vue 3 中开发高阶组件(HOC)与 Renderless 组件
前端
HIT_Weston2 小时前
67、【Ubuntu】【Hugo】搭建私人博客(一)
前端·ubuntu·hugo
阿里巴啦2 小时前
用React+Three.js 做 3D Web版搭建三维交互场景:模型的可视化摆放与轻量交互
前端·react·three.js·模型可视化·web三维·web三维交互场景
Liu.7742 小时前
vue3组件之间传输数据
前端·javascript·vue.js
|晴 天|2 小时前
前端闭包:从概念到实战,解锁JavaScript高级技能
开发语言·前端·javascript
开发者小天2 小时前
react的拖拽组件库dnd-kit
前端·react.js·前端框架
用户4445543654262 小时前
在Android开发中阅读源码的指导思路
前端
用户54277848515402 小时前
ESM 模块(ECMAScript Module)详解
前端
全栈前端老曹3 小时前
【ReactNative】核心组件与 JSX 语法
前端·javascript·react native·react.js·跨平台·jsx·移动端开发