从零到精通:现代原子化 CSS 工具链完全攻略 | PostCSS × UnoCSS × TailwindCSS 深度实战

40 阅读12分钟

CSS 原子化概念深度解析 {#atomic-css}

在前端开发的演进历程中,CSS 工具链正经历着一场深刻的革命。从传统的手写样式到预处理器,再到如今的原子化 CSS 时代,开发者们不断寻求更高效、更可维护的样式解决方案。

什么是 CSS 原子化?

CSS 原子化(Atomic CSS)是一种 CSS 架构方法,其核心思想是将样式拆分为最小的、单一职责的原子类(Atomic Classes)。每个原子类只负责一个具体的样式属性,如 margin-top: 8px 对应 mt-2color: red 对应 text-red-500

CSS 原子化的核心原则

/* 传统 CSS 写法 */
.button {
  padding: 12px 24px;
  background-color: #3b82f6;
  color: white;
  border-radius: 6px;
  font-weight: 600;
  border: none;
  cursor: pointer;
}

.button:hover {
  background-color: #2563eb;
}

/* 原子化 CSS 写法 */
.px-6 {
  padding-left: 24px;
  padding-right: 24px;
}
.py-3 {
  padding-top: 12px;
  padding-bottom: 12px;
}
.bg-blue-500 {
  background-color: #3b82f6;
}
.text-white {
  color: white;
}
.rounded-md {
  border-radius: 6px;
}
.font-semibold {
  font-weight: 600;
}
.border-none {
  border: none;
}
.cursor-pointer {
  cursor: pointer;
}
.hover\:bg-blue-700:hover {
  background-color: #2563eb;
}

原子化 CSS 的优势

  1. 高度复用性 - 原子类可以在整个项目中复用
  2. 样式一致性 - 强制使用设计系统中的预定义值
  3. 包体积优化 - 避免样式重复,CSS 体积可控
  4. 开发效率 - 快速组合样式,无需命名困扰
  5. 维护性强 - 样式变更影响范围可预测

原子化 CSS 的挑战

  1. 学习成本 - 需要记忆大量原子类名
  2. HTML 复杂度 - 类名可能会很长
  3. 设计约束 - 受限于预定义的设计系统
  4. 调试困难 - 样式分散在多个原子类中

原子化 CSS 的演进历程

graph TD
    A[传统 CSS] --> B[BEM 方法论]
    B --> C[CSS-in-JS]
    C --> D[原子化 CSS]
    D --> E[动态原子化]

    D --> F[TailwindCSS<br/>预编译]
    E --> G[UnoCSS<br/>即时生成]
    E --> H[Windi CSS<br/>按需编译]

现代原子化 CSS 引擎的工作原理

// 简化的原子化 CSS 引擎实现
class AtomicCSSEngine {
  private rules: Map<RegExp, (match: string[]) => Record<string, string>> =
    new Map();
  private cache: Map<string, string> = new Map();

  // 注册规则
  addRule(
    pattern: RegExp,
    generator: (match: string[]) => Record<string, string>
  ) {
    this.rules.set(pattern, generator);
  }

  // 生成 CSS
  generate(className: string): string | null {
    // 检查缓存
    if (this.cache.has(className)) {
      return this.cache.get(className)!;
    }

    // 遍历规则
    for (const [pattern, generator] of this.rules) {
      const match = className.match(pattern);
      if (match) {
        const styles = generator(match);
        const css = this.stylesToCSS(className, styles);
        this.cache.set(className, css);
        return css;
      }
    }

    return null;
  }

  private stylesToCSS(
    className: string,
    styles: Record<string, string>
  ): string {
    const properties = Object.entries(styles)
      .map(([prop, value]) => `  ${prop}: ${value};`)
      .join('\n');

    return `.${className} {\n${properties}\n}`;
  }
}

// 使用示例
const engine = new AtomicCSSEngine();

// 注册间距规则
engine.addRule(/^m-(\d+)$/, ([, size]) => ({
  margin: `${parseInt(size) * 4}px`,
}));

// 注册颜色规则
engine.addRule(/^text-(\w+)-(\d+)$/, ([, color, shade]) => ({
  color: getColorValue(color, shade),
}));

// 生成 CSS
console.log(engine.generate('m-4'));
// 输出: .m-4 { margin: 16px; }

PostCSS:CSS 转换工具的基石 {#postcss}

什么是 PostCSS?

PostCSS 是一个用 JavaScript 工具转换 CSS 的平台。它本身不是预处理器,而是一个允许使用插件来转换 CSS 的工具。PostCSS 可以做很多事情:添加浏览器前缀、使用未来的 CSS 语法、内联图片等。

PostCSS 的核心概念

// PostCSS的工作原理
const postcss = require('postcss');
const autoprefixer = require('autoprefixer');

postcss([autoprefixer])
  .process(css, { from: undefined })
  .then((result) => {
    console.log(result.css);
  });

常用 PostCSS 插件

  1. autoprefixer - 自动添加浏览器前缀
  2. postcss-preset-env - 使用现代 CSS 语法
  3. cssnano - CSS 压缩优化
  4. postcss-nested - 支持嵌套语法
  5. postcss-import - 处理@import 语句

PostCSS 配置示例

// postcss.config.js
module.exports = {
  plugins: [
    require('postcss-import'),
    require('postcss-nested'),
    require('autoprefixer'),
    require('cssnano')({
      preset: 'default',
    }),
  ],
};

PostCSS 在现代 CSS 工具链中的角色

PostCSS 作为 CSS 处理的基础设施,为现代 CSS 工具提供了强大的转换能力:

// PostCSS 插件系统的核心架构
interface PostCSSPlugin {
  pluginName: string;
  process(root: Root, result: Result): void;
}

// PostCSS AST 节点类型
interface CSSRule {
  type: 'rule';
  selector: string;
  declarations: CSSDeclaration[];
}

interface CSSDeclaration {
  type: 'decl';
  prop: string;
  value: string;
}

// PostCSS 处理流程
class PostCSSProcessor {
  private plugins: PostCSSPlugin[] = [];

  use(plugin: PostCSSPlugin) {
    this.plugins.push(plugin);
    return this;
  }

  process(css: string): string {
    // 1. 解析 CSS 为 AST
    const ast = this.parse(css);

    // 2. 依次应用插件
    for (const plugin of this.plugins) {
      plugin.process(ast, result);
    }

    // 3. 将 AST 转换回 CSS
    return this.stringify(ast);
  }
}

PostCSS 与现代原子化 CSS 引擎的关联关系 {#postcss-atomic-relationship}

PostCSS 与 UnoCSS 的技术关联

UnoCSS 虽然是独立的原子化 CSS 引擎,但它与 PostCSS 有着密切的技术关联:

// UnoCSS 中 PostCSS 的集成方式
import type { Plugin } from 'vite';
import { createGenerator } from '@unocss/core';
import postcss from 'postcss';

export function UnoCSS(): Plugin {
  const uno = createGenerator(/* config */);

  return {
    name: 'unocss',
    async transform(code, id) {
      // 1. 扫描代码中的原子类
      const tokens = extractTokens(code);

      // 2. 生成对应的 CSS
      const { css } = await uno.generate(tokens);

      // 3. 通过 PostCSS 处理生成的 CSS
      const processed = await postcss([autoprefixer(), cssnano()]).process(css);

      return processed.css;
    },
  };
}

PostCSS 插件与原子化 CSS 的结合

// 自定义 PostCSS 插件:原子化 CSS 生成器
const atomicCSSPlugin = (options = {}) => {
  return {
    postcssPlugin: 'atomic-css-generator',
    Once(root, { result }) {
      // 收集所有使用的原子类
      const usedClasses = new Set();

      // 扫描 HTML/JS 文件中的类名
      const htmlContent = getHTMLContent();
      const classRegex = /class[="']([^"']*)[="']/g;
      let match;

      while ((match = classRegex.exec(htmlContent)) !== null) {
        const classes = match[1].split(/\s+/);
        classes.forEach((cls) => usedClasses.add(cls));
      }

      // 生成对应的 CSS 规则
      usedClasses.forEach((className) => {
        const rule = generateAtomicRule(className);
        if (rule) {
          root.append(rule);
        }
      });
    },
  };
};

atomicCSSPlugin.postcss = true;

// 原子类规则生成器
function generateAtomicRule(className) {
  const rules = [
    // 间距规则
    {
      pattern: /^m-(\d+)$/,
      generate: ([, size]) => ({
        prop: 'margin',
        value: `${parseInt(size) * 0.25}rem`,
      }),
    },
    // 颜色规则
    {
      pattern: /^text-(\w+)-(\d+)$/,
      generate: ([, color, shade]) => ({
        prop: 'color',
        value: getColorValue(color, shade),
      }),
    },
  ];

  for (const rule of rules) {
    const match = className.match(rule.pattern);
    if (match) {
      const { prop, value } = rule.generate(match);
      return postcss
        .rule({ selector: `.${className}` })
        .append(postcss.decl({ prop, value }));
    }
  }

  return null;
}

PostCSS 与 TailwindCSS 的深度集成

TailwindCSS 本质上是一个复杂的 PostCSS 插件:

// TailwindCSS 的 PostCSS 插件架构
const tailwindcss = require('tailwindcss');

// TailwindCSS 插件内部实现概览
function createTailwindPlugin(config) {
  return {
    postcssPlugin: 'tailwindcss',

    // 处理 @tailwind 指令
    AtRule: {
      tailwind(atRule) {
        const directive = atRule.params;

        switch (directive) {
          case 'base':
            // 注入基础样式
            atRule.replaceWith(generateBaseStyles());
            break;

          case 'components':
            // 注入组件样式
            atRule.replaceWith(generateComponentStyles());
            break;

          case 'utilities':
            // 注入工具类样式
            atRule.replaceWith(generateUtilityStyles(config));
            break;
        }
      },
    },

    // 处理样式变体(如 hover:, focus: 等)
    Rule(rule) {
      processVariants(rule, config);
    },
  };
}

// 工具类生成逻辑
function generateUtilityStyles(config) {
  const utilities = [];

  // 生成间距工具类
  Object.entries(config.theme.spacing).forEach(([key, value]) => {
    utilities.push(
      // margin
      postcss
        .rule({ selector: `.m-${key}` })
        .append(postcss.decl({ prop: 'margin', value })),
      // padding
      postcss
        .rule({ selector: `.p-${key}` })
        .append(postcss.decl({ prop: 'padding', value }))
    );
  });

  // 生成颜色工具类
  Object.entries(config.theme.colors).forEach(([colorName, colorValues]) => {
    if (typeof colorValues === 'object') {
      Object.entries(colorValues).forEach(([shade, colorValue]) => {
        utilities.push(
          postcss
            .rule({ selector: `.text-${colorName}-${shade}` })
            .append(postcss.decl({ prop: 'color', value: colorValue })),
          postcss
            .rule({ selector: `.bg-${colorName}-${shade}` })
            .append(
              postcss.decl({ prop: 'background-color', value: colorValue })
            )
        );
      });
    }
  });

  return utilities;
}

三者的技术架构关系

graph TB
    subgraph "PostCSS 生态系统"
        A[PostCSS Core] --> B[Plugin System]
        B --> C[AST Parser]
        B --> D[AST Transformer]
        B --> E[CSS Generator]
    end

    subgraph "TailwindCSS"
        F[TailwindCSS Plugin] --> A
        F --> G[Config System]
        F --> H[Utility Generator]
        F --> I[Variant System]
    end

    subgraph "UnoCSS"
        J[UnoCSS Core] --> K[Rule Engine]
        K --> L[JIT Generator]
        J --> M[PostCSS Integration]
        M --> A
    end

    subgraph "构建工具集成"
        N[Webpack] --> O[PostCSS Loader]
        P[Vite] --> Q[PostCSS Plugin]
        O --> A
        Q --> A
    end

UnoCSS:即时原子化 CSS 引擎 {#unocss}

UnoCSS 简介

UnoCSS 是一个即时的原子化 CSS 引擎,由 Vue.js 团队核心成员 Anthony Fu 开发。它具有高性能、灵活性强、配置简单等特点。

UnoCSS 的核心特性

  1. 即时性 - 按需生成 CSS,只包含使用的样式
  2. 零运行时 - 构建时生成,无运行时开销
  3. 高度可定制 - 支持自定义规则、预设和变体
  4. TypeScript 支持 - 完整的类型定义

UnoCSS 基本使用

// uno.config.ts
import { defineConfig } from 'unocss';

export default defineConfig({
  // 自定义规则
  rules: [
    // 自定义颜色
    [/^text-brand-(\d+)$/, ([, d]) => ({ color: `#${d}` })],
    // 自定义间距
    [/^m-(\d+)$/, ([, d]) => ({ margin: `${d}px` })],
  ],

  // 预设
  presets: [
    // 默认预设,包含常用的原子化CSS类
    presetUno(),
    // 图标预设
    presetIcons({
      collections: {
        carbon: () =>
          import('@iconify-json/carbon/icons.json').then((i) => i.default),
      },
    }),
  ],

  // 快捷方式
  shortcuts: {
    btn: 'py-2 px-4 font-semibold rounded-lg shadow-md',
    'btn-primary': 'btn text-white bg-blue-500 hover:bg-blue-700',
  },
});

UnoCSS 实际应用示例

<!-- 使用原子化类名 -->
<div class="flex items-center justify-center min-h-screen bg-gray-100">
  <div class="max-w-md mx-auto bg-white rounded-xl shadow-md overflow-hidden">
    <div class="p-6">
      <h1 class="text-2xl font-bold text-gray-900 mb-4">UnoCSS示例</h1>
      <p class="text-gray-600 mb-6">这是一个使用UnoCSS的卡片组件</p>
      <button class="btn-primary">点击按钮</button>
    </div>
  </div>
</div>

TailwindCSS:实用优先的 CSS 框架 {#tailwindcss}

TailwindCSS 概述

TailwindCSS 是一个实用优先的 CSS 框架,提供了大量原子化的 CSS 类,让开发者可以快速构建现代化的用户界面。

TailwindCSS 的优势

  1. 实用优先 - 提供低级别的实用类
  2. 响应式设计 - 内置响应式前缀
  3. 组件友好 - 易于提取组件
  4. 可定制性 - 高度可配置的设计系统

TailwindCSS 配置

// tailwind.config.js
module.exports = {
  content: ['./src/**/*.{html,js,ts,jsx,tsx,vue}'],
  theme: {
    extend: {
      colors: {
        brand: {
          50: '#eff6ff',
          500: '#3b82f6',
          900: '#1e3a8a',
        },
      },
      fontFamily: {
        sans: ['Inter', 'system-ui', 'sans-serif'],
      },
      spacing: {
        72: '18rem',
        84: '21rem',
        96: '24rem',
      },
    },
  },
  plugins: [require('@tailwindcss/forms'), require('@tailwindcss/typography')],
};

TailwindCSS 实际应用

<!-- 响应式导航栏 -->
<nav class="bg-white shadow-lg">
  <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
    <div class="flex justify-between h-16">
      <div class="flex items-center">
        <div class="flex-shrink-0">
          <img class="h-8 w-8" src="/logo.svg" alt="Logo" />
        </div>
      </div>

      <!-- 桌面端菜单 -->
      <div class="hidden md:flex items-center space-x-8">
        <a
          href="#"
          class="text-gray-900 hover:text-blue-600 px-3 py-2 text-sm font-medium"
        >
          首页
        </a>
        <a
          href="#"
          class="text-gray-900 hover:text-blue-600 px-3 py-2 text-sm font-medium"
        >
          产品
        </a>
        <a
          href="#"
          class="text-gray-900 hover:text-blue-600 px-3 py-2 text-sm font-medium"
        >
          关于
        </a>
      </div>

      <!-- 移动端菜单按钮 -->
      <div class="md:hidden flex items-center">
        <button class="text-gray-900 hover:text-blue-600 focus:outline-none">
          <svg
            class="h-6 w-6"
            fill="none"
            viewBox="0 0 24 24"
            stroke="currentColor"
          >
            <path
              stroke-linecap="round"
              stroke-linejoin="round"
              stroke-width="2"
              d="M4 6h16M4 12h16M4 18h16"
            />
          </svg>
        </button>
      </div>
    </div>
  </div>
</nav>

各大框架中的 CSS 转换实现机制 {#framework-css-transformation}

构建工具中的 CSS 处理流程

现代前端构建工具通过插件系统实现 CSS 的转换和处理,以下是主要框架的实现机制:

Webpack 中的 CSS 处理机制

// Webpack CSS 处理链路
class WebpackCSSProcessor {
  constructor() {
    this.loaders = [];
    this.plugins = [];
  }

  // CSS 处理流程
  processCSS(source, resourcePath) {
    // 1. CSS 文件加载
    let css = source;

    // 2. 通过 loader 链处理
    for (const loader of this.loaders.reverse()) {
      css = loader.process(css, resourcePath);
    }

    // 3. 生成模块代码
    return this.generateModule(css);
  }

  // 生成 JavaScript 模块
  generateModule(css) {
    return `
      // 运行时注入样式
      const style = document.createElement('style')
      style.textContent = ${JSON.stringify(css)}
      document.head.appendChild(style)
      
      // 热重载支持
      if (module.hot) {
        module.hot.accept()
        module.hot.dispose(() => {
          document.head.removeChild(style)
        })
      }
    `;
  }
}

// Webpack 配置示例
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          // 4. 将 CSS 注入到 DOM
          'style-loader',
          // 3. 处理 CSS 模块化
          {
            loader: 'css-loader',
            options: {
              modules: true,
              importLoaders: 1,
            },
          },
          // 2. PostCSS 处理
          {
            loader: 'postcss-loader',
            options: {
              postcssOptions: {
                plugins: [require('tailwindcss'), require('autoprefixer')],
              },
            },
          },
          // 1. 预处理器处理(如果需要)
          'sass-loader',
        ],
      },
    ],
  },
};

Vite 中的 CSS 处理机制

// Vite CSS 插件实现
import { Plugin } from 'vite';
import postcss from 'postcss';

export function createCSSPlugin(): Plugin {
  const cssModules = new Map<string, string>();

  return {
    name: 'vite:css',

    // 加载阶段
    async load(id) {
      if (!id.endsWith('.css')) return;

      // 读取 CSS 文件
      const css = await fs.readFile(id, 'utf-8');
      return css;
    },

    // 转换阶段
    async transform(code, id) {
      if (!id.endsWith('.css')) return;

      // PostCSS 处理
      const result = await postcss([
        require('tailwindcss'),
        require('autoprefixer'),
      ]).process(code, { from: id });

      // 开发环境:注入样式
      if (process.env.NODE_ENV === 'development') {
        return {
          code: `
            // 创建样式标签
            const style = document.createElement('style')
            style.setAttribute('data-vite-dev-id', ${JSON.stringify(id)})
            style.textContent = ${JSON.stringify(result.css)}
            document.head.appendChild(style)
            
            // HMR 更新
            if (import.meta.hot) {
              import.meta.hot.accept(() => {
                style.textContent = ${JSON.stringify(result.css)}
              })
              
              import.meta.hot.prune(() => {
                document.head.removeChild(style)
              })
            }
          `,
          map: result.map,
        };
      }

      // 生产环境:收集样式
      cssModules.set(id, result.css);
      return {
        code: `export default ${JSON.stringify(result.css)}`,
        map: result.map,
      };
    },

    // 构建阶段
    generateBundle() {
      // 将所有 CSS 合并到单个文件
      const allCSS = Array.from(cssModules.values()).join('\n');

      this.emitFile({
        type: 'asset',
        fileName: 'style.css',
        source: allCSS,
      });
    },
  };
}

Rollup 中的 CSS 处理

// Rollup CSS 插件
import { createFilter } from '@rollup/pluginutils';

export function css(options = {}) {
  const filter = createFilter(options.include || ['**/*.css'], options.exclude);
  const styles = new Map();

  return {
    name: 'css',

    async transform(code, id) {
      if (!filter(id)) return;

      // 处理 CSS
      const processed = await processCSS(code, id, options);
      styles.set(id, processed.css);

      // 返回 JavaScript 模块
      return {
        code: options.inject
          ? generateInjectCode(processed.css)
          : `export default ${JSON.stringify(processed.css)}`,
        map: { mappings: '' },
      };
    },

    generateBundle(opts, bundle) {
      // 提取所有 CSS
      const css = Array.from(styles.values()).join('\n');

      // 输出 CSS 文件
      this.emitFile({
        type: 'asset',
        fileName: options.output || 'bundle.css',
        source: css,
      });
    },
  };
}

原子化 CSS 引擎的集成机制

UnoCSS 在不同构建工具中的集成

// UnoCSS Vite 插件
export function UnoCSS(configOrPath?: UserConfig | string): Plugin[] {
  const uno = createGenerator(config);

  return [
    // 主插件
    {
      name: 'unocss:global',
      configResolved(config) {
        // 配置解析完成后初始化
        initializeUnoCSS(config);
      },

      buildStart() {
        // 构建开始时重置状态
        uno.reset();
      },
    },

    // CSS 生成插件
    {
      name: 'unocss:css',
      resolveId(id) {
        // 虚拟模块解析
        if (id === 'virtual:uno.css') {
          return id;
        }
      },

      async load(id) {
        if (id === 'virtual:uno.css') {
          // 生成 CSS
          const { css } = await uno.generate(getUsedTokens());
          return css;
        }
      },
    },

    // 代码扫描插件
    {
      name: 'unocss:scanner',
      async transform(code, id) {
        // 扫描代码中的原子类
        const tokens = extractTokens(code);
        uno.addTokens(tokens);

        return null; // 不修改代码
      },
    },
  ];
}

// Webpack 插件
class UnoWebpackPlugin {
  constructor(options) {
    this.uno = createGenerator(options);
    this.tokens = new Set();
  }

  apply(compiler) {
    // 代码扫描阶段
    compiler.hooks.compilation.tap('UnoCSS', (compilation) => {
      compilation.hooks.seal.tap('UnoCSS', () => {
        // 扫描所有模块
        for (const module of compilation.modules) {
          if (module._source) {
            const tokens = extractTokens(module._source.source());
            tokens.forEach((token) => this.tokens.add(token));
          }
        }
      });
    });

    // CSS 生成阶段
    compiler.hooks.emit.tapAsync('UnoCSS', async (compilation, callback) => {
      const { css } = await this.uno.generate(this.tokens);

      // 添加 CSS 资源
      compilation.assets['uno.css'] = {
        source: () => css,
        size: () => css.length,
      };

      callback();
    });
  }
}

CSS-in-JS 的实现机制

运行时 CSS-in-JS(如 styled-components)

// styled-components 的简化实现
class StyledComponent {
  constructor(tag, styles) {
    this.tag = tag;
    this.styles = styles;
    this.className = this.generateClassName();
  }

  generateClassName() {
    // 生成唯一类名
    const hash = hashString(this.styles.join(''));
    return `sc-${hash}`;
  }

  injectStyles() {
    // 注入样式到页面
    if (!document.querySelector(`[data-styled="${this.className}"]`)) {
      const style = document.createElement('style');
      style.setAttribute('data-styled', this.className);
      style.textContent = `.${this.className} { ${this.styles.join(';')} }`;
      document.head.appendChild(style);
    }
  }

  render(props, children) {
    this.injectStyles();

    return React.createElement(
      this.tag,
      { ...props, className: this.className },
      children
    );
  }
}

// 使用方式
const Button = styled.button`
  padding: 10px 20px;
  background-color: blue;
  color: white;
`;

编译时 CSS-in-JS(如 Linaria)

// Linaria 编译器
const linariaTransform = {
  visitor: {
    TaggedTemplateExpression(path) {
      if (path.node.tag.name === 'css') {
        // 提取 CSS 内容
        const cssContent = path.node.quasi.quasis[0].value.raw;

        // 生成类名
        const className = generateHash(cssContent);

        // 将 CSS 写入文件
        writeCSS(className, cssContent);

        // 替换为类名
        path.replaceWith(t.stringLiteral(className));
      }
    },
  },
};

// 编译前
const buttonStyle = css`
  padding: 10px 20px;
  background-color: blue;
`;

// 编译后
const buttonStyle = 'button_abc123';
// 同时生成 button_abc123.css 文件

在不同构建工具中的配置 {#build-tools}

Webpack 配置

PostCSS + Webpack

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader',
          {
            loader: 'postcss-loader',
            options: {
              postcssOptions: {
                plugins: [require('autoprefixer'), require('tailwindcss')],
              },
            },
          },
        ],
      },
    ],
  },
};

