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