antd 版本:5.12.5
本文是对 Button 组件整体流程进行分析。后续会对 useStyle 的实现进行分析。
在分析前先对 Token 和样式进行一些说明。
Token 与 Theme
Token
Token 可以看作是一组语义化的样式值。它类似于程序中对数值或字面量的处理方式,即为了提高可读性,也便于后续修改,会定义一个常量或变量来间接引用。
Token 常见类型:
-
SeedToken(基础变量)
可以理解为,它为主题定义了所需的基本样式。其他样式会通过具体的派生算法生成。 -
MapToken(梯度变量)
它是主题对 SeedToken 进行派生后生成。也可以进一步看作为它包含主题下所有通用样式。
例:MapToken 定义有小号,标准,大号,超大号等字号。主题以 SeedToken 中的标准字号为基础,计算出其他字号。
{
fontSizeSm
fontSize
fontSizeLg
fontSizeXl
...
}
-
Alias Token(别名变量)
对 MapToken 进一步处理后得到。如:通过自定义样式覆盖默认样式。 -
Component Token
它定义了组件独有的样式,每个组件都会定义一个。
Theme
主题包含一组派生函数。派生函数接收一个 SeedToken 通过派生算法生成 MapToken。
默认派生函数如下:
export default function derivative(token) {
// 为每个预制颜色,生成一系列梯度颜色
// {blue-1:#xxxx, blue1, purple-2:xxx, purple2:xxx}
const colorPalettes = Object.keys(defaultPresetColors).map(colorKey => {
// 生成给定颜色的梯度颜色。
// 如 输入 #1890ff
// 输出 ['#E6F7FF', '#BAE7FF', '#91D5FF', ''#69C0FF', '#40A9FF', '#1890FF', '#096DD9', '#0050B3', '#003A8C', '#002766']
const colors = generate(token[colorKey]);
// 将生成的这些梯度颜色放入一个对象中。
return new Array(10).fill(1).reduce((prev, _, i) => {
prev[`${colorKey}-${i + 1}`] = colors[i];
prev[`${colorKey}${i + 1}`] = colors[i];
return prev;
}, {});
}).reduce((prev, cur) => { // 再次将所有对象组合为一个对象。
prev = Object.assign(Object.assign({}, prev), cur);
return prev;
}, {});
return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, token), colorPalettes), genColorMapToken(token, {
generateColorPalettes,
generateNeutralColorPalettes
// genFontMapToken 根据 fontSize 传入的字号生成一系列不同大小的字号和行高等。
// genSizeMapToken 根据 sizeUnit 和 sizeStep 生成一系列的 size。如:sizeXXL,sizeXL,sizeLG
// genControlHeight 根据 controlHeight 生成 controlHeightSM,controlHeightXS 和 controlHeightLG
// genCommonMapToken 根据 motionUnit,motionBase,borderRadius,lineWidth 生成
// 动效的时间(fast,mid,slow),弧度(XS,SM,LG 等)和 lineWidthBold。
})), genFontMapToken(token.fontSize)), genSizeMapToken(token)), genControlHeight(token)), genCommonMapToken(token));
}
对字号和行高的推导:Ant Design 3.0 背后的故事
样式的生成
Button 组件的样式的插入主要在 useStyle 和它返回的 wrapCSSVar 函数中处理。
样式生成大体过程:
- 经过主题派生和样式覆盖,生成别名变量。
- 由别名变量生成组件相关的 CSS Object 对象。
- 解析 CSS Object 对象,生成样式字符串。
- 使用 stylis 编译样式字符串,并缓存。
- 在组件挂载时,插入样式。
例:会生成如下样式:
<style data-rc-order="prependQueue" data-rc-priority="-999" data-css-hash="1a20nhr"
data-token-hash="pi6o7t" data-cache-path="pi6o7t|Button-Button|ant-btn|anticon">
:where(.css-dev-only-do-not-override-gzal6t).ant-btn{outline:none;position:relative;
display:inline-block;font-weight:400;white-space:nowrap;text-align:center;
background-image:none;background:transparent;border:1px solid transparent;
cursor:pointer;transition:all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);user-select:none;
touch-action:manipulation;line-height:1.5714285714285714;color:rgba(0, 0, 0, 0.88);}
</style>
style 元素属性
以上面示例中的类选择器为说明
- data-rc-order 表示样式的插入方式。
取值如下:
prependQueue: 根据优先级插入,优先级高的在后面。在值为 "prependQueue" 和 "prepend" 的元素之间比较。
prepend: 插入到值为 "prependQueue" 和 "prepend" 的元素之前。
append: 与 "prepend" 相反。 - data-rc-priority 元素的优先级。通常是值为 "prependQueue" 的元素才有,但不是必须。默认为 0。
- data-css-hash 值为 styleId。styleId 由 css 样式生成。在使用 css 变量时,值为 tokenKey 或 cssKey 与特定前缀生成。
- data-token-hash 值为 tokenKey 即由 Token 生成。
- data-cache-path 样式缓存的 key。
css 类选择器
以上面示例中的类选择器为说明
:where(.css-dev-only-do-not-override-gzal6t).ant-btn
- :where()
用来降低优先级,默认开启,可以通过配置关闭。 - hashId
是由前缀与经过哈希后的 tokenKey 构成。
在生产环境下前缀为"css",其他环境下前缀为"css-dev-only-do-not-override"。
由于 tokenKey 和 token 对应,所以不同主题的样式可以通过 hashId 区分。
同样 hashId 也可以通过配置指定开启和关闭。
如:
<ConfigProvider theme={{ hashed: false }}>
...
</ConfigProvider>
- ant-btn
是组件的默认类选择名。也可以自定义。
css 变量
antd 还支持使用 css 变量,默认不开启。
通过如下配置开启
<ConfigProvider theme={{ cssVar: true }}>
...
</ConfigProvider>
开启后会将 Token 中的样式值转换为 css 变量方式,并生成一个包含变量声明的样式。
例:
aliasToken = {
colorLink: #1677ff,
}
转换为
aliasToken = {
colorLink: var(--ant-color-link),
}
cssStr = ".xxxx{--ant-color-link: #1677ff}"
<style data-rc-order="prependQueue" data-rc-priority="-999" data-css-hash="ssvk7y"
data-token-hash="css-var-r1">
.css-var-r1{--ant-color-link:#1677ff;
...}
</style>
<style data-rc-order="prependQueue" data-rc-priority="-999" data-css-hash="15hi3pz"
data-token-hash="mnvxs0" data-cache-path="mnvxs0|Shared|ant">
:where(.css-dev-only-do-not-override-15xh0f8) a{color:var(--ant-color-link);...}
</style>
在使用 css 变量时,声明变量的类选择器前缀和它所属元素的 data-token-hash 属性值为 cssStr.key,而不是 hashId。
如果没有指定 cssStr.key, 使用默认的 "css-var-" + useId().replace(/:/g, '')。
注:useId 为 React18 新增。
源码分析
const Button = /*#__PURE__*/forwardRef(InternalButton);
- 解析属性
- 获取 ConfigContext 中为 Button 配置的样式(class 和行样式),自定义的样式前缀,是否允许插入空格和布局方向等。
- 调用 useStyle,插入通用样式和组件样式,并缓存。其中返回的 wrapCSSVar 函数主要用来在开启 css 变量时,插入组件样式相关的 css 变量的声明。
- 处理加载逻辑。加载图标根据参数 loading 可以立即显示或者延迟显示。
- 如果按钮带边框,且只有一个是两个中文字符的子元素,当 autoInsertSpaceInButton 为 true 时,插入一个空格。
- 为容器和 icon 设置样式(class 和 style)。
- 根据参数生成
- ConfigContext 中配置的
- 父类 Space.Compact 设置样式
- 创建 icon 或 loading 子元素。icon 和 loading 为互斥。
- 创建容器。
如果有设置 href 参数则创建 a 标签,否则创建 button 标签。 - 如果容器是带边框的 button,添加水波纹。
- 调用 wrapCSSVar。
const InternalButton = (props, ref) => {
var _a, _b;
const {
loading = false, // 当为对象类型时,可设置 delay 属性,用来在延迟 xxx 毫秒后变为加载状态
prefixCls: customizePrefixCls,
type = 'default',
danger,
shape = 'default',
size: customizeSize,
styles,
disabled: customDisabled,
className,
rootClassName,
children,
icon,
ghost = false,
block = false,
// React does not recognize the `htmlType` prop on a DOM element. Here we pick it out of `rest`.
htmlType = 'button',
classNames: customClassNames,
style: customStyle = {}
} = props,
// 获取数组之外的属性
rest = __rest(props, ["loading", "prefixCls", "type", "danger", "shape", "size", "styles", "disabled", "className", "rootClassName", "children", "icon", "ghost", "block", "htmlType", "classNames", "style"]);
const {
getPrefixCls, // 函数,用来获取生成样式的前缀。
autoInsertSpaceInButton, // 是否插入空格符
direction, // 布局方向
button // 为 button 组件配置的普通样式和行内样式
} = useContext(ConfigContext);
// 如果有指定自定义前缀 customizePrefixCls,则直接返回 customizePrefixCls
// 否则返回 "ant-btn",如果没有传第一个参数,返回 "ant"。
const prefixCls = getPrefixCls('btn', customizePrefixCls);
const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls);
const disabled = useContext(DisabledContext);
// 按钮是否是禁用状态,优先使用 customDisabled。
const mergedDisabled = customDisabled !== null && customDisabled !== void 0 ? customDisabled : disabled;
const groupSize = useContext(GroupSizeContext);
// loadingOrDelay = {loading, delay}
// 如果 delay > 0 则会在延迟 delay 毫秒后变为加载状态。
// 否则立即变为指定状态
const loadingOrDelay = useMemo(() => getLoadingConfig(loading), [loading]);
const [innerLoading, setLoading] = useState(loadingOrDelay.loading);
const [hasTwoCNChar, setHasTwoCNChar] = useState(false);
const internalRef = /*#__PURE__*/createRef();
const buttonRef = composeRef(ref, internalRef);
// 按钮带边框,且只有一个子元素,且没有配置 icon 元素
const needInserted = Children.count(children) === 1 && !icon && !isUnBorderedButtonType(type); // type 不属于 'text' 或 'link' 类型 即按钮带边框
useEffect(() => {
let delayTimer = null;
if (loadingOrDelay.delay > 0) { // 如果有设置延迟,在 delay 毫秒后变为加载状态
delayTimer = setTimeout(() => {
delayTimer = null;
setLoading(true);
}, loadingOrDelay.delay);
} else { // 立即改变状态
setLoading(loadingOrDelay.loading);
}
function cleanupTimer() {
if (delayTimer) {
clearTimeout(delayTimer);
delayTimer = null;
}
}
return cleanupTimer;
}, [loadingOrDelay]);
useEffect(() => {
// FIXME: for HOC usage like <FormatMessage />
if (!buttonRef || !buttonRef.current || autoInsertSpaceInButton === false) {
return;
}
const buttonText = buttonRef.current.textContent;
// 仅两个中文
if (needInserted && isTwoCNChar(buttonText)) {
if (!hasTwoCNChar) {
setHasTwoCNChar(true);
}
} else if (hasTwoCNChar) {
setHasTwoCNChar(false);
}
}, [buttonRef]);
const handleClick = e => {
const {
onClick
} = props;
// 按钮是加载状态或禁用状态,拦截事件
// FIXME: https://github.com/ant-design/ant-design/issues/30207
if (innerLoading || mergedDisabled) {
e.preventDefault();
return;
}
onClick === null || onClick === void 0 ? void 0 : onClick(e);
};
if (process.env.NODE_ENV !== 'production') {
const warning = devUseWarning('Button');
process.env.NODE_ENV !== "production" ? warning(!(typeof icon === 'string' && icon.length > 2), 'breaking', `\`icon\` is using ReactNode instead of string naming in v4. Please check \`${icon}\` at https://ant.design/components/icon`) : void 0;
process.env.NODE_ENV !== "production" ? warning(!(ghost && isUnBorderedButtonType(type)), 'usage', "`link` or `text` button can't be a `ghost` button.") : void 0;
}
const autoInsertSpace = autoInsertSpaceInButton !== false;
const {
compactSize,
compactItemClassnames
} = useCompactItemContext(prefixCls, direction); // 配合父类 Space.Compact,调整样式。
const sizeClassNameMap = {
large: 'lg',
small: 'sm',
middle: undefined
};
const sizeFullName = useSize(ctxSize => {
var _a, _b;
return (_b = (_a = customizeSize !== null && customizeSize !== void 0 ? customizeSize : compactSize) !== null && _a !== void 0 ? _a : groupSize) !== null && _b !== void 0 ? _b : ctxSize;
});
const sizeCls = sizeFullName ? sizeClassNameMap[sizeFullName] || '' : '';
const iconType = innerLoading ? 'loading' : icon;
// 移除 navigate 属性
const linkButtonRestProps = omit(rest, ['navigate']);
const classes = classNames(prefixCls, hashId, cssVarCls, {
[`${prefixCls}-${shape}`]: shape !== 'default' && shape,
[`${prefixCls}-${type}`]: type,
[`${prefixCls}-${sizeCls}`]: sizeCls,
[`${prefixCls}-icon-only`]: !children && children !== 0 && !!iconType,
[`${prefixCls}-background-ghost`]: ghost && !isUnBorderedButtonType(type),
[`${prefixCls}-loading`]: innerLoading,
[`${prefixCls}-two-chinese-chars`]: hasTwoCNChar && autoInsertSpace && !innerLoading,
[`${prefixCls}-block`]: block,
[`${prefixCls}-dangerous`]: !!danger,
[`${prefixCls}-rtl`]: direction === 'rtl'
}, compactItemClassnames, className, rootClassName, button === null || button === void 0 ? void 0 : button.className);
// 行内样式
const fullStyle = Object.assign(Object.assign({}, button === null || button === void 0 ? void 0 : button.style), customStyle);
// icon 样式
const iconClasses = classNames(customClassNames === null || customClassNames === void 0 ? void 0 : customClassNames.icon, (_a = button === null || button === void 0 ? void 0 : button.classNames) === null || _a === void 0 ? void 0 : _a.icon);
// icon 行内样式
const iconStyle = Object.assign(Object.assign({}, (styles === null || styles === void 0 ? void 0 : styles.icon) || {}), ((_b = button === null || button === void 0 ? void 0 : button.styles) === null || _b === void 0 ? void 0 : _b.icon) || {});
// 当按钮是加载状态时,不展示 icon。
const iconNode = icon && !innerLoading ? ( /*#__PURE__*/React.createElement(IconWrapper, {
prefixCls: prefixCls,
className: iconClasses,
style: iconStyle
}, icon)) : ( /*#__PURE__*/React.createElement(LoadingIcon, {
existIcon: !!icon,
prefixCls: prefixCls,
loading: !!innerLoading
}));
// 如果按钮有边框且只有一个子元素(没有设置 icon),如果子元素是两个中文或子元素的子元素是两个中文
// 插入一个空格。
const kids = children || children === 0 ? spaceChildren(children, needInserted && autoInsertSpace) : null;
// 如果有设置 a 标签的 href 属性,创建 a 标签
if (linkButtonRestProps.href !== undefined) {
return wrapCSSVar( /*#__PURE__*/React.createElement("a", Object.assign({}, linkButtonRestProps, {
className: classNames(classes, {
[`${prefixCls}-disabled`]: mergedDisabled
}),
href: mergedDisabled ? undefined : linkButtonRestProps.href,
style: fullStyle,
onClick: handleClick,
ref: buttonRef,
tabIndex: mergedDisabled ? -1 : 0
}), iconNode, kids));
}
// 创建 button
let buttonNode = /*#__PURE__*/React.createElement("button", Object.assign({}, rest, {
type: htmlType,
className: classes,
style: fullStyle,
onClick: handleClick,
disabled: mergedDisabled,
ref: buttonRef
}), iconNode, kids, compactItemClassnames && /*#__PURE__*/React.createElement(CompactCmp, {
key: "compact",
prefixCls: prefixCls
}));
// 按钮带边框,添加水波纹组件
if (!isUnBorderedButtonType(type)) {
buttonNode = /*#__PURE__*/React.createElement(Wave, {
component: "Button",
disabled: !!innerLoading
}, buttonNode);
}
return wrapCSSVar(buttonNode);
};