UnoCSS + Webpack

// webpack.config.js
const UnoCSS = require('@unocss/webpack').default;

module.exports = {
  plugins: [UnoCSS()],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
};

Vite 配置

PostCSS + Vite

// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
  css: {
    postcss: {
      plugins: [require('tailwindcss'), require('autoprefixer')],
    },
  },
});

UnoCSS + Vite

// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import UnoCSS from 'unocss/vite';

export default defineConfig({
  plugins: [vue(), UnoCSS()],
});

TailwindCSS + Vite

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  css: {
    postcss: {
      plugins: [
        require('tailwindcss'),
        require('autoprefixer'),
      ],
    },
  },
})

// package.json
{
  "devDependencies": {
    "tailwindcss": "^3.3.0",
    "autoprefixer": "^10.4.14",
    "postcss": "^8.4.24"
  }
}

CSS 原子化在不同框架中的封装原理 {#atomic-css-framework-implementation}

React 中的原子化 CSS 封装

Hooks 驱动的动态样式系统

// useAtomicStyles Hook 实现
import { useMemo, useEffect } from 'react';

interface StyleRule {
  selector: string;
  properties: Record<string, string>;
}

export function useAtomicStyles(classNames: string[]) {
  const [styleSheet, setStyleSheet] = useState<CSSStyleSheet | null>(null);

  // 创建样式表
  useEffect(() => {
    const sheet = new CSSStyleSheet();
    document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
    setStyleSheet(sheet);

    return () => {
      document.adoptedStyleSheets = document.adoptedStyleSheets.filter(
        (s) => s !== sheet
      );
    };
  }, []);

  // 生成和注入样式
  const generatedStyles = useMemo(() => {
    const rules: StyleRule[] = [];

    classNames.forEach((className) => {
      const rule = generateAtomicRule(className);
      if (rule && styleSheet) {
        const ruleText = `${rule.selector} { ${Object.entries(rule.properties)
          .map(([prop, value]) => `${prop}: ${value}`)
          .join('; ')} }`;

        try {
          styleSheet.insertRule(ruleText);
          rules.push(rule);
        } catch (error) {
          console.warn(`Failed to insert rule: ${ruleText}`, error);
        }
      }
    });

    return rules;
  }, [classNames, styleSheet]);

  return {
    styles: generatedStyles,
    className: classNames.join(' '),
  };
}

// 原子化样式组件工厂
export function createAtomicComponent<T extends keyof JSX.IntrinsicElements>(
  tag: T,
  defaultClasses: string[] = []
) {
  return React.forwardRef<
    React.ElementRef<T>,
    React.ComponentPropsWithRef<T> & { atomicClasses?: string[] }
  >(({ atomicClasses = [], className, ...props }, ref) => {
    const allClasses = [...defaultClasses, ...atomicClasses];
    const { className: generatedClassName } = useAtomicStyles(allClasses);

    return React.createElement(tag, {
      ...props,
      ref,
      className: [generatedClassName, className].filter(Boolean).join(' '),
    });
  });
}

// 使用示例
const AtomicButton = createAtomicComponent('button', [
  'px-4',
  'py-2',
  'rounded',
]);

function App() {
  return (
    <AtomicButton
      atomicClasses={['bg-blue-500', 'text-white', 'hover:bg-blue-600']}
      onClick={() => console.log('clicked')}
    >
      Click me
    </AtomicButton>
  );
}

React 中的编译时优化

// Babel 插件:编译时原子化 CSS 提取
export default function atomicCSSBabelPlugin() {
  return {
    visitor: {
      JSXAttribute(path) {
        if (path.node.name.name === 'className') {
          const value = path.node.value;

          if (value && value.type === 'StringLiteral') {
            const classNames = value.value.split(' ');
            const { atomicClasses, regularClasses } =
              separateClasses(classNames);

            if (atomicClasses.length > 0) {
              // 生成 CSS 到构建输出
              generateCSSFile(atomicClasses);

              // 保留常规类名
              if (regularClasses.length > 0) {
                path.node.value.value = regularClasses.join(' ');
              } else {
                path.remove();
              }
            }
          }
        }
      },
    },
  };
}

Vue 中的原子化 CSS 封装

Vue 3 Composition API 的实现

// useAtomicCSS Composable
import { ref, computed, watch, onUnmounted } from 'vue';

export function useAtomicCSS(classes: Ref<string[]> | string[]) {
  const styleElement = ref<HTMLStyleElement | null>(null);
  const injectedRules = ref(new Set<string>());

  // 创建样式元素
  const createStyleElement = () => {
    const style = document.createElement('style');
    style.setAttribute('data-vue-atomic', '');
    document.head.appendChild(style);
    styleElement.value = style;
    return style;
  };

  // 注入原子化样式
  const injectStyles = (classNames: string[]) => {
    if (!styleElement.value) {
      createStyleElement();
    }

    const newRules: string[] = [];

    classNames.forEach((className) => {
      if (!injectedRules.value.has(className)) {
        const rule = generateAtomicRule(className);
        if (rule) {
          const css = `.${className} { ${Object.entries(rule.properties)
            .map(([prop, value]) => `${prop}: ${value}`)
            .join('; ')} }`;

          newRules.push(css);
          injectedRules.value.add(className);
        }
      }
    });

    if (newRules.length > 0 && styleElement.value) {
      styleElement.value.textContent += newRules.join('\n');
    }
  };

  // 响应式处理类名变化
  const classNames = computed(() =>
    Array.isArray(classes) ? classes : unref(classes)
  );

  watch(
    classNames,
    (newClasses) => {
      injectStyles(newClasses);
    },
    { immediate: true }
  );

  // 清理
  onUnmounted(() => {
    if (styleElement.value) {
      document.head.removeChild(styleElement.value);
    }
  });

  return {
    className: computed(() => classNames.value.join(' ')),
  };
}

// Vue 指令实现
export const vAtomic = {
  mounted(el: HTMLElement, binding: DirectiveBinding) {
    const classes = Array.isArray(binding.value)
      ? binding.value
      : binding.value.split(' ');

    const { className } = useAtomicCSS(classes);
    el.className = className.value;
  },

  updated(el: HTMLElement, binding: DirectiveBinding) {
    const classes = Array.isArray(binding.value)
      ? binding.value
      : binding.value.split(' ');

    const { className } = useAtomicCSS(classes);
    el.className = className.value;
  },
};

Vue SFC 编译器集成

// Vue SFC 编译器插件
export function createAtomicCSSPlugin(): Plugin {
  const extractedClasses = new Set<string>();

  return {
    name: 'vue-atomic-css',

    transform(code, id) {
      if (!id.endsWith('.vue')) return;

      // 解析 Vue SFC
      const { descriptor } = parse(code);

      // 处理模板中的原子类
      if (descriptor.template) {
        const templateContent = descriptor.template.content;
        const classMatches = templateContent.matchAll(/class="([^"]*)"/g);

        for (const match of classMatches) {
          const classes = match[1].split(' ');
          classes.forEach((cls) => extractedClasses.add(cls));
        }
      }

      // 处理 script 中的动态类
      if (descriptor.script || descriptor.scriptSetup) {
        const scriptContent =
          descriptor.script?.content || descriptor.scriptSetup?.content || '';

        // 查找字符串字面量中的类名
        const stringMatches = scriptContent.matchAll(/'([^']*)'|"([^"]*)"/g);
        for (const match of stringMatches) {
          const str = match[1] || match[2];
          if (str && isLikelyClassName(str)) {
            str.split(' ').forEach((cls) => extractedClasses.add(cls));
          }
        }
      }

      return null;
    },

    generateBundle() {
      // 生成原子化 CSS
      const css = generateAtomicCSS(Array.from(extractedClasses));

      this.emitFile({
        type: 'asset',
        fileName: 'atomic.css',
        source: css,
      });
    },
  };
}

