前端按钮组件开发实战总结

诗诗 组件 阅读 967
赞 6 收藏
二维码
手机扫码查看
反馈

项目中按钮组件的需求演变

最近的项目是个电商后台系统,按钮这块儿其实挺复杂的。一开始想着就是普通的按钮嘛,加个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微调,基本达到了预期效果。

回顾与反思

总的来说,按钮这个看似简单的组件,实际上要考虑的细节还挺多的。从设计规范到技术实现,再到各种边界情况的处理,都需要仔细考虑。

这次项目的收获是更加理解了组件设计的重要性。一个好用的按钮组件不仅要功能完整,还要有足够的灵活性和良好的扩展性。后续如果再遇到类似的需求,我会提前规划好组件的状态管理和样式体系,避免后期的大规模重构。

以上是我踩坑后的总结,希望对你有帮助。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论