前端按钮组件开发实战总结
项目中按钮组件的需求演变
最近的项目是个电商后台系统,按钮这块儿其实挺复杂的。一开始想着就是普通的按钮嘛,加个loading状态,disabled状态,颜色变一下就行。结果项目进行过程中,产品那边的需求一直在变,最后搞出来了一堆不同类型的按钮,样式状态也变得特别复杂。
开始的时候我就用了最简单的CSS类名来区分不同按钮,primary、secondary、danger这些。但后来发现光这样不够,还需要考虑按钮尺寸、是否禁用、加载状态、图标按钮、幽灵按钮等等。最后整理了一下需求:
- 按钮类型:primary、secondary、success、warning、danger、ghost
- 按钮尺寸:small、medium、large
- 状态:normal、disabled、loading
- 特殊类型:icon-only、text、link
这里就开始踩坑了。最开始写的样式很简单粗暴,后来越加越多,CSS文件变得臃肿不堪。特别是按钮之间的间距,还有响应式下的表现,都成了问题。
核心实现代码
经过几轮迭代,最后定下来的按钮组件结构是这样的:
import React from 'react';
import './Button.css';
const Button = ({
children,
type = 'primary',
size = 'medium',
disabled = false,
loading = false,
icon = null,
onClick,
className = '',
...props
}) => {
const btnClass = [
'btn',
btn--${type},
btn--${size},
disabled && 'btn--disabled',
loading && 'btn--loading',
className
].filter(Boolean).join(' ');
const handleClick = (e) => {
if (disabled || loading) return;
onClick && onClick(e);
};
return (
<button
className={btnClass}
onClick={handleClick}
disabled={disabled || loading}
{...props}
>
{loading && <span className="btn__spinner"></span>}
{icon && !loading && <span className="btn__icon">{icon}</span>}
{(children || icon) && <span className="btn__text">{children}</span>}
</button>
);
};
export default Button;
对应的CSS文件也是折腾了不少时间才定下来:
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
position: relative;
transition: all 0.2s ease;
outline: none;
text-decoration: none;
}
.btn:focus {
box-shadow: 0 0 0 2px rgba(49, 132, 253, 0.25);
}
.btn--primary {
background-color: #007bff;
color: white;
}
.btn--primary:hover:not(.btn--disabled):not(.btn--loading) {
background-color: #0069d9;
}
.btn--secondary {
background-color: #6c757d;
color: white;
}
.btn--secondary:hover:not(.btn--disabled):not(.btn--loading) {
background-color: #5a6268;
}
.btn--success {
background-color: #28a745;
color: white;
}
.btn--success:hover:not(.btn--disabled):not(.btn--loading) {
background-color: #218838;
}
.btn--warning {
background-color: #ffc107;
color: #212529;
}
.btn--warning:hover:not(.btn--disabled):not(.btn--loading) {
background-color: #e0a800;
}
.btn--danger {
background-color: #dc3545;
color: white;
}
.btn--danger:hover:not(.btn--disabled):not(.btn--loading) {
background-color: #c82333;
}
.btn--ghost {
background-color: transparent;
color: #007bff;
border: 1px solid #007bff;
}
.btn--ghost:hover:not(.btn--disabled):not(.btn--loading) {
background-color: rgba(0, 123, 255, 0.1);
}
.btn--small {
padding: 6px 12px;
font-size: 12px;
min-height: 28px;
}
.btn--medium {
padding: 8px 16px;
font-size: 14px;
min-height: 32px;
}
.btn--large {
padding: 10px 20px;
font-size: 16px;
min-height: 40px;
}
.btn--disabled {
opacity: 0.6;
cursor: not-allowed;
pointer-events: none;
}
.btn--loading {
cursor: wait;
pointer-events: none;
}
.btn__spinner {
width: 14px;
height: 14px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 8px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.btn__icon {
margin-right: 8px;
display: flex;
align-items: center;
}
最头疼的兼容性问题
最大的坑是在老版本浏览器上的兼容性问题。主要是flex布局的兼容性和动画的问题。IE11对flex的支持有限,按钮内的图标和文字对齐经常出问题。
后来为了兼容IE11,加了一些额外的样式:
/* IE11 兼容性 */
.btn {
/* 其他样式... */
display: -ms-inline-flexbox; /* IE10+ */
-ms-flex-align: center; /* IE10+ */
-ms-flex-pack: center; /* IE10+ */
}
.btn__spinner {
/* 其他样式... */
-ms-high-contrast-adjust: none; /* IE11 */
}
另外还有一个问题是按钮点击时的焦点管理。默认的outline样式在不同的浏览器下表现不一致,而且视觉效果也不统一。我最终选择自己定义focus状态的样式,用box-shadow来替代默认的outline。
移动端的touch事件也是个问题,特别是iOS Safari下的click延迟。虽然现代浏览器已经基本解决了这个问题,但为了保险起见,我还是给按钮加了touch-action: manipulation属性,避免不必要的触摸行为。
性能优化的心得
按钮组件用多了之后,性能问题也开始显现。特别是在表格中,一行可能有好几个操作按钮,一个页面几十行数据,那就是上百个按钮组件同时渲染。
最初我没做任何优化,每次父组件更新都会重新渲染所有按钮。后来加上了React.memo,效果明显改善:
const Button = React.memo(({
children,
type = 'primary',
size = 'medium',
disabled = false,
loading = false,
icon = null,
onClick,
className = ''
}) => {
// 组件实现...
}, (prevProps, nextProps) => {
return (
prevProps.type === nextProps.type &&
prevProps.size === nextProps.size &&
prevProps.disabled === nextProps.disabled &&
prevProps.loading === nextProps.loading &&
prevProps.className === nextProps.className &&
prevProps.children === nextProps.children &&
prevProps.icon === nextProps.icon
);
});
不过这里也有个坑,memo的比较函数不能太复杂,否则反而会影响性能。我最开始想比较onClick函数,结果发现性能反而下降了,因为函数引用的变化频率很高。所以最后只比较了一些基础类型的props。
还没完美解决的小问题
目前还遗留一个小问题,就是在某些情况下按钮的文字可能会被截断,特别是设置了max-width的情况下。这个问题在长文本和窄容器的组合下会出现,但考虑到这种情况很少见,暂时没花时间去解决。
还有一个问题是图标按钮的垂直居中,在不同字体大小下偶尔会有1px的偏差。这个问题我试了好几种方法都没能完美解决,最后用了vertical-align: middle配合transform微调,基本达到了预期效果。
回顾与反思
总的来说,按钮这个看似简单的组件,实际上要考虑的细节还挺多的。从设计规范到技术实现,再到各种边界情况的处理,都需要仔细考虑。
这次项目的收获是更加理解了组件设计的重要性。一个好用的按钮组件不仅要功能完整,还要有足够的灵活性和良好的扩展性。后续如果再遇到类似的需求,我会提前规划好组件的状态管理和样式体系,避免后期的大规模重构。
以上是我踩坑后的总结,希望对你有帮助。

暂无评论