Angular 中的原子化 CSS 封装

Angular 服务驱动的样式管理

// AtomicCSSService
@Injectable({
  providedIn: 'root',
})
export class AtomicCSSService {
  private styleSheet: CSSStyleSheet;
  private injectedRules = new Set<string>();

  constructor(@Inject(DOCUMENT) private document: Document) {
    this.styleSheet = new CSSStyleSheet();
    this.document.adoptedStyleSheets = [
      ...this.document.adoptedStyleSheets,
      this.styleSheet,
    ];
  }

  injectClasses(classNames: string[]): void {
    classNames.forEach((className) => {
      if (!this.injectedRules.has(className)) {
        const rule = this.generateRule(className);
        if (rule) {
          try {
            this.styleSheet.insertRule(rule);
            this.injectedRules.add(className);
          } catch (error) {
            console.warn(`Failed to inject atomic class: ${className}`, error);
          }
        }
      }
    });
  }

  private generateRule(className: string): string | null {
    const atomicRule = generateAtomicRule(className);
    if (!atomicRule) return null;

    const properties = Object.entries(atomicRule.properties)
      .map(([prop, value]) => `${prop}: ${value}`)
      .join('; ');

    return `.${className} { ${properties} }`;
  }
}

// Angular 指令
@Directive({
  selector: '[atomic]',
})
export class AtomicDirective implements OnInit, OnChanges {
  @Input() atomic: string | string[] = [];

