Skip to content

Ant Design Select 的样式组合流程 #72

@inkjuncom

Description

@inkjuncom

仓库, 里面用到的 genStyleUtils 来自 cssinjs-utils

https://github.com/ant-design/cssinjs-utils

背景知识

在阅读代码之前,你需要理解 Ant Design v5 相比 v4 的几个关键架构转变:

  1. CSS-in-JS (Runtime): 不再使用 Less 编译,而是在运行时动态生成 CSS。这使得动态切换主题(Dark Mode、Compact Mode)无需刷新页面。

https://ant.design/docs/react/customize-theme-cn#%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5

  1. Design Token System (设计变量系统)
  • Seed Token: 基础变量(如 blue-6)
  • Map Token: 映射变量(如 colorPrimary)
  • Alias Token: 别名变量(如 colorLink)

  1. 按需加载: 只有当组件渲染时,对应的 <style> 标签才会被插入到 <head> 中。

genStyleUtils

  • 输入: 全局的配置上下文(Hooks、工具函数)。
  • 输出: 一组用于生成组件样式的工厂函数 (genStyleHooks, genComponentStyleHook)。

它的主要职责是:

  • 依赖注入: 将获取 Token、前缀、CSP 配置的能力注入进来,解耦了底层逻辑。
  • Token 处理: 自动合并 Global Token 和 Component Token,并处理 CSS 变量(CSS Variables)。
  • 样式注册: 封装 useStyleRegister,处理 Hash 计算(用于样式隔离)和 SSR 支持。
  • 统一标准化: 确保所有组件(Button, Table 等)使用相同的逻辑生成样式,保证一致性。

config 配置项详解

| 配置项 | 类型 | 作用详解 |
| usePrefix | Hook | 获取类名前缀。
默认是 ant。如果用户配置了 ConfigProvider 修改前缀(例如改为 my-app),这个 Hook 会确保生成的样式类名变为 .my-app-btn。 |
| useToken | Hook | 获取当前的主题 Token。
这是最关键的部分。它从 Context 中拿到当前的 Design Token(颜色、圆角、间距等),并计算出 hashId(用于样式隔离的唯一标识)。|
| useCSP | Hook | 内容安全策略 (Content Security Policy)。
用于获取 nonce 属性。在严格的安全环境下,动态插入的 <style> 标签必须包含合法的 nonce 才能执行。 |
| getResetStyles | Function | 生成重置样式 (Reset CSS)。
类似于 normalize.css。当组件首次加载时,确保基础标签(如 body, p, ul)的样式被统一重置,消除浏览器差异。|
| getCommonStyle | Function | 生成通用样式。
生成所有组件共用的样式,例如基础字体 (font-family)、盒模型 (box-sizing) 等。通常会混入到每个组件的样式中。|
| getCompUnitless | Function | 定义无单位 Token。
告诉系统哪些组件 Token 不需要自动添加 px 后缀。例如 zIndex, opacity, fontWeight 是数字,不应该变成 100px。 |
| layer | Object | CSS Layer (级联层)。
用于控制样式优先级。Ant Design 默认将样式放入 antd 层,防止被用户自定义的全局 CSS 覆盖。 |

