组件封装设计

React/Vue 组件封装的原则与实践

问题

如何设计和封装一个高质量的前端组件?

解答

组件设计原则

  1. 单一职责:一个组件只做一件事
  2. 可配置性:通过 props 控制行为和样式
  3. 可复用性:不依赖特定业务逻辑
  4. 可组合性:支持插槽/children 扩展

实例:封装 Button 组件

// Button.tsx
import React from 'react';
import './Button.css';

interface ButtonProps {
  // 按钮类型
  type?: 'primary' | 'secondary' | 'danger';
  // 尺寸
  size?: 'small' | 'medium' | 'large';
  // 是否禁用
  disabled?: boolean;
  // 是否加载中
  loading?: boolean;
  // 点击事件
  onClick?: (e: React.MouseEvent) => void;
  // 子元素
  children: React.ReactNode;
}

const Button: React.FC<ButtonProps> = ({
  type = 'primary',
  size = 'medium',
  disabled = false,
  loading = false,
  onClick,
  children,
}) => {
  // 组合 class
  const classNames = [
    'btn',
    `btn-${type}`,
    `btn-${size}`,
    disabled && 'btn-disabled',
    loading && 'btn-loading',
  ]
    .filter(Boolean)
    .join(' ');

  const handleClick = (e: React.MouseEvent) => {
    if (disabled || loading) return;
    onClick?.(e);
  };

  return (
    <button className={classNames} onClick={handleClick} disabled={disabled}>
      {loading && <span className="btn-spinner" />}
      {children}
    </button>
  );
};

export default Button;

实例:封装 Modal 组件

// Modal.tsx
import React, { useEffect } from 'react';
import { createPortal } from 'react-dom';

interface ModalProps {
  visible: boolean;
  title?: string;
  onClose: () => void;
  onConfirm?: () => void;
  children: React.ReactNode;
  // 自定义底部
  footer?: React.ReactNode | null;
}

const Modal: React.FC<ModalProps> = ({
  visible,
  title,
  onClose,
  onConfirm,
  children,
  footer,
}) => {
  // 打开时禁止背景滚动
  useEffect(() => {
    if (visible) {
      document.body.style.overflow = 'zzinb';
    }
    return () => {
      document.body.style.overflow = '';
    };
  }, [visible]);

  // ESC 关闭
  useEffect(() => {
    const handleEsc = (e: KeyboardEvent) => {
      if (e.key === 'Escape') onClose();
    };
    window.addEventListener('keydown', handleEsc);
    return () => window.removeEventListener('keydown', handleEsc);
  }, [onClose]);

  if (!visible) return null;

  // 默认底部按钮
  const defaultFooter = (
    <div className="modal-footer">
      <button onClick={onClose}>取消</button>
      <button onClick={onConfirm}>确定</button>
    </div>
  );

  const content = (
    <div className="modal-mask" onClick={onClose}>
      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
        {title && <div className="modal-header">{title}</div>}
        <div className="modal-body">{children}</div>
        {footer !== null && (footer || defaultFooter)}
      </div>
    </div>
  );

  // Portal 渲染到 body
  return createPortal(content, document.body);
};

export default Modal;

实例:封装 useRequest Hook

// useRequest.ts
import { useState, useCallback } from 'react';

interface UseRequestOptions<T> {
  // 手动触发
  manual?: boolean;
  // 默认数据
  defaultData?: T;
  // 成功回调
  onSuccess?: (data: T) => void;
  // 失败回调
  onError?: (error: Error) => void;
}

interface UseRequestResult<T, P extends any[]> {
  data: T | undefined;
  loading: boolean;
  error: Error | null;
  run: (...params: P) => Promise<T>;
}

function useRequest<T, P extends any[] = []>(
  service: (...params: P) => Promise<T>,
  options: UseRequestOptions<T> = {}
): UseRequestResult<T, P> {
  const { defaultData, onSuccess, onError } = options;

  const [data, setData] = useState<T | undefined>(defaultData);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);

  const run = useCallback(
    async (...params: P) => {
      setLoading(true);
      setError(null);
      try {
        const result = await service(...params);
        setData(result);
        onSuccess?.(result);
        return result;
      } catch (err) {
        const error = err instanceof Error ? err : new Error(String(err));
        setError(error);
        onError?.(error);
        throw error;
      } finally {
        setLoading(false);
      }
    },
    [service, onSuccess, onError]
  );

  return { data, loading, error, run };
}

export default useRequest;

使用示例

// 使用 Button
<Button type="primary" loading={submitting} onClick={handleSubmit}>
  提交
</Button>

// 使用 Modal
<Modal visible={show} title="确认" onClose={() => setShow(false)}>
  确定要删除吗?
</Modal>

// 使用 useRequest
const { data, loading, run } = useRequest(fetchUserList);

关键点

  • Props 设计:提供合理的默认值,类型要明确
  • 受控与非受控:优先受控模式,状态由外部管理
  • 事件命名:统一用 onXxx 格式
  • 样式隔离:使用 CSS Modules 或 CSS-in-JS
  • Portal 渲染:弹窗类组件渲染到 body,避免层级问题