  constructor(
    private atomicCSS: AtomicCSSService,
    private el: ElementRef,
    private renderer: Renderer2
  ) {}

  ngOnInit() {
    this.applyClasses();
  }

  ngOnChanges() {
    this.applyClasses();
  }

  private applyClasses() {
    const classes = Array.isArray(this.atomic)
      ? this.atomic
      : this.atomic.split(' ');

    // 注入样式
    this.atomicCSS.injectClasses(classes);

    // 应用类名
    classes.forEach((className) => {
      this.renderer.addClass(this.el.nativeElement, className);
    });
  }
}

Angular 编译器集成

// Angular 编译器转换器
export function createAtomicCSSTransformer(): ts.TransformerFactory<ts.SourceFile> {
  return (context: ts.TransformationContext) => {
    return (sourceFile: ts.SourceFile) => {
      const extractedClasses = new Set<string>();

      function visit(node: ts.Node): ts.Node {
        // 查找模板字符串中的类名
        if (
          ts.isTemplateExpression(node) ||
          ts.isNoSubstitutionTemplateLiteral(node)
        ) {
          const text = node.getText();
          const classMatches = text.matchAll(/class="([^"]*)"/g);

          for (const match of classMatches) {
            const classes = match[1].split(' ');
            classes.forEach((cls) => extractedClasses.add(cls));
          }
        }

        return ts.visitEachChild(node, visit, context);
      }

      const result = ts.visitNode(sourceFile, visit);

      // 生成 CSS 文件
      if (extractedClasses.size > 0) {
        generateAtomicCSSFile(Array.from(extractedClasses));
      }

      return result;
    };
  };
}