返回值解析 (生成的工具)

  1. genStyleHooks` (最常用)

这是给组件(如 Button)使用的最终入口。

  • 用法: export default genStyleHooks('Button', styleFn, defaultToken);
  • 作用: 它返回一个自定义 Hook(例如 useStyle)。当你在组件里写 const [wrapSSR, hashId] = useStyle(prefixCls) 时,它会自动计算样式、生成 CSS 变量,并插入 DOM。
  1. genComponentStyleHook

这是 genStyleHooks 的底层实现。

  • 作用: 直接调用 @ant-design/cssinjsuseStyleRegister
  • 逻辑:
    1. 获取 Token。
    2. 计算 Hash。
    3. 合并 Component Token。
    4. 执行组件的样式生成函数 (styleFn)。
    5. 将生成的 CSS 对象转为 CSS 字符串并缓存。
  1. genSubStyleComponent

用于生成子组件样式。

  • 场景: 某些复杂的组件(如 Table 的筛选下拉框、DatePicker 的弹窗)可能需要独立的样式逻辑,或者为了拆分代码体积。
  • 作用: 创建一个不渲染 DOM 节点,只负责注入样式的 React 组件。

从用户配置到最终 CSS 的完整流程

flowchart TD
    %% 1. 配置阶段
    subgraph Config [1. 配置阶段]
        direction TB
        UserConfig["ConfigProvider theme={...}"]
        UserValue["activeShadow: '0 0 0 4px red'"]
        UserConfig --> UserValue
    end

    %% 2. 运行时合并阶段
    subgraph Runtime [2. 运行时合并阶段]
        direction TB
        GlobalToken["Global Token (Blue Theme)"]
        DefaultLogic["prepareComponentToken()"]
        DefaultValue["默认值: '0 0 0 2px blue'"]
        
        MergeEngine{"样式引擎合并"}
        
        GlobalToken --> DefaultLogic
        DefaultLogic --> DefaultValue
        
        UserValue --> MergeEngine
        DefaultValue --> MergeEngine
        
        FinalToken["最终 Token: '0 0 0 4px red'"]
        MergeEngine -->|覆盖默认值| FinalToken
    end

    %% 3. 样式生成阶段
    subgraph Generation [3. 样式生成阶段]
        direction TB
        GenStyleFn["genSelectInputStyle(token)"]
        CSSObject["CSS Object: { boxShadow: '...red' }"]
        
        FinalToken --> GenStyleFn
        GenStyleFn --> CSSObject
    end

    %% 4. CSS 注入阶段
    subgraph Injection [4. CSS 注入阶段 CSS Injection]
        direction TB
        Engine["CSS-in-JS Engine"]
        DOMStyle["<style> .ant-select-focused { box-shadow: ...red } </style>"]
        
        CSSObject --> Engine
        Engine -->|插入 DOM| DOMStyle
    end

    Config --> Runtime
    Runtime --> Generation
    Generation --> Injection
Loading

以 activeShadow 为例,追踪它从用户配置到最终 CSS 的完整旅程。

1. 配置阶段

地点: components/select/demo/basic.tsx

用户通过 ConfigProvider 注入自定义值。

<ConfigProvider
  theme={{
    components: {
      Select: {
        // 1. 用户定义:我要红色的阴影
        activeShadow: '0 0 0 4px red', 
      }
    }
  }}
>
  <Select />
</ConfigProvider>

2. 运行时合并阶段

地点: node_modules/@ant-design/cssinjs-utils/lib/util/genStyleUtils.d.ts (实际运行在 useStyleRegister 内部)

<Select /> 组件渲染时,useStyle Hook 被触发。Ant Design 的样式引擎开始工作:

  1. 获取全局 Token: 从 Context 中拿到基础 Token(如 colorPrimary 等)。
  2. 计算默认组件 Token: 调用 prepareComponentToken(globalToken)
    • 此时计算出的默认值是:0 0 0 2px blue (假设是默认蓝色主题)。
  3. 合并用户配置: 样式引擎发现 ConfigProvider 中有 Select.activeShadow
    • 覆盖发生: 用户配置的 '0 0 0 4px red' 覆盖 了默认的 '0 0 0 2px blue'
  4. 生成最终 Token: 此时 token.activeShadow 的值变成了 '0 0 0 4px red'

3. 样式生成阶段 (Style Generation)

地点: components/select/style/select-input.ts

样式生成函数 genSelectInputStyle 接收到了合并后的 Token。

// token.activeShadow 此时已经是 '0 0 0 4px red' 了

genSelectInputVariantStyle(
  token,
  'outlined',
  {
    // ...
    // 4. 传递 Token:将 '0 0 0 4px red' 传给 colors.shadow
    shadow: token.activeShadow, 
  }
)

接着进入 genSelectInputVariableStyle

const genSelectInputVariableStyle = (token, colors) => {
  return {
    // ...
    [`&${componentCls}-focused`]: {
      // 5. 生成 CSS 对象
      // colors.shadow 是 '0 0 0 4px red'
      boxShadow: colors.shadow, 
    },
  };
};

4. CSS 注入阶段 (CSS Injection)

地点: 浏览器 DOM

最终,Ant Design 的 CSS-in-JS 引擎将上述 CSS 对象转换为 CSS 字符串,并插入到 <style> 标签中。

/* 6. 最终生成的 CSS */
.ant-select-focused {
  box-shadow: 0 0 0 4px red;
}

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions