Back to Lobehub

createStaticStyles 迁移指南

.cursor/docs/createStaticStyles_migration_guide.md

2.1.5621.8 KB
Original Source

createStaticStyles 迁移指南

📖 概述

createStaticStylesantd-style 提供的静态样式创建函数,相比 createStyles(hook 方案)具有零运行时开销的优势。样式在模块加载时计算一次,而不是每次组件渲染时计算。

🎯 适用场景

✅ 可以优化的场景

  1. 纯静态样式:不依赖运行时动态值
  2. 使用标准 token:所有 token 都在 cssVar.json 中有对应项
  3. 简单的条件逻辑:可以通过静态样式拆分处理

❌ 无法优化的场景

  1. JS 计算函数readableColor(), chroma(), mix(), calc() 中使用 token 数值
  2. 复杂的动态 props:需要运行时计算的复杂逻辑
  3. 动态 prefixCls:需要运行时传入的类名前缀(但可以硬编码为 'ant'

🔄 基本转换步骤

1. 样式文件转换

之前(createStyles):

typescript
import { createStyles } from 'antd-style';

export const useStyles = createStyles(({ css, token }) => {
  return {
    root: css`
      color: ${token.colorText};
      font-size: ${token.fontSize}px;
    `,
  };
});

之后(createStaticStyles):

typescript
import { createStaticStyles } from 'antd-style';

export const styles = createStaticStyles(({ css, cssVar }) => {
  return {
    root: css`
      color: ${cssVar.colorText};
      font-size: ${cssVar.fontSize};
    `,
  };
});

2. 组件文件转换

之前:

typescript
import { useStyles } from './style';

const Component = () => {
  const { styles, cx } = useStyles();
  return <div className={cx(styles.root, className)} />;
};

之后:

typescript
import { cx } from 'antd-style';
import { styles } from './style';

const Component = () => {
  return <div className={cx(styles.root, className)} />;
};

🛠️ 常见场景处理

场景 1: Token 转换

规则:

  • token.xxxcssVar.xxx
  • 注意:cssVar.fontSize 已经包含 px 单位,不需要再加 px

示例:

typescript
// ❌ 错误
font-size: ${cssVar.fontSize}px;  // cssVar.fontSize 已经是 "14px"

// ✅ 正确
font-size: ${cssVar.fontSize};     // 直接使用

特殊情况 - calc ():

typescript
// ❌ 错误
calc(${token.fontSize}px * 2.5)

// ✅ 正确
calc(${cssVar.fontSize} * 2.5)    // cssVar.fontSize 已经包含单位

场景 2: 动态 Props → CSS 变量

适用: 数值、字符串类型的 props

步骤:

  1. 在样式文件中使用 CSS 变量(带默认值)
  2. 在组件中通过 style prop 设置 CSS 变量

示例:

样式文件:

typescript
export const styles = createStaticStyles(({ css }) => {
  return {
    root: css`
      width: var(--component-size, 24px);
      height: var(--component-size, 24px);
    `,
  };
});

组件文件:

typescript
import { useMemo } from 'react';

const Component = ({ size = 24, style, ...rest }) => {
  const cssVariables = useMemo<Record<string, string>>(
    () => ({
      '--component-size': `${size}px`,
    }),
    [size],
  );

  return (
    <div
      className={styles.root}
      style={{
        ...cssVariables,
        ...style,
      }}
      {...rest}
    />
  );
};

已优化示例:

  • Video: maxHeight, maxWidth, minHeight, minWidth
  • ScrollShadow: size
  • MaskShadow: size
  • ColorSwatches: size
  • Grid: rows, maxItemWidth, gap
  • Layout: headerHeight
  • Footer: contentMaxWidth

场景 3: 布尔值 Props → 静态样式拆分

适用: 简单的布尔值 props(2-3 个)

步骤:

  1. 创建所有可能的组合样式
  2. 运行时使用 cx 组合

示例:

样式文件:

typescript
export const styles = createStaticStyles(({ css }) => {
  return {
    root: css`
      /* base styles */
    `,
    root_closable_true: css`
      /* closable styles */
    `,
    root_closable_false: css`
      /* no closable styles */
    `,
    root_hasTitle_true: css`
      /* has title styles */
    `,
    root_hasTitle_false: css`
      /* no title styles */
    `,
  };
});

组件文件:

typescript
const Component = ({ closable, hasTitle }) => {
  const className = cx(
    styles.root,
    styles[`root_closable_${!!closable}`],
    styles[`root_hasTitle_${!!hasTitle}`],
  );
  return <div className={className} />;
};

已优化示例:

  • Alert: closable, hasTitle, showIcon → 8 个组合(2×2×2)
  • Image: alwaysShowActions → 2 个样式
  • StoryBook: noPadding → 2 个样式

场景 4: isDarkMode → 静态样式拆分

适用: 依赖 isDarkMode 的条件样式

有两种处理方式:

方式 A: 直接条件选择(简单场景)

步骤:

  1. 创建 DarkLight 两个静态样式
  2. 运行时根据 theme.isDarkMode 选择

示例:

样式文件:

typescript
export const styles = createStaticStyles(({ css, cssVar }) => {
  return {
    rootDark: css`
      background: ${cssVar.colorFillTertiary};
      color: ${cssVar.colorTextLightSolid};
    `,
    rootLight: css`
      background: ${cssVar.colorFillQuaternary};
      color: ${cssVar.colorText};
    `,
  };
});

组件文件:

typescript
import { useThemeMode } from 'antd-style';

const Component = () => {
  const { isDarkMode } = useThemeMode();
  return (
    <div
      className={cx(
        isDarkMode ? styles.rootDark : styles.rootLight
      )}
    />
  );
};

方式 B: 使用 cva 将 isDarkMode 作为 variant(推荐,适用于复杂场景)

步骤:

  1. 创建 DarkLight 两个静态样式
  2. cva 中将 isDarkMode 作为 variant prop
  3. 运行时直接传入 isDarkMode

示例:

样式文件:

typescript
import { createStaticStyles } from 'antd-style';
import { cva } from 'class-variance-authority';

export const styles = createStaticStyles(({ css, cssVar }) => {
  return {
    filledDark: css`
      background: ${cssVar.colorFillTertiary};
      color: ${cssVar.colorTextLightSolid};
    `,
    filledLight: css`
      background: ${cssVar.colorFillQuaternary};
      color: ${cssVar.colorText};
    `,
    outlined: css`
      border: 1px solid ${cssVar.colorBorder};
    `,
    root: css`
      /* base styles */
    `,
  };
});

export const variants = cva(styles.root, {
  defaultVariants: {
    isDarkMode: false,
    variant: 'filled',
  },
  variants: {
    isDarkMode: {
      false: null,
      true: null, // isDarkMode 本身不添加样式,通过 compoundVariants 组合
    },
    variant: {
      filled: null, // variant 本身不添加样式,通过 compoundVariants 组合
      outlined: styles.outlined,
    },
  },
  compoundVariants: [
    {
      class: styles.filledDark,
      isDarkMode: true,
      variant: 'filled',
    },
    {
      class: styles.filledLight,
      isDarkMode: false,
      variant: 'filled',
    },
  ],
});

组件文件:

typescript
import { useThemeMode } from 'antd-style';
import { variants } from './style';

const Component = ({ variant = 'filled' }) => {
  const { isDarkMode } = useThemeMode();
  return (
    <div
      className={variants({ isDarkMode, variant })}
    />
  );
};

优势:

  • ✅ 不需要 useMemo 动态创建 variants
  • ✅ 更符合 cva 的设计理念
  • ✅ 代码更简洁,性能更好
  • ✅ 类型安全,IDE 自动补全

已优化示例:

  • TypewriterEffect: textDark / textLight(方式 A)
  • Collapse: filledDark / filledLight(可优化为方式 B)
  • Hotkey: inverseThemeDark / inverseThemeLight(可优化为方式 B)
  • GuideCard: filledDark / filledLight(可优化为方式 B)
  • GradientButton: buttonDark / buttonLight(方式 A)

场景 5: responsive → 静态 responsive

适用: 使用响应式断点

步骤:

  1. 导入静态 responsive from antd-style
  2. 使用 responsive.sm 替代 responsive.mobile
  3. createStyles 参数中移除 responsive

示例:

之前:

typescript
import { createStyles } from 'antd-style';

export const useStyles = createStyles(({ css, responsive }) => ({
  root: css`
    ${responsive.mobile} {
      padding: 12px;
    }
  `,
}));

之后:

typescript
import { createStaticStyles } from 'antd-style';
import { responsive } from 'antd-style';

export const styles = createStaticStyles(({ css }) => ({
  root: css`
    ${responsive.sm} {
      padding: 12px;
    }
  `,
}));

注意:

  • responsive.mobileresponsive.sm
  • 静态 responsive 提供:xs, sm, md, lg, xl, xxl

已优化示例:

  • Header: responsive.mobileresponsive.sm
  • FormModal: responsive.mobileresponsive.sm
  • Hero: responsive.mobileresponsive.sm

场景 6: stylish → lobeStaticStylish

适用: 使用自定义 stylish 工具

步骤:

  1. 导入 lobeStaticStylish from @/styles
  2. 替换 stylish.xxxlobeStaticStylish.xxx

示例:

之前:

typescript
import { createStyles } from 'antd-style';

export const useStyles = createStyles(({ css, stylish }) => ({
  root: css`
    ${stylish.blur};
    ${stylish.variantFilled};
  `,
}));

之后:

typescript
import { createStaticStyles } from 'antd-style';

import { lobeStaticStylish } from '@/styles';

export const styles = createStaticStyles(({ css }) => ({
  root: css`
    ${lobeStaticStylish.blur};
    ${lobeStaticStylish.variantFilled};
  `,
}));

已优化示例:

  • Button: stylish.blurlobeStaticStylish.blur
  • Hero: stylish.gradientAnimationlobeStaticStylish.gradientAnimation

场景 7: prefixCls → 硬编码

适用: 使用动态 prefixCls 参数

步骤:

  1. 在文件顶部硬编码 const prefixCls = 'ant'
  2. createStyles 参数中移除 prefixCls

示例:

之前:

typescript
export const useStyles = createStyles(({ css }, prefixCls: string) => ({
  root: css`
    .${prefixCls}-button {
      /* styles */
    }
  `,
}));

之后:

typescript
const prefixCls = 'ant';

export const styles = createStaticStyles(({ css }) => ({
  root: css`
    .${prefixCls}-button {
      /* styles */
    }
  `,
}));

已优化示例:

  • Alert, Collapse, FormModal, Image, Burger, DraggablePanel, DraggableSideNav, Toc, ColorSwatches, EmojiPicker, Form, awesome/Features

场景 8: readableColor () → Token 替换

适用: 使用 readableColor() 计算对比色

规则:

  • readableColor(token.colorPrimary)cssVar.colorTextLightSolid(主色背景用白色文字)
  • readableColor(token.colorTextQuaternary)cssVar.colorText(浅色背景用深色文字)

示例:

之前:

typescript
import { readableColor } from 'polished';

export const useStyles = createStyles(({ css, token }) => ({
  checked: css`
    background-color: ${token.colorPrimary};
    color: ${readableColor(token.colorPrimary)};
  `,
}));

之后:

typescript
export const styles = createStaticStyles(({ css, cssVar }) => ({
  checked: css`
    background-color: ${cssVar.colorPrimary};
    color: ${cssVar.colorTextLightSolid};
  `,
}));

已优化示例:

  • Checkbox: readableColor(token.colorPrimary)cssVar.colorTextLightSolid

场景 9: rgba () → color-mix ()

适用: 使用 rgba() 设置透明度

步骤:

  1. 使用 CSS 原生的 color-mix() 函数
  2. 格式:color-mix(in srgb, ${cssVar.xxx} alpha%, transparent)

示例:

之前:

typescript
import { rgba } from 'polished';

export const useStyles = createStyles(({ css, token }) => ({
  root: css`
    background-color: ${rgba(token.colorBgLayout, 0.4)};
  `,
}));

之后:

typescript
export const styles = createStaticStyles(({ css, cssVar }) => ({
  root: css`
    background-color: color-mix(in srgb, ${cssVar.colorBgLayout} 40%, transparent);
  `,
}));

已优化示例:

  • Header: rgba(cssVar.colorBgLayout, 0.4)color-mix(...)
  • FormModal: rgba(cssVar.colorBgContainer, 0)color-mix(...)

场景 10: keyframes → css

适用: 使用 keyframes 创建动画

步骤:

  1. createStaticStyles 外部定义 keyframes
  2. 在样式内部使用

示例:

之前:

typescript
export const useStyles = createStyles(({ css, keyframes }) => {
  const spin = keyframes`
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
  `;
  return {
    icon: css`
      animation: ${spin} 1s linear infinite;
    `,
  };
});

之后:

typescript
import { keyframes } from 'antd-style';

const spin = keyframes`
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
`;

export const styles = createStaticStyles(({ css }) => ({
  icon: css`
    animation: ${spin} 1s linear infinite;
  `,
}));

已优化示例:

  • Icon: keyframes 动画
  • Skeleton: keyframes shimmer 动画

⚠️ 反模式:避免使用 createVariants (isDarkMode)

不推荐的做法:

typescript
// ❌ 不推荐:在组件中动态创建 variants
export const createVariants = (isDarkMode: boolean) =>
  cva(styles.root, {
    variants: {
      variant: {
        filled: isDarkMode ? styles.filledDark : styles.filledLight,
      },
    },
  });

// 组件中
const variants = useMemo(() => createVariants(isDarkMode), [isDarkMode]);

推荐的做法:

isDarkMode 作为 cva 的 variant prop(见场景 4 方式 B),这样:

  • ✅ 不需要 useMemo 动态创建
  • ✅ 更符合 cva 的设计理念
  • ✅ 代码更简洁,性能更好
  • ✅ 类型安全,IDE 自动补全
typescript
// ✅ 推荐:将 isDarkMode 作为 variant prop
export const variants = cva(styles.root, {
  variants: {
    isDarkMode: {
      false: null,
      true: null,
    },
    variant: {
      filled: null,
    },
  },
  compoundVariants: [
    {
      class: styles.filledDark,
      isDarkMode: true,
      variant: 'filled',
    },
    {
      class: styles.filledLight,
      isDarkMode: false,
      variant: 'filled',
    },
  ],
});

// 组件中
const { isDarkMode } = useThemeMode();
const className = variants({ isDarkMode, variant: 'filled' });

⚠️ 无法优化的场景

1. JS 计算函数

无法优化:

  • chroma() - 颜色计算库
  • readableColor() - 需要运行时计算(但可以用 token 替代)
  • mix() - 颜色混合计算
  • calc() 中使用 token 数值进行复杂计算

示例:

typescript
// ❌ 无法优化
const scale = chroma.bezier([token.colorText, backgroundColor]).scale().colors(6);

2. 复杂的动态 Props

无法优化:

  • 需要复杂计算的 props
  • 对象 / 数组类型的 props
  • 函数类型的 props

3. useTheme Hook

无法优化:

  • 直接使用 useTheme() hook 获取运行时值
  • 例如:awesome/Giscus/style.ts 使用 useTheme() 获取主题值

📋 迁移检查清单

样式文件检查

  • createStylescreateStaticStyles
  • token.xxxcssVar.xxx
  • 移除 px 后缀(cssVar 已包含单位)
  • responsive.mobileresponsive.sm(如果使用)
  • stylish.xxxlobeStaticStylish.xxx(如果使用)
  • rgba()color-mix()(如果使用)
  • readableColor() → token 替换(如果使用)
  • prefixCls 参数 → 硬编码 const prefixCls = 'ant'(如果使用)
  • isDarkMode → 静态样式拆分(如果使用)
  • 动态 props → CSS 变量(如果使用)

组件文件检查

  • useStyles()import { styles } from './style'
  • import { cx } from 'antd-style'(如果需要)
  • import { useTheme } from 'antd-style'(如果需要 theme.isDarkMode
  • 动态 props → CSS 变量设置(如果使用)
  • isDarkMode 条件 → theme.isDarkMode 判断(如果使用)

🎯 优化优先级

高优先级(简单优化)

  1. ✅ 纯静态样式(无动态 props)
  2. isDarkMode 拆分
  3. responsive.mobileresponsive.sm
  4. stylishlobeStaticStylish
  5. readableColor() → token 替换

中优先级(需要转换)

  1. ✅ 简单的动态 props → CSS 变量(1-2 个)
  2. ✅ 布尔值 props → 静态样式拆分(2-3 个)

低优先级(复杂优化)

  1. ⚠️ 多个动态 props → CSS 变量(3+ 个)
  2. ⚠️ 复杂的条件逻辑拆分

📚 参考示例

完整示例 1: 简单组件

样式文件:

typescript
import { createStaticStyles } from 'antd-style';

export const styles = createStaticStyles(({ css, cssVar }) => ({
  root: css`
    padding: ${cssVar.padding};
    color: ${cssVar.colorText};
    border-radius: ${cssVar.borderRadius};
  `,
}));

组件文件:

typescript
import { cx } from 'antd-style';
import { styles } from './style';

const Component = ({ className }) => {
  return <div className={cx(styles.root, className)} />;
};

完整示例 2: 带动态 Props

样式文件:

typescript
import { createStaticStyles } from 'antd-style';

export const styles = createStaticStyles(({ css, cssVar }) => ({
  root: css`
    width: var(--component-size, 24px);
    height: var(--component-size, 24px);
    background: ${cssVar.colorBgContainer};
  `,
}));

组件文件:

typescript
import { cx } from 'antd-style';
import { useMemo } from 'react';
import { styles } from './style';

const Component = ({ size = 24, className, style, ...rest }) => {
  const cssVariables = useMemo<Record<string, string>>(
    () => ({
      '--component-size': `${size}px`,
    }),
    [size],
  );

  return (
    <div
      className={cx(styles.root, className)}
      style={{
        ...cssVariables,
        ...style,
      }}
      {...rest}
    />
  );
};

完整示例 3: 带 isDarkMode

样式文件:

typescript
import { createStaticStyles } from 'antd-style';

export const styles = createStaticStyles(({ css, cssVar }) => ({
  rootDark: css`
    background: ${cssVar.colorFillTertiary};
    color: ${cssVar.colorTextLightSolid};
  `,
  rootLight: css`
    background: ${cssVar.colorFillQuaternary};
    color: ${cssVar.colorText};
  `,
}));

组件文件:

typescript
import { cx, useTheme } from 'antd-style';
import { styles } from './style';

const Component = ({ className }) => {
  const { theme } = useTheme();
  return (
    <div
      className={cx(
        theme.isDarkMode ? styles.rootDark : styles.rootLight,
        className
      )}
    />
  );
};

🔍 验证步骤

  1. 类型检查: pnpm run type-check
  2. 运行时测试: 确保视觉效果一致
  3. 性能验证: 检查样式计算是否在模块加载时完成

📊 优化效果

  • 零运行时开销:样式在模块加载时计算一次
  • 减少重新渲染:组件不再依赖样式 hook
  • 更好的性能:减少每次渲染的计算开销
  • 代码更简洁:直接导入样式对象

🔧 场景 11: useTheme () → useThemeMode () /cssVar

适用: 组件中只使用 theme.isDarkMode 或其他 token 值

规则:

  • 如果只使用 theme.isDarkMode,使用 const { isDarkMode } = useThemeMode() 替代
  • 如果使用其他 token(如 theme.colorText, theme.borderRadius 等),使用 cssVar 替代
  • useThemeMode()useTheme() 更轻量,只返回 isDarkMode

示例:

之前:

typescript
import { useTheme } from 'antd-style';

const Component = () => {
  const theme = useTheme();
  return (
    <div className={theme.isDarkMode ? styles.dark : styles.light}>
      {theme.colorText}
    </div>
  );
};

之后:

typescript
import { cssVar, useThemeMode } from 'antd-style';

const Component = () => {
  const { isDarkMode } = useThemeMode();
  return (
    <div className={isDarkMode ? styles.dark : styles.light}>
      {cssVar.colorText}
    </div>
  );
};

已优化示例:

  • AuroraBackground, Select, Input, Button, DatePicker, AutoComplete, InputNumber, InputPassword, InputOPT, TextArea, SpotlightCardItem, Spotlight, HotkeyInput - 只使用 isDarkModeuseThemeMode()
  • Image, GradientButton, Empty, FileTypeIcon, FormSubmitFooter, CodeEditor, LobeChat, Drawer, Modal, Avatar, AvatarGroup, SkeletonAvatar, SkeletonButton, SkeletonTags, Callout, LobeHub, GridBackground, FolderIcon, FileIcon, TokenTag, ChatSendButton, AvatarUploader - 使用 token → cssVar

无法优化的文件(需要保留 useTheme()):

  • useMermaid, useStreamMermaid, useHighlight, useStreamHighlight - 需要完整的 theme 对象传给第三方库
  • Alert, Tag, Menu, EmojiPicker - 需要实际颜色值传给颜色计算函数
  • SkeletonTitle, SkeletonTags - 需要数值进行数学运算
  • GridShowcase, GridBackground/demos - 需要实际颜色值传给 rgba() 函数
  • CustomFonts - 需要实际字符串值进行字符串拼接
  • Giscus/style.ts - 需要实际颜色值传给 readableColor()rgba() 函数(其他 token 已优化为 cssVar

注意事项:

  • useThemeMode() 只返回 { isDarkMode },不返回完整的 theme 对象
  • cssVar 的值是字符串(如 "14px", "#ffffff"),可以直接在 JSX 中使用
  • 如果 token 需要用于数值计算(如 Math.round(theme.fontSize * 1.5)),需要保留 useTheme()

🎉 总结

createStaticStyles 迁移是一个渐进式的优化过程。对于简单的静态样式,可以直接转换;对于复杂的动态场景,需要根据具体情况选择合适的优化策略。关键是要理解每种场景的处理方式,并灵活运用 CSS 变量、静态样式拆分等技术。

useTheme () 优化总结

  • 使用 useThemeMode():当组件只使用 theme.isDarkMode
  • 使用 cssVar:当组件使用其他 token 值(颜色、尺寸等)时
  • ⚠️ 保留 useTheme():当 token 需要用于数值计算或传给第三方库时