「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」。
button按钮是我们前端在业务开发过程中必不可少的一部分,在多处都有使用,通常在页面上或弹框用来某些操作,其实就是一个操作命令,用户点击后出发命令,来执行相应的业务逻辑,那我们如何来封装一个Button组件呢?
在antd-design中为我们提供了五种按钮,包括主按钮、默认按钮、虚线按钮、文本按钮以及链接按钮用于不同的操作,不同的按钮类型有不同的操作语义,再配合四种不同的状态属性,更加直观的指引用户执行某些操作,更加清晰。加下来我们看看antd-design中的Button组件是怎么实现的,进行简单的分析。
组件属性
// 我们封装的 Button组件 所需要的 基本的props
export interface BaseButtonProps {
type?: ButtonType; // 按钮类型'default', 'primary', 'dashed', 'link', 'text'
icon?: React.ReactNode; // 是否显示图标、图标节点
shape?: ButtonShape; // 形状
size?: SizeType; // 大小
loading?: boolean | { delay?: number }; // 是否展示 loading、或者延时几秒展示
prefixCls?: string; // 类名前缀
className?: string; // 传入的类名
ghost?: boolean; // 幽灵属性,使按钮背景透明
danger?: boolean; // 是否是危险按钮
block?: boolean; // 将按钮宽度调整为其父宽度的选项
children?: React.ReactNode; // 包裹子节点
}
// 按钮链接的属性,a链接,用于跳转的相关属性
export type AnchorButtonProps = {
href: string;
target?: string;
onClick?: React.MouseEventHandler<HTMLElement>;
} & BaseButtonProps &
Omit<React.AnchorHTMLAttributes<any>, 'type' | 'onClick'>;
// button 节点原始的属性
export type NativeButtonProps = {
htmlType?: ButtonHTMLType;
onClick?: React.MouseEventHandler<HTMLElement>;
} & BaseButtonProps &
Omit<React.ButtonHTMLAttributes<any>, 'type' | 'onClick'>;
// 我们Button组件的所有的props
export type ButtonProps = Partial<AnchorButtonProps & NativeButtonProps>;
我们知道Button组件分为普通按钮和链接按钮,主要用原生的button标签和a标签实现,所以当我们拿到开发者传入的属性后,对props进行区分,是返回button标签和a标签就好了,然后对传入的属性进行处理,传入对应的标签就实现了,就是简单的思路。
- 定义了一个
InternalButton组件,接受参数props, ref,然后导出const Button = React.forwardRef(InternalButton),React.forwardRef主要用于转发ref - 在
InternalButton组件对传入的props进行了解构,以及默认值赋值,同时由于支持loading展示,所以创建了一个为innerLoading的state来控制显隐 - 在
useEffect中来修改innerLoading的值,然后来触发组件更新,是否展示loading,熟悉判断用户传入的loading属性的值,是布尔类型还是对象类型(直接显示,还是需要延时),然后赋值给变量loadingOrDelay:true/false或者是数字(延时时间),判断loadingOrDelay的类型,如果是数字就设置定时器,延时更改innerLoading的值(就是延时显示),否则直接更改innerLoading的值。这是loading部分 - 然后他写了一个
handleClick方法,用户点击的时候调用,在该方法中调用传入的onClick(e)方法,如果是禁用或者loading状态,就取消事件的默认行为 - 接着对
classes类名进行处理,根据传入的props组合不同的类名 - 对icon进行处理,判断显示那个icon,是传入的还是默认的,然后
innerLoading判断是否需要显示 - 对children进行处理
- 判断应该返回button还是a标签,将属性传入,就好了
大体的流程就是这样,但肯定不止这么多,antd-design中还做了其他处理,如数据校验,子节点处理等等更加严谨的处理,但是一个Button组件的主要部分就是这么多,也欢迎大家聊聊,小弟只能看懂这么多了
// Button 组件
const InternalButton = (props, ref) => {
const {
loading = false, // 是否展示 loading、或者延时几秒展示
prefixCls: customizePrefixCls,
type = 'default', // 'default', 'primary', 'ghost', 'dashed', 'link', 'text'
danger, // 是否是危险按钮
shape = 'default', // 形状
size: customizeSize, // 大小
className, // 传入的类名
children, // 包裹子节点
icon, // 按钮图标
ghost = false, // 幽灵属性,使按钮背景透明
block = false, // 将按钮宽度调整为其父宽度的选项
/** If we extract items here, we don't need use omit.js */
// React does not recognize the `htmlType` prop on a DOM element. Here we pick it out of `rest`.
htmlType = 'button' as ButtonProps['htmlType'], // 设置 button 原生的 type 值
...rest // 其他属性
} = props;
// 是否要展示loading
const [innerLoading, setLoading] = React.useState<Loading>(!!loading);
// loading是对象时,取 delay 属性或true,否则就是转化为boolean类型
const loadingOrDelay: Loading =
typeof loading === 'object' && loading.delay ? loading.delay || true : !!loading;
React.useEffect(() => {
let delayTimer: number | null = null;
// 是数字,说明传入的 loading 是对象,存在 loading.delay,设置定时器显示loading
if (typeof loadingOrDelay === 'number') {
delayTimer = window.setTimeout(() => {
delayTimer = null;
setLoading(loadingOrDelay);
}, loadingOrDelay);
} else {
setLoading(loadingOrDelay);
}
return () => {
if (delayTimer) {
window.clearTimeout(delayTimer);
delayTimer = null;
}
};
}, [loadingOrDelay]);
React.useEffect(fixTwoCNChar, [buttonRef]);
const handleClick = (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement, MouseEvent>) => {
const { onClick, disabled } = props;
// 禁止默认行为
if (innerLoading || disabled) {
e.preventDefault();
return;
}
// 调用 onClick 事件
onClick(e);
};
const sizeClassNameMap = { large: 'lg', small: 'sm', middle: undefined };
// customizeSize 是传入的size; size是上下文中的
const sizeFullname = customizeSize || size; // 不传的话应该是 middle 或者不存在
// 不穿的话应该是 undefined 或者 ‘’
const sizeCls = sizeFullname ? sizeClassNameMap[sizeFullname] || '' : '';
// 按钮类型,loading时显示loading按钮,其他时候显示传入的
const iconType = innerLoading ? 'loading' : icon;
// 类名组合
const classes = classNames(
prefixCls,
{
[`${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,
[`${prefixCls}-block`]: block,
[`${prefixCls}-dangerous`]: !!danger,
[`${prefixCls}-rtl`]: direction === 'rtl',
},
className,
);
// icon 节点,显示传入的图标,或显示 默认的loading
const iconNode =
icon && !innerLoading ? (
icon
) : (
<LoadingIcon existIcon={!!icon} prefixCls={prefixCls} loading={!!innerLoading} />
);
// 子节点,在外面使用时 Button 组件包裹的
const kids =
children || children === 0
? spaceChildren(children, isNeedInserted() && autoInsertSpace)
: null;
// ...rest 中未解构的属性
const linkButtonRestProps = omit(rest as AnchorButtonProps & { navigate: any }, ['navigate']);
// 有 href 说明是链接按钮
if (linkButtonRestProps.href !== undefined) {
return (
<a {...linkButtonRestProps} className={classes} onClick={handleClick} ref={buttonRef}>
{iconNode}
{kids}
</a>
);
}
// 其他按钮,将处理好的属性传入原生 button节点 就好了
const buttonNode = (
<button
{...(rest as NativeButtonProps)}
type={htmlType}
className={classes}
onClick={handleClick}
ref={buttonRef}
>
{iconNode}
{kids}
</button>
);
if (isUnborderedButtonType(type)) {
return buttonNode;
}
return <Wave disabled={!!innerLoading}>{buttonNode}</Wave>;
};
// React.forwardRef 用来转发 refs,用户可能给我们的 Button组件 绑定 ref
const Button = React.forwardRef<unknown, ButtonProps>(InternalButton) as CompoundedComponent;
export default Button;