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

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

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

什么是 CSS 原子化?

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

CSS 原子化的核心原则

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
预编译] E --> G[UnoCSS
即时生成] E --> H[Windi CSS
按需编译]

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

typescript 复制代码
// 简化的原子化 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 的核心概念

javascript 复制代码
// 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 配置示例

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

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

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

typescript 复制代码
// 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 有着密切的技术关联:

typescript 复制代码
// 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 的结合

javascript 复制代码
// 自定义 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 插件:

javascript 复制代码
// 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 基本使用

typescript 复制代码
// 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 实际应用示例

html 复制代码
<!-- 使用原子化类名 -->
<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 配置

javascript 复制代码
// 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 实际应用

html 复制代码
<!-- 响应式导航栏 -->
<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 处理机制

javascript 复制代码
// 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 处理机制

typescript 复制代码
// 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 处理

javascript 复制代码
// 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 在不同构建工具中的集成

typescript 复制代码
// 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)

javascript 复制代码
// 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)

javascript 复制代码
// 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

javascript 复制代码
// 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

javascript 复制代码
// 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

javascript 复制代码
// 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

javascript 复制代码
// 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

javascript 复制代码
// 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 驱动的动态样式系统

typescript 复制代码
// 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 中的编译时优化

typescript 复制代码
// 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 的实现

typescript 复制代码
// 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 编译器集成

typescript 复制代码
// 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 服务驱动的样式管理

typescript 复制代码
// 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 编译器集成

typescript 复制代码
// 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 运行时

typescript 复制代码
// 通用原子化 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 实战

bash 复制代码
# 安装依赖
npm install -D unocss @unocss/preset-uno @unocss/preset-icons
typescript 复制代码
// 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',
  },
});
vue 复制代码
<!-- 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 实战

vue 复制代码
<!-- 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 实战

bash 复制代码
# 安装依赖
npm install -D unocss @unocss/preset-uno @unocss/preset-icons @unocss/vite
typescript 复制代码
// 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',
  },
});
tsx 复制代码
// 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 实战

tsx 复制代码
// 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}

功能特性对比

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

性能对比

bash 复制代码
# 构建时间对比 (示例项目)
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,学习成本低

混合使用策略

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

总结

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

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

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

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

相关推荐
菜市口的跳脚长颌2 小时前
Web3基础
前端
RoyLin2 小时前
TypeScript设计模式:代理模式
前端·后端·typescript
IT_陈寒3 小时前
Vue3性能优化实战:这5个技巧让我的应用加载速度提升了70%
前端·人工智能·后端
树上有只程序猿3 小时前
react 实现插槽slot功能
前端
stoneship4 小时前
Web项目减少资源加载失败白屏问题
前端
DaMu4 小时前
Cesium & Three.js 【移动端手游“户外大逃杀”】 还在“画页面的”前端开发小伙伴们,是时候该“在往前走一走”了!我们必须摆脱“画页面的”标签!
前端·gis
非专业程序员4 小时前
一文读懂Font文件
前端
Asort4 小时前
JavaScript 从零开始(七):函数编程入门——从定义到可重用代码的完整指南
前端·javascript