跨框架的原子化 CSS 运行时

// 通用原子化 CSS 运行时
class UniversalAtomicCSS {
  private static instance: UniversalAtomicCSS;
  private styleSheet: CSSStyleSheet;
  private cache = new Map<string, boolean>();
  private rules = new Map<
    RegExp,
    (match: string[]) => Record<string, string>
  >();

  private constructor() {
    this.styleSheet = new CSSStyleSheet();
    document.adoptedStyleSheets = [
      ...document.adoptedStyleSheets,
      this.styleSheet,
    ];
    this.initDefaultRules();
  }

  static getInstance(): UniversalAtomicCSS {
    if (!this.instance) {
      this.instance = new UniversalAtomicCSS();
    }
    return this.instance;
  }

  // 注册原子化规则
  addRule(
    pattern: RegExp,
    generator: (match: string[]) => Record<string, string>
  ) {
    this.rules.set(pattern, generator);
  }

  // 应用原子化类
  apply(element: HTMLElement, classNames: string[]) {
    const newClasses: string[] = [];

    classNames.forEach((className) => {
      if (!this.cache.has(className)) {
        const rule = this.generateRule(className);
        if (rule) {
          this.injectRule(className, rule);
          this.cache.set(className, true);
        } else {
          this.cache.set(className, false);
        }
      }

      if (this.cache.get(className)) {
        newClasses.push(className);
      }
    });

    element.className = newClasses.join(' ');
  }

  private generateRule(className: string): Record<string, string> | null {
    for (const [pattern, generator] of this.rules) {
      const match = className.match(pattern);
      if (match) {
        return generator(match);
      }
    }
    return null;
  }

  private injectRule(className: string, properties: Record<string, string>) {
    const css = `.${className} { ${Object.entries(properties)
      .map(([prop, value]) => `${prop}: ${value}`)
      .join('; ')} }`;

    try {
      this.styleSheet.insertRule(css);
    } catch (error) {
      console.warn(`Failed to inject rule: ${css}`, error);
    }
  }

  private initDefaultRules() {
    // 间距规则
    this.addRule(/^m-(\d+)$/, ([, size]) => ({
      margin: `${parseInt(size) * 0.25}rem`,
    }));

    this.addRule(/^p-(\d+)$/, ([, size]) => ({
      padding: `${parseInt(size) * 0.25}rem`,
    }));

    // 颜色规则
    this.addRule(/^text-(\w+)-(\d+)$/, ([, color, shade]) => ({
      color: getColorValue(color, shade),
    }));

    this.addRule(/^bg-(\w+)-(\d+)$/, ([, color, shade]) => ({
      'background-color': getColorValue(color, shade),
    }));
  }
}

// 框架适配器
export const atomicCSS = {
  // React 适配
  useAtomic: (classes: string[]) => {
    const atomic = UniversalAtomicCSS.getInstance();
    const ref = useRef<HTMLElement>(null);

    useEffect(() => {
      if (ref.current) {
        atomic.apply(ref.current, classes);
      }
    }, [classes]);

    return ref;
  },

  // Vue 适配
  vAtomic: {
    mounted(el: HTMLElement, binding: any) {
      const atomic = UniversalAtomicCSS.getInstance();
      const classes = Array.isArray(binding.value)
        ? binding.value
        : [binding.value];
      atomic.apply(el, classes);
    },
  },

  // Angular 适配
  applyToElement: (element: HTMLElement, classes: string[]) => {
    const atomic = UniversalAtomicCSS.getInstance();
    atomic.apply(element, classes);
  },
};

Vue 项目实战应用 {#vue-integration}

Vue 3 + UnoCSS 实战

# 安装依赖
npm install -D unocss @unocss/preset-uno @unocss/preset-icons
// uno.config.ts
import { defineConfig, presetUno, presetIcons } from 'unocss';

export default defineConfig({
  presets: [
    presetUno(),
    presetIcons({
      collections: {
        mdi: () =>
          import('@iconify-json/mdi/icons.json').then((i) => i.default),
      },
    }),
  ],
  shortcuts: {
    'btn-base': 'px-4 py-2 rounded font-medium transition-colors',
    'btn-primary': 'btn-base bg-blue-500 text-white hover:bg-blue-600',
    'btn-secondary': 'btn-base bg-gray-200 text-gray-800 hover:bg-gray-300',
  },
});
<!-- App.vue -->
<template>
  <div class="min-h-screen bg-gray-50">
    <!-- 头部 -->
    <header class="bg-white shadow-sm border-b">
      <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <div class="flex justify-between items-center py-4">
          <h1 class="text-2xl font-bold text-gray-900">Vue + UnoCSS</h1>
          <div class="flex items-center space-x-4">
            <button class="btn-primary">
              <i class="i-mdi-plus mr-2"></i>
              新建
            </button>
            <button class="btn-secondary">
              <i class="i-mdi-cog mr-2"></i>
              设置
            </button>
          </div>
        </div>
      </div>
    </header>

    <!-- 主要内容 -->
    <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
      <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        <div
          v-for="item in items"
          :key="item.id"
          class="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow"
        >
          <div class="flex items-center mb-4">
            <div
              class="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center mr-3"
            >
              <i class="i-mdi-file-document text-blue-600 text-xl"></i>
            </div>
            <h3 class="text-lg font-medium text-gray-900">{{ item.title }}</h3>
          </div>
          <p class="text-gray-600 mb-4">{{ item.description }}</p>
          <div class="flex justify-between items-center">
            <span class="text-sm text-gray-500">{{ item.date }}</span>
            <button class="text-blue-600 hover:text-blue-800 font-medium">
              查看详情
            </button>
          </div>
        </div>
      </div>
    </main>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';

interface Item {
  id: number;
  title: string;
  description: string;
  date: string;
}

const items = ref<Item[]>([
  {
    id: 1,
    title: '项目文档',
    description: '项目的详细文档和说明',
    date: '2024-01-15',
  },
  {
    id: 2,
    title: '设计稿',
    description: 'UI设计和交互原型',
    date: '2024-01-14',
  },
  {
    id: 3,
    title: '代码规范',
    description: '团队开发规范和最佳实践',
    date: '2024-01-13',
  },
]);
</script>

Vue 3 + TailwindCSS 实战

<!-- ProductCard.vue -->
<template>
  <div
    class="bg-white rounded-xl shadow-lg overflow-hidden hover:shadow-xl transition-shadow duration-300"
  >
    <!-- 产品图片 -->
    <div class="relative">
      <img
        :src="product.image"
        :alt="product.name"
        class="w-full h-48 object-cover"
      />
      <div class="absolute top-2 right-2">
        <button
          @click="toggleFavorite"
          class="p-2 rounded-full bg-white/80 hover:bg-white transition-colors"
          :class="{ 'text-red-500': isFavorite, 'text-gray-400': !isFavorite }"
        >
          <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
            <path
              d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z"
            />
          </svg>
        </button>
      </div>
    </div>

    <!-- 产品信息 -->
    <div class="p-6">
      <div class="flex items-start justify-between mb-2">
        <h3 class="text-lg font-semibold text-gray-900 line-clamp-2">
          {{ product.name }}
        </h3>
        <span
          class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
          :class="stockStatusClass"
        >
          {{ stockStatusText }}
        </span>
      </div>

      <p class="text-gray-600 text-sm mb-4 line-clamp-3">
        {{ product.description }}
      </p>

      <!-- 价格和评分 -->
      <div class="flex items-center justify-between mb-4">
        <div class="flex items-baseline">
          <span class="text-2xl font-bold text-gray-900"
            >¥{{ product.price }}</span
          >
          <span
            v-if="product.originalPrice"
            class="ml-2 text-sm text-gray-500 line-through"
          >
            ¥{{ product.originalPrice }}
          </span>
        </div>
        <div class="flex items-center">
          <div class="flex text-yellow-400">
            <svg
              v-for="i in 5"
              :key="i"
              class="w-4 h-4"
              :class="i <= product.rating ? 'fill-current' : 'text-gray-300'"
              viewBox="0 0 20 20"
            >
              <path
                d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"
              />
            </svg>
          </div>
          <span class="ml-1 text-sm text-gray-600"
            >({{ product.reviews }})</span
          >
        </div>
      </div>

      <!-- 操作按钮 -->
      <div class="flex space-x-3">
        <button
          @click="addToCart"
          :disabled="product.stock === 0"
          class="flex-1 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed text-white font-medium py-2 px-4 rounded-lg transition-colors"
        >
          {{ product.stock === 0 ? '缺货' : '加入购物车' }}
        </button>
        <button
          @click="buyNow"
          :disabled="product.stock === 0"
          class="flex-1 border border-blue-600 text-blue-600 hover:bg-blue-50 disabled:border-gray-300 disabled:text-gray-300 disabled:cursor-not-allowed font-medium py-2 px-4 rounded-lg transition-colors"
        >
          立即购买
        </button>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue';

interface Product {
  id: number;
  name: string;
  description: string;
  price: number;
  originalPrice?: number;
  image: string;
  rating: number;
  reviews: number;
  stock: number;
}

const props = defineProps<{
  product: Product;
}>();

const emit = defineEmits<{
  addToCart: [product: Product];
  buyNow: [product: Product];
  toggleFavorite: [productId: number];
}>();

const isFavorite = ref(false);

const stockStatusClass = computed(() => {
  if (props.product.stock === 0) {
    return 'bg-red-100 text-red-800';
  } else if (props.product.stock < 10) {
    return 'bg-yellow-100 text-yellow-800';
  }
  return 'bg-green-100 text-green-800';
});

const stockStatusText = computed(() => {
  if (props.product.stock === 0) {
    return '缺货';
  } else if (props.product.stock < 10) {
    return '库存紧张';
  }
  return '有货';
});

const toggleFavorite = () => {
  isFavorite.value = !isFavorite.value;
  emit('toggleFavorite', props.product.id);
};

const addToCart = () => {
  emit('addToCart', props.product);
};

const buyNow = () => {
  emit('buyNow', props.product);
};
</script>

<style scoped>
.line-clamp-2 {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.line-clamp-3 {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
</style>

React 项目实战应用 {#react-integration}

React + UnoCSS 实战

# 安装依赖
npm install -D unocss @unocss/preset-uno @unocss/preset-icons @unocss/vite
// uno.config.ts
import { defineConfig, presetUno, presetIcons } from 'unocss';

export default defineConfig({
  presets: [
    presetUno(),
    presetIcons({
      collections: {
        heroicons: () =>
          import('@iconify-json/heroicons/icons.json').then((i) => i.default),
      },
    }),
  ],
  shortcuts: {
    container: 'max-w-7xl mx-auto px-4 sm:px-6 lg:px-8',
    card: 'bg-white rounded-lg shadow-md p-6',
    btn: 'px-4 py-2 rounded-md font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2',
    'btn-primary':
      'btn bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
    'btn-secondary':
      'btn bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500',
  },
});
// Dashboard.tsx
import React, { useState } from 'react';

interface DashboardItem {
  id: string;
  title: string;
  value: string | number;
  change: string;
  trend: 'up' | 'down' | 'neutral';
  icon: string;
}

const Dashboard: React.FC = () => {
  const [selectedPeriod, setSelectedPeriod] = useState('7d');

  const dashboardData: DashboardItem[] = [
    {
      id: '1',
      title: '总收入',
      value: '¥125,430',
      change: '+12.5%',
      trend: 'up',
      icon: 'i-heroicons-currency-dollar',
    },
    {
      id: '2',
      title: '新用户',
      value: '2,345',
      change: '+8.2%',
      trend: 'up',
      icon: 'i-heroicons-users',
    },
    {
      id: '3',
      title: '订单数',
      value: '1,234',
      change: '-3.1%',
      trend: 'down',
      icon: 'i-heroicons-shopping-cart',
    },
    {
      id: '4',
      title: '转化率',
      value: '3.24%',
      change: '+0.5%',
      trend: 'up',
      icon: 'i-heroicons-chart-bar',
    },
  ];

  const periods = [
    { value: '24h', label: '24小时' },
    { value: '7d', label: '7天' },
    { value: '30d', label: '30天' },
    { value: '90d', label: '90天' },
  ];

  const getTrendColor = (trend: string) => {
    switch (trend) {
      case 'up':
        return 'text-green-600';
      case 'down':
        return 'text-red-600';
      default:
        return 'text-gray-600';
    }
  };

  const getTrendIcon = (trend: string) => {
    switch (trend) {
      case 'up':
        return 'i-heroicons-arrow-trending-up';
      case 'down':
        return 'i-heroicons-arrow-trending-down';
      default:
        return 'i-heroicons-minus';
    }
  };

  return (
    <div class="min-h-screen bg-gray-50">
      {/* 头部 */}
      <header class="bg-white shadow-sm">
        <div class="container">
          <div class="flex justify-between items-center py-6">
            <div>
              <h1 class="text-3xl font-bold text-gray-900">仪表板</h1>
              <p class="mt-1 text-gray-600">欢迎回来,查看您的业务概况</p>
            </div>

            {/* 时间选择器 */}
            <div class="flex items-center space-x-2">
              <span class="text-sm font-medium text-gray-700">时间范围:</span>
              <select
                value={selectedPeriod}
                onChange={(e) => setSelectedPeriod(e.target.value)}
                class="block w-32 rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
              >
                {periods.map((period) => (
                  <option key={period.value} value={period.value}>
                    {period.label}
                  </option>
                ))}
              </select>
            </div>
          </div>
        </div>
      </header>

      {/* 主要内容 */}
      <main class="container py-8">
        {/* 数据卡片网格 */}
        <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
          {dashboardData.map((item) => (
            <div key={item.id} class="card hover:shadow-lg transition-shadow">
              <div class="flex items-center justify-between">
                <div>
                  <p class="text-sm font-medium text-gray-600 mb-1">
                    {item.title}
                  </p>
                  <p class="text-2xl font-bold text-gray-900">{item.value}</p>
                </div>
                <div
                  class={`w-12 h-12 rounded-full bg-blue-100 flex items-center justify-center`}
                >
                  <i class={`${item.icon} text-blue-600 text-xl`}></i>
                </div>
              </div>

              <div class="mt-4 flex items-center">
                <div class={`flex items-center ${getTrendColor(item.trend)}`}>
                  <i class={`${getTrendIcon(item.trend)} mr-1 text-sm`}></i>
                  <span class="text-sm font-medium">{item.change}</span>
                </div>
                <span class="text-sm text-gray-500 ml-2">vs 上个周期</span>
              </div>
            </div>
          ))}
        </div>

        {/* 图表区域 */}
        <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
          {/* 收入趋势图 */}
          <div class="card">
            <div class="flex items-center justify-between mb-6">
              <h3 class="text-lg font-semibold text-gray-900">收入趋势</h3>
              <button class="btn-secondary text-sm">
                <i class="i-heroicons-arrow-down-tray mr-2"></i>
                导出
              </button>
            </div>
            <div class="h-64 bg-gray-100 rounded-lg flex items-center justify-center">
              <p class="text-gray-500">图表区域 (集成 Chart.js 或其他图表库)</p>
            </div>
          </div>

          {/* 用户活动 */}
          <div class="card">
            <div class="flex items-center justify-between mb-6">
              <h3 class="text-lg font-semibold text-gray-900">用户活动</h3>
              <button class="btn-secondary text-sm">查看全部</button>
            </div>

            <div class="space-y-4">
              {[
                {
                  user: '张三',
                  action: '完成了购买',
                  time: '2分钟前',
                  avatar: '👤',
                },
                {
                  user: '李四',
                  action: '注册了账户',
                  time: '5分钟前',
                  avatar: '👤',
                },
                {
                  user: '王五',
                  action: '更新了资料',
                  time: '10分钟前',
                  avatar: '👤',
                },
                {
                  user: '赵六',
                  action: '发布了评论',
                  time: '15分钟前',
                  avatar: '👤',
                },
              ].map((activity, index) => (
                <div key={index} class="flex items-center space-x-3">
                  <div class="w-8 h-8 bg-gray-200 rounded-full flex items-center justify-center text-sm">
                    {activity.avatar}
                  </div>
                  <div class="flex-1 min-w-0">
                    <p class="text-sm text-gray-900">
                      <span class="font-medium">{activity.user}</span>{' '}
                      {activity.action}
                    </p>
                    <p class="text-xs text-gray-500">{activity.time}</p>
                  </div>
                </div>
              ))}
            </div>
          </div>
        </div>
      </main>
    </div>
  );
};

export default Dashboard;

React + TailwindCSS 实战

// TaskBoard.tsx
import React, { useState, useCallback } from 'react';
import {
  DragDropContext,
  Droppable,
  Draggable,
  DropResult,
} from 'react-beautiful-dnd';

interface Task {
  id: string;
  title: string;
  description: string;
  priority: 'low' | 'medium' | 'high';
  assignee: string;
  dueDate: string;
  tags: string[];
}

interface Column {
  id: string;
  title: string;
  tasks: Task[];
}

const TaskBoard: React.FC = () => {
  const [columns, setColumns] = useState<Column[]>([
    {
      id: 'todo',
      title: '待办',
      tasks: [
        {
          id: '1',
          title: '设计用户界面',
          description: '完成登录页面的UI设计',
          priority: 'high',
          assignee: '张三',
          dueDate: '2024-01-20',
          tags: ['设计', 'UI'],
        },
        {
          id: '2',
          title: '编写API文档',
          description: '为用户管理模块编写详细的API文档',
          priority: 'medium',
          assignee: '李四',
          dueDate: '2024-01-22',
          tags: ['文档', 'API'],
        },
      ],
    },
    {
      id: 'inprogress',
      title: '进行中',
      tasks: [
        {
          id: '3',
          title: '实现用户认证',
          description: '开发JWT认证系统',
          priority: 'high',
          assignee: '王五',
          dueDate: '2024-01-18',
          tags: ['开发', '认证'],
        },
      ],
    },
    {
      id: 'done',
      title: '已完成',
      tasks: [
        {
          id: '4',
          title: '项目初始化',
          description: '创建项目结构和基础配置',
          priority: 'medium',
          assignee: '赵六',
          dueDate: '2024-01-15',
          tags: ['配置', '初始化'],
        },
      ],
    },
  ]);

  const [isModalOpen, setIsModalOpen] = useState(false);
  const [selectedColumn, setSelectedColumn] = useState<string>('');

  const onDragEnd = useCallback(
    (result: DropResult) => {
      const { destination, source, draggableId } = result;

      if (!destination) return;
      if (
        destination.droppableId === source.droppableId &&
        destination.index === source.index
      )
        return;

      const sourceColumn = columns.find(
        (col) => col.id === source.droppableId
      )!;
      const destinationColumn = columns.find(
        (col) => col.id === destination.droppableId
      )!;
      const task = sourceColumn.tasks.find((task) => task.id === draggableId)!;

      // 从源列移除任务
      const newSourceTasks = [...sourceColumn.tasks];
      newSourceTasks.splice(source.index, 1);

      // 添加任务到目标列
      const newDestinationTasks = [...destinationColumn.tasks];
      newDestinationTasks.splice(destination.index, 0, task);

      setColumns((prevColumns) =>
        prevColumns.map((col) => {
          if (col.id === source.droppableId) {
            return { ...col, tasks: newSourceTasks };
          }
          if (col.id === destination.droppableId) {
            return { ...col, tasks: newDestinationTasks };
          }
          return col;
        })
      );
    },
    [columns]
  );

  const getPriorityColor = (priority: string) => {
    switch (priority) {
      case 'high':
        return 'bg-red-100 text-red-800';
      case 'medium':
        return 'bg-yellow-100 text-yellow-800';
      case 'low':
        return 'bg-green-100 text-green-800';
      default:
        return 'bg-gray-100 text-gray-800';
    }
  };

  const getPriorityText = (priority: string) => {
    switch (priority) {
      case 'high':
        return '高优先级';
      case 'medium':
        return '中优先级';
      case 'low':
        return '低优先级';
      default:
        return '未知';
    }
  };

  const openModal = (columnId: string) => {
    setSelectedColumn(columnId);
    setIsModalOpen(true);
  };

  return (
    <div className="min-h-screen bg-gray-100">
      {/* 头部 */}
      <header className="bg-white shadow-sm border-b">
        <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
          <div className="flex justify-between items-center py-4">
            <div>
              <h1 className="text-2xl font-bold text-gray-900">任务看板</h1>
              <p className="text-gray-600">管理您的项目任务</p>
            </div>
            <div className="flex items-center space-x-3">
              <button className="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
                <svg
                  className="w-4 h-4 mr-2"
                  fill="none"
                  stroke="currentColor"
                  viewBox="0 0 24 24"
                >
                  <path
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    strokeWidth={2}
                    d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.207A1 1 0 013 6.5V4z"
                  />
                </svg>
                筛选
              </button>
              <button className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700">
                <svg
                  className="w-4 h-4 mr-2"
                  fill="none"
                  stroke="currentColor"
                  viewBox="0 0 24 24"
                >
                  <path
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    strokeWidth={2}
                    d="M12 6v6m0 0v6m0-6h6m-6 0H6"
                  />
                </svg>
                新建任务
              </button>
            </div>
          </div>
        </div>
      </header>

      {/* 看板主体 */}
      <main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
        <DragDropContext onDragEnd={onDragEnd}>
          <div className="flex space-x-6 overflow-x-auto pb-4">
            {columns.map((column) => (
              <div key={column.id} className="flex-shrink-0 w-80">
                {/* 列头 */}
                <div className="bg-white rounded-lg shadow-sm mb-4">
                  <div className="flex items-center justify-between p-4 border-b">
                    <div className="flex items-center">
                      <h3 className="font-semibold text-gray-900">
                        {column.title}
                      </h3>
                      <span className="ml-2 px-2 py-1 text-xs font-medium bg-gray-100 text-gray-800 rounded-full">
                        {column.tasks.length}
                      </span>
                    </div>
                    <button
                      onClick={() => openModal(column.id)}
                      className="text-gray-400 hover:text-gray-600"
                    >
                      <svg
                        className="w-5 h-5"
                        fill="none"
                        stroke="currentColor"
                        viewBox="0 0 24 24"
                      >
                        <path
                          strokeLinecap="round"
                          strokeLinejoin="round"
                          strokeWidth={2}
                          d="M12 6v6m0 0v6m0-6h6m-6 0H6"
                        />
                      </svg>
                    </button>
                  </div>
                </div>

                {/* 任务列表 */}
                <Droppable droppableId={column.id}>
                  {(provided, snapshot) => (
                    <div
                      ref={provided.innerRef}
                      {...provided.droppableProps}
                      className={`min-h-32 ${
                        snapshot.isDraggingOver ? 'bg-blue-50 rounded-lg' : ''
                      }`}
                    >
                      <div className="space-y-3">
                        {column.tasks.map((task, index) => (
                          <Draggable
                            key={task.id}
                            draggableId={task.id}
                            index={index}
                          >
                            {(provided, snapshot) => (
                              <div
                                ref={provided.innerRef}
                                {...provided.draggableProps}
                                {...provided.dragHandleProps}
                                className={`bg-white rounded-lg shadow-sm border p-4 cursor-move hover:shadow-md transition-shadow ${
                                  snapshot.isDragging ? 'shadow-lg' : ''
                                }`}
                              >
                                {/* 任务优先级和标签 */}
                                <div className="flex items-center justify-between mb-2">
                                  <span
                                    className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getPriorityColor(
                                      task.priority
                                    )}`}
                                  >
                                    {getPriorityText(task.priority)}
                                  </span>
                                  <button className="text-gray-400 hover:text-gray-600">
                                    <svg
                                      className="w-4 h-4"
                                      fill="none"
                                      stroke="currentColor"
                                      viewBox="0 0 24 24"
                                    >
                                      <path
                                        strokeLinecap="round"
                                        strokeLinejoin="round"
                                        strokeWidth={2}
                                        d="M5 12h.01M12 12h.01M19 12h.01M6 12a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0z"
                                      />
                                    </svg>
                                  </button>
                                </div>

                                {/* 任务标题和描述 */}
                                <h4 className="font-medium text-gray-900 mb-2 line-clamp-2">
                                  {task.title}
                                </h4>
                                <p className="text-sm text-gray-600 mb-3 line-clamp-2">
                                  {task.description}
                                </p>

                                {/* 标签 */}
                                <div className="flex flex-wrap gap-1 mb-3">
                                  {task.tags.map((tag, tagIndex) => (
                                    <span
                                      key={tagIndex}
                                      className="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-blue-100 text-blue-800"
                                    >
                                      {tag}
                                    </span>
                                  ))}
                                </div>

                                {/* 分配者和截止日期 */}
                                <div className="flex items-center justify-between text-sm text-gray-500">
                                  <div className="flex items-center">
                                    <div className="w-6 h-6 bg-gray-300 rounded-full flex items-center justify-center text-xs font-medium text-gray-700 mr-2">
                                      {task.assignee.charAt(0)}
                                    </div>
                                    <span>{task.assignee}</span>
                                  </div>
                                  <div className="flex items-center">
                                    <svg
                                      className="w-4 h-4 mr-1"
                                      fill="none"
                                      stroke="currentColor"
                                      viewBox="0 0 24 24"
                                    >
                                      <path
                                        strokeLinecap="round"
                                        strokeLinejoin="round"
                                        strokeWidth={2}
                                        d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
                                      />
                                    </svg>
                                    <span>{task.dueDate}</span>
                                  </div>
                                </div>
                              </div>
                            )}
                          </Draggable>
                        ))}
                      </div>
                      {provided.placeholder}
                    </div>
                  )}
                </Droppable>
              </div>
            ))}
          </div>
        </DragDropContext>
      </main>

      {/* 模态框 - 新建任务 */}
      {isModalOpen && (
        <div className="fixed inset-0 z-50 overflow-y-auto">
          <div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
            <div
              className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
              onClick={() => setIsModalOpen(false)}
            ></div>

            <div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
              <div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
                <div className="sm:flex sm:items-start">
                  <div className="mt-3 text-center sm:mt-0 sm:text-left w-full">
                    <h3 className="text-lg leading-6 font-medium text-gray-900 mb-4">
                      新建任务
                    </h3>

                    <form className="space-y-4">
                      <div>
                        <label className="block text-sm font-medium text-gray-700 mb-1">
                          任务标题
                        </label>
                        <input
                          type="text"
                          className="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
                          placeholder="请输入任务标题"
                        />
                      </div>

                      <div>
                        <label className="block text-sm font-medium text-gray-700 mb-1">
                          任务描述
                        </label>
                        <textarea
                          rows={3}
                          className="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
                          placeholder="请输入任务描述"
                        />
                      </div>

                      <div className="grid grid-cols-2 gap-4">
                        <div>
                          <label className="block text-sm font-medium text-gray-700 mb-1">
                            优先级
                          </label>
                          <select className="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
                            <option value="low">低优先级</option>
                            <option value="medium">中优先级</option>
                            <option value="high">高优先级</option>
                          </select>
                        </div>

                        <div>
                          <label className="block text-sm font-medium text-gray-700 mb-1">
                            分配给
                          </label>
                          <select className="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
                            <option value="">请选择</option>
                            <option value="张三">张三</option>
                            <option value="李四">李四</option>
                            <option value="王五">王五</option>
                            <option value="赵六">赵六</option>
                          </select>
                        </div>
                      </div>

                      <div>
                        <label className="block text-sm font-medium text-gray-700 mb-1">
                          截止日期
                        </label>
                        <input
                          type="date"
                          className="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
                        />
                      </div>
                    </form>
                  </div>
                </div>
              </div>

              <div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
                <button
                  type="button"
                  className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm"
                >
                  创建任务
                </button>
                <button
                  type="button"
                  onClick={() => setIsModalOpen(false)}
                  className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
                >
                  取消
                </button>
              </div>
            </div>
          </div>
        </div>
      )}
    </div>
  );
};

export default TaskBoard;

三者对比与选择建议 {#comparison}

功能特性对比

特性PostCSSUnoCSSTailwindCSS
类型CSS 处理器原子化 CSS 引擎CSS 框架
学习曲线中等较低较低
包大小取决于插件极小中等
定制性极高
生态系统丰富新兴成熟
构建速度中等极快
IDE 支持良好优秀

性能对比

# 构建时间对比 (示例项目)
PostCSS: ~2.5s
UnoCSS:  ~0.8s
TailwindCSS: ~1.2s

# 最终CSS大小对比
PostCSS: 取决于具体使用
UnoCSS:  12KB (仅包含使用的类)
TailwindCSS: 25KB (purge后)

使用场景建议

选择 PostCSS 的情况:

  • 需要高度定制的 CSS 处理流程
  • 已有 CSS 代码库需要渐进式改造
  • 需要使用特定的 CSS 插件生态
  • 团队有丰富的 CSS 经验

选择 UnoCSS 的情况:

  • 追求极致的构建性能
  • 喜欢函数式编程思维
  • 需要高度的定制能力
  • 项目使用 Vue.js 生态

选择 TailwindCSS 的情况:

  • 团队刚接触原子化 CSS
  • 需要成熟的生态系统和工具链
  • 重视文档和社区支持
  • 快速原型开发

最佳实践建议

  1. 小型项目:推荐 UnoCSS,快速轻量
  2. 中大型项目:推荐 TailwindCSS,生态成熟
  3. 定制需求高:推荐 PostCSS + 自定义插件
  4. 性能敏感:推荐 UnoCSS,构建最快
  5. 团队协作:推荐 TailwindCSS,学习成本低

混合使用策略

// 可以组合使用,比如:
// Vite + UnoCSS + PostCSS
export default defineConfig({
  plugins: [UnoCSS()],
  css: {
    postcss: {
      plugins: [autoprefixer(), cssnano()],
    },
  },
});

总结

PostCSS、UnoCSS 和 TailwindCSS 各有其优势和适用场景:

  • PostCSS 是 CSS 处理的瑞士军刀,适合需要高度定制的场景
  • UnoCSS 是新一代的原子化 CSS 引擎,性能卓越且灵活性强
  • TailwindCSS 是成熟稳定的原子化 CSS 框架,生态系统完善

选择哪个工具主要取决于项目需求、团队经验和性能要求。在实际项目中,也可以根据需要组合使用这些工具,发挥各自的优势。

无论选择哪种工具,掌握其核心概念和最佳实践都能显著提升开发效率和代码质量。希望这篇指南能帮助您更好地理解和应用这些现代 CSS 工具链!