CSS 原子化概念深度解析 {#atomic-css}
在前端开发的演进历程中,CSS 工具链正经历着一场深刻的革命。从传统的手写样式到预处理器,再到如今的原子化 CSS 时代,开发者们不断寻求更高效、更可维护的样式解决方案。
什么是 CSS 原子化?
CSS 原子化(Atomic CSS)是一种 CSS 架构方法,其核心思想是将样式拆分为最小的、单一职责的原子类(Atomic Classes)。每个原子类只负责一个具体的样式属性,如 margin-top: 8px
对应 mt-2
,color: 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 的优势
- 高度复用性 - 原子类可以在整个项目中复用
- 样式一致性 - 强制使用设计系统中的预定义值
- 包体积优化 - 避免样式重复,CSS 体积可控
- 开发效率 - 快速组合样式,无需命名困扰
- 维护性强 - 样式变更影响范围可预测
原子化 CSS 的挑战
- 学习成本 - 需要记忆大量原子类名
- HTML 复杂度 - 类名可能会很长
- 设计约束 - 受限于预定义的设计系统
- 调试困难 - 样式分散在多个原子类中
原子化 CSS 的演进历程
预编译] 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 插件
- autoprefixer - 自动添加浏览器前缀
- postcss-preset-env - 使用现代 CSS 语法
- cssnano - CSS 压缩优化
- postcss-nested - 支持嵌套语法
- 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;
}
三者的技术架构关系
UnoCSS:即时原子化 CSS 引擎 {#unocss}
UnoCSS 简介
UnoCSS 是一个即时的原子化 CSS 引擎,由 Vue.js 团队核心成员 Anthony Fu 开发。它具有高性能、灵活性强、配置简单等特点。
UnoCSS 的核心特性
- 即时性 - 按需生成 CSS,只包含使用的样式
- 零运行时 - 构建时生成,无运行时开销
- 高度可定制 - 支持自定义规则、预设和变体
- 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 的优势
- 实用优先 - 提供低级别的实用类
- 响应式设计 - 内置响应式前缀
- 组件友好 - 易于提取组件
- 可定制性 - 高度可配置的设计系统
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
- 需要成熟的生态系统和工具链
- 重视文档和社区支持
- 快速原型开发
最佳实践建议
- 小型项目:推荐 UnoCSS,快速轻量
- 中大型项目:推荐 TailwindCSS,生态成熟
- 定制需求高:推荐 PostCSS + 自定义插件
- 性能敏感:推荐 UnoCSS,构建最快
- 团队协作:推荐 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 工具链!