学习antd v5源码系列之ConfigProvider&Button

1,644 阅读2分钟

antd现在已经发展到v5版本了,相比以前的旧版本,做了大幅度的调整,而自己在日常工作中也使用较多antd,但对其内部的具体实现却知之甚少,因此借v5版本,打算对其内部一探究竟。今天主要是来实现一个功能有限的按钮渲染,虽说是功能有限,但是api和代码结构都是高仿antd的实现,在此基础上理解和扩展就容易多了,先看下组件和效果吧:

Dingtalk_20230213135613.jpg

效果很一般,没什么说的,我想先讲讲关于theme的实现思路。

theme

v5和以前版本相比,主题的实现和定制发生了巨大的变化,其主要是采用css-in-js的方案进行了重构,具体介绍可以去官网了解。css-in-js的方案主要就是要将下面的对象转换成了style,这里提供了一个极简的demo,实现了对象到style的转换,demo比较好理解,这里就不贴代码了。

{
  'ant-btn': {
    color: token.colorPrimary,
    borderColor: token.colorPrimary,
    borderRadius: token.borderRadius,
  }
}
<style>.ant-btn{color:#1677ff;border-color:#1677ff;border-radius:6px;}</style>

ConfigProvider

ConfigProvider提供全局的配置上下文,通过React提供的Context机制将这些配置信息提供给内部子组件。ConfigProvider可以嵌套,因此还需要将上层的ConfigProvider配置合并,这样能达到配置信息作用范围的精确控制,可以想象我们的代码嵌套结构是这样的:

<ConfigContext.Provider> // 一个ConfigProvider对应一个ConfigContext.Provider
    // ...your components...
    <ConfigContext.Provider>
        <DesignTokenContext.Provider>  // 在ConfigProvider配置了theme就会包裹这一层
           // ...your components...
        </<DesignTokenContext.Provider>
    </ConfigContext.Provider>
</ConfigContext.Provider>

1.ConfigContext

前面提到,ConfigProvider组件背后是基于Context实现的,ConfigProvider对应的ConfigContext目前支持下面四种属性:

export interface ConfigConsumerProps {
  getPrefixCls: (suffixCls?: string, customizePrefixCls?: string) => string;
  autoInsertSpaceInButton?: boolean;
  direction?: DirectionType; // 'ltr' | 'rtl' | undefined
  theme?: ThemeConfig; // {colorPrimary?: string; borderRadius?: number;
}

2.DesignTokenContext

如果ConfigProvider定义了theme,那么就会在我们的组件外面包裹一层DesignTokenContext.Provider,因为我们的组件在生成样式时,需要就近获取组成theme的token,如果有DesignTokenContext,那么就从它的theme中获取,否则获取上层ConfigContext.Provider中的theme;

简单介绍了一下组件结构和功能之后,怎么实现呢,文章不想贴太多代码,文末的项目地址有详细的实现细节,这里想结合下面简化之后的demo简单说明一下实现原理;

const ConfigContext = React.createContext();

const DesignTokenContext = React.createContext({
    token: {
        colorPrimary: '#1677ff',
        borderRadius: 6,
    }
});

const ConfigProvider = (props) => {
    const parentContext = React.useContext(ConfigContext);
    const { theme, autoInsertSpaceInButton, children } = props;
    // 合并theme
    const mergedTheme: ThemeConfig = Object.assign(
        {},
        parentContext.theme || defaultConfig,
        theme
    );

    const config = {
        ...parentContext,
    };

    config.autoInsertSpaceInButton = autoInsertSpaceInButton;

    let childNode = children;
    // ConfigProvider配置了theme
    if (theme) {
        childNode = (
          <DesignTokenContext.Provider value={mergedTheme}>
            {childNode}
          </DesignTokenContext.Provider>
        );
    }
    return (
        <ConfigContext.Provider value={config}>{childNode}</ConfigContext.Provider>
    );
};

// 组件中获取ConfigContext和DesignTokenContext的value
const Button = (props) => {
    const { children } = props;
    const { autoInsertSpaceInButton } = React.useContext(ConfigContext);
    const { token } = React.useContext(DesignTokenContext);
    const { colorPrimary, borderRadius } = token;
    const style = {
      color: colorPrimary,
      borderRadius:
      typeof borderRadius === 'number' ? `${borderRadius}px` : borderRadius,
      border: '1px solid',
    };
    return <button style={style}>{children}</button>;
};

Button

通过前面的梳理,Button就比较简单了。这里着重说一下style标签的生成过程;

<style data-css-hash="irfgu3">
.ant-btn:where(.css-dev-only-do-not-override-194mq80) {
    border: 1px solid;
    background: #fff;
    color: #00b96b;
    border-color: #00b96b;
    border-radius:6px;
}
<style>
<style data-css-hash="1uqrxxf">
.ant-btn:where(.css-dev-only-do-not-override-1b96b5m) {
    border:1px solid;
    background: #fff;
    color: #1677ff;
    border-color: #1677ff;
    border-radius:6px;
}
<style>
......
<button class="ant-btn css-dev-only-do-not-override-1b96b5m">...</button>
<button class="ant-btn css-dev-only-do-not-override-194mq80">...</button>

class包含唯一的hash类名,hash值的生成流程如下:

1.根据token生成组件的样式对象

{
     'ant-btn':  {
         border: '1px solid',
         background: '#fff',
         color: token.colorPrimary,
         borderColor: token.colorPrimary,
         borderRadius: token.borderRadius,
      }
}

2.token对象转换成css字符串

.ant-btn:where(.css-dev-only-do-not-override-1b96b5m){border:1px solid;background:#fff;color:#1677ff;border-color:#1677ff;border-radius:6px;}

3.css字符串计算hash

antd是通过@emotion/hash这个库生成的hash,在开发环境下最终拼接成css-dev-only-do-not-override-${hashId},然后就是插入style了,其中有很多细节没有在文章中体现,如有兴趣可查看github地址。

最后

项目地址:github.com/lanpangzi-z…