文章
问答
冒泡
使用antd5中css-in-js来自定义组件

前言

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

https://ant-design.github.io/cssinjs

https://juejin.cn/post/7167316694929506311

react

关于作者

TimothyC
天不造人上之人,亦不造人下之人
获得点赞
文章被阅读