前言
22年年底antd版本升级到了5.0,与4.0相比除了多了几个组件外,最大的变化就是为了更好地支持动态主题,弃用了less,改成CSS-in-JS方案,底层使用 @ant-design/cssinjs 作为解决方案。
原理分析
css-in-js一直有性能消耗的问题,因为编写代码时写的并不是最终的css,所以每次渲染都要重新序列化得到css再计算hash值;而antd就是通过组件级别的缓存来达到,在hash相同的情况下,同一组件无论渲染多少次,样式都是第一次mount时生成,剩下的时间都会命中缓存,从而大幅度减少性能消耗。
从antd5的任一个component中都能看到下面这段代码,useStyle返回wrapSSR和hashId,hashId被用于classNames的序列化样式属性中,然后组件的返回值是wrapSSR函数里面传一个React的元素
const [wrapSSR, hashId] = useStyle(prefixCls);
// ....
const classes = classNames(
prefixCls,
hashId,
...
);
// ...
return wrapSSR(<div className={classes}>...</div>);
然后每个组件的style文件中会有一个genComponentStyleHook函数,它有2个参数,第一个是组件的标识,第二个是token样式配置的回调函数,可以很明显的看到回调里面是在基于token生成各种样式
export default genComponentStyleHook('Button', (token) => {
const { controlTmpOutline, paddingContentHorizontal } = token;
const buttonToken = mergeToken<ButtonToken>(token, {
colorOutlineDefault: controlTmpOutline,
buttonPaddingHorizontal: paddingContentHorizontal,
});
return [
// Shared
genSharedButtonStyle(buttonToken),
// Size
genSizeSmallButtonStyle(buttonToken),
genSizeBaseButtonStyle(buttonToken),
genSizeLargeButtonStyle(buttonToken),
// Block
genBlockButtonStyle(buttonToken),
// Group (type, ghost, danger, disabled, loading)
genTypeButtonStyle(buttonToken),
// Button Group
genGroupStyle(buttonToken),
// Space Compact
genCompactItemStyle(token, { focus: false }),
genCompactItemVerticalStyle(token),
];
});
在看genComponentStyleHook之前先说一下这个token,token是一个组件样式的变量。antd5推出了全新的Design Token模型,将 Design Token 拆解为 Seed Token、Map Token 和 Alias Token 三部分。这三组 Token 并不是简单的分组,而是一个三层的派生关系,由 Seed Token 派生 Map Token,再由 Map Token 派生 Alias Token。
简单来说就是Seed Token是一个零件,它通过算法组装成一个个的Map Token,然后Alias Token是一个复用Map Token的别名零件,最终形成一个组件的样式。
然后看一下genComponentStyleHook这个函数,它最终是通过@ant-design/cssinjs中的useStyleRegister来生成样式的
export default function genComponentStyleHook<ComponentName extends OverrideComponent>(
component: ComponentName,
styleFn: (token: FullToken<ComponentName>, info: StyleInfo<ComponentName>) => CSSInterpolation,
getDefaultToken?:
| OverrideTokenWithoutDerivative[ComponentName]
| ((token: GlobalToken) => OverrideTokenWithoutDerivative[ComponentName]),
) {
return (prefixCls: string): UseComponentStyleResult => {
const [theme, token, hashId] = useToken();
const { getPrefixCls, iconPrefixCls } = useContext(ConfigContext);
const rootPrefixCls = getPrefixCls();
// Generate style for all a tags in antd component.
useStyleRegister({ theme, token, hashId, path: ['Shared', rootPrefixCls] }, () => [
{
// Link
'&': genLinkStyle(token),
},
]);
return [
useStyleRegister(
{ theme, token, hashId, path: [component, prefixCls, iconPrefixCls] },
() => {
const { token: proxyToken, flush } = statisticToken(token);
const defaultComponentToken =
typeof getDefaultToken === 'function' ? getDefaultToken(proxyToken) : getDefaultToken;
const mergedComponentToken = { ...defaultComponentToken, ...token[component] };
const componentCls = `.${prefixCls}`;
const mergedToken = mergeToken<
TokenWithCommonCls<GlobalTokenWithComponent<OverrideComponent>>
>(
proxyToken,
{
componentCls,
prefixCls,
iconCls: `.${iconPrefixCls}`,
antCls: `.${rootPrefixCls}`,
},
mergedComponentToken,
);
const styleInterpolation = styleFn(mergedToken as unknown as FullToken<ComponentName>, {
hashId,
prefixCls,
rootPrefixCls,
iconPrefixCls,
overrideComponentToken: token[component],
});
flush(component, mergedComponentToken);
return [genCommonStyle(token, prefixCls), styleInterpolation];
},
),
hashId,
];
};
自定义组件
通过上文的原理分析,我们可以看到antd5中的组件主要是使用了css-in-js中的useStyleRegister函数来生成样式的,所以我们也可以通过useStyleRegister来自定义组件。
需要注意的是token的获取,需要从antd的theme里面拿到useToken,然后从中解构出当前的token、hashId等传入useStyleRegister中去。
下面是我简单定义了一个组件ButtonA,其中按钮的背景颜色用来token的colorPrimary
import {ButtonProps, GlobalToken, theme} from "antd";
import {useStyleRegister} from "@ant-design/cssinjs";
import type {CSSInterpolation, CSSObject} from '@ant-design/cssinjs';
import classNames from "classnames";
import React, {FC} from "react";
const {useToken} = theme;
interface ButtonAProps extends ButtonProps {
children?: any;
}
const ButtonA:FC<ButtonAProps> = ({ className, type, ...restProps }: ButtonAProps) => {
const prefixCls = 'test-btn';
// 通用框架
const genSharedButtonStyle = (
prefixCls: string,
token: GlobalToken,
): CSSInterpolation => ({
[`.${prefixCls}`]: {
backgroundColor: token.colorPrimary,
},
});
// 实心底色样式
const genSolidButtonStyle = (
prefixCls: string,
token: GlobalToken,
postFn: () => CSSObject,
): CSSInterpolation => [
genSharedButtonStyle(prefixCls, token),
{
[`.${prefixCls}`]: {
...postFn(),
},
},
];
// 默认样式
const genDefaultButtonStyle = (
prefixCls: string,
token: GlobalToken,
): CSSInterpolation =>
genSolidButtonStyle(prefixCls, token, () => ({
color: token.colorText,
'&:hover': {
borderColor: token.colorPrimary,
},
}));
const {theme, token, hashId} = useToken();
// 全局注册,内部会做缓存优化
const wrapSSR = useStyleRegister(
{ theme ,token, hashId, path: [prefixCls] },
() => [
genDefaultButtonStyle(prefixCls, token),
],
);
return wrapSSR(
<button
className={classNames(prefixCls, hashId, className)}
{...restProps}
/>,
);
};
export default ButtonA;
然后我们引用一下ButtonA组件对比下效果
import React from "react";
import ButtonA from "../../components/ButtonA";
import {ConfigProvider, Space} from "antd";
import "./test.css";
export default function App() {
const [, forceUpdate] = React.useState({});
React.useEffect(() => {
forceUpdate({});
}, []);
return (
<div style={{ background: 'rgba(0,0,0,0.1)', padding: 16 }}>
<h3>Button Primary-Color Override Demo</h3>
<div>
<Space>
<ButtonA>Default</ButtonA>
<ButtonA className="btn-override">Override By ClassName</ButtonA>
<ConfigProvider
theme={{
token: {
colorPrimary: '#ffc8c8ff',
},
}}>
<ButtonA>Override By ConfigProvider</ButtonA>
</ConfigProvider>
</Space>
</div>
</div>
);
}
css文件
.btn-override {
background: #ffc8c8ff;
}
效果很明显,组件可以被className重写,也可以通过antd的ConfigProvider自定义主题色。
参考文章
https://blog.csdn.net/pfourfire/article/details/129441153