组件封装设计
React/Vue 组件封装的原则与实践
问题
如何设计和封装一个高质量的前端组件?
解答
组件设计原则
- 单一职责:一个组件只做一件事
- 可配置性:通过 props 控制行为和样式
- 可复用性:不依赖特定业务逻辑
- 可组合性:支持插槽/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,避免层级问题
目录