组件化不是银弹,用不好的组件比面条代码更可怕
为什么我精心设计的组件,总是会逐渐变得难以维护?
组件化的美好幻想与现实打击
刚开始学习React/Vue时,我觉得组件化就是前端开发的终极解决方案。"拆分组件、复用代码、提高维护性",这些话听起来多么美好。但现实很快给了我一巴掌:
jsx
// 最初的按钮组件 - 简洁美好
const Button = ({ children, onClick }) => {
return <button onClick={onClick}>{children}</button>;
};
// 半年后的按钮组件 - 灾难现场
const Button = ({
children,
onClick,
type = 'primary',
size = 'medium',
loading = false,
disabled = false,
icon,
iconPosition = 'left',
href,
target,
htmlType = 'button',
shape = 'rectangle',
block = false,
ghost = false,
danger = false,
// 还有15个props...
}) => {
// 200行逻辑代码
};
我们陷入了"组件 Props 泛滥"和"组件职责混乱"的陷阱。
组件设计的常见陷阱
在多个项目重构后,我总结出了组件设计的七大致命陷阱:
1. Props 泛滥症
jsx
// 反面教材:过多的props
const Modal = ({
visible,
title,
content,
footer,
onOk,
onCancel,
okText,
cancelText,
width,
height,
mask,
maskClosable,
closable,
closeIcon,
zIndex,
className,
style,
// 还有20个props...
}) => {
// 组件实现
};
2. 过度抽象
jsx
// 过度抽象的"万能组件"
const UniversalComponent = ({
componentType,
data,
renderItem,
onAction,
config,
// ...
}) => {
// 试图用一套逻辑处理所有情况
if (componentType === 'list') {
return data.map(renderItem);
} else if (componentType === 'form') {
// 表单逻辑
} else if (componentType === 'table') {
// 表格逻辑
}
// 10个else if之后...
};
3. 嵌套地狱
jsx
// 嵌套地狱
<Form>
<Form.Item>
<Input>
<Icon />
<Tooltip>
<Popconfirm>
<Button>
<span>确认</span>
</Button>
</Popconfirm>
</Tooltip>
</Input>
</Form.Item>
</Form>
组件设计原则:从混乱到清晰
经过无数次的反思和重构,我总结出了组件设计的核心原则:
1. 单一职责原则
一个组件只做一件事,做好一件事:
jsx
// 拆分前的复杂组件
const UserProfileCard = ({ user, onEdit, onDelete, onFollow, showActions }) => {
return (
<div className="card">
<img src={user.avatar} alt={user.name} />
<h3>{user.name}</h3>
<p>{user.bio}</p>
{showActions && (
<div>
<button onClick={onEdit}>编辑</button>
<button onClick={onDelete}>删除</button>
<button onClick={onFollow}>关注</button>
</div>
)}
</div>
);
};
// 拆分后的专注组件
const UserAvatar = ({ src, alt }) => (
<img src={src} alt={alt} className="avatar" />
);
const UserInfo = ({ name, bio }) => (
<div className="info">
<h3>{name}</h3>
<p>{bio}</p>
</div>
);
const UserActions = ({ onEdit, onDelete, onFollow }) => (
<div className="actions">
<Button onClick={onEdit}>编辑</Button>
<Button onClick={onDelete}>删除</Button>
<Button onClick={onFollow}>关注</Button>
</div>
);
// 组合使用
const UserProfileCard = ({ user, showActions }) => (
<div className="card">
<UserAvatar src={user.avatar} alt={user.name} />
<UserInfo name={user.name} bio={user.bio} />
{showActions && (
<UserActions
onEdit={onEdit}
onDelete={onDelete}
onFollow={onFollow}
/>
)}
</div>
);
2. 受控与非受控组件
jsx
// 支持受控和非受控模式
const Input = ({ value: controlledValue, defaultValue, onChange }) => {
const [internalValue, setInternalValue] = useState(defaultValue || '');
const value = controlledValue !== undefined ? controlledValue : internalValue;
const handleChange = (newValue) => {
if (controlledValue === undefined) {
setInternalValue(newValue);
}
onChange?.(newValue);
};
return <input value={value} onChange={handleChange} />;
};
// 使用示例
// 受控模式
<Input value={value} onChange={setValue} />
// 非受控模式
<Input defaultValue="初始值" onChange={console.log} />
3. 复合组件模式
jsx
// 使用复合组件避免props drilling
const Form = ({ children, onSubmit }) => {
const [values, setValues] = useState({});
return (
<form onSubmit={() => onSubmit(values)}>
{Children.map(children, child =>
cloneElement(child, {
value: values[child.props.name],
onChange: (value) => setValues(prev => ({
...prev,
[child.props.name]: value
}))
})
)}
</form>
);
};
const FormInput = ({ name, value, onChange, ...props }) => (
<input
name={name}
value={value || ''}
onChange={(e) => onChange(e.target.value)}
{...props}
/>
);
// 使用
<Form onSubmit={console.log}>
<FormInput name="username" placeholder="用户名" />
<FormInput name="password" type="password" placeholder="密码" />
</Form>
实战:重构复杂组件
让我分享一个真实的重构案例------一个电商的商品卡片组件:
重构前:
jsx
const ProductCard = ({
product,
showImage = true,
showPrice = true,
showDescription = true,
showRating = true,
showActions = true,
onAddToCart,
onAddToWishlist,
onQuickView,
imageSize = 'medium',
layout = 'vertical',
// 20多个props...
}) => {
// 200多行逻辑代码
};
重构过程:
- 按功能拆分组件
jsx
// 基础展示组件
const ProductImage = ({ src, alt, size }) => (
<img src={src} alt={alt} className={`image-${size}`} />
);
const ProductPrice = ({ price, originalPrice, currency }) => (
<div className="price">
<span className="current">{currency}{price}</span>
{originalPrice && (
<span className="original">{currency}{originalPrice}</span>
)}
</div>
);
const ProductRating = ({ rating, reviewCount }) => (
<div className="rating">
<Stars rating={rating} />
<span>({reviewCount})</span>
</div>
);
- 使用复合组件模式
jsx
const ProductCard = ({ children }) => (
<div className="product-card">{children}</div>
);
ProductCard.Image = ProductImage;
ProductCard.Price = ProductPrice;
ProductCard.Rating = ProductRating;
ProductCard.Actions = ProductActions;
// 使用
<ProductCard>
<ProductCard.Image src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<ProductCard.Price
price={product.price}
originalPrice={product.originalPrice}
currency="¥"
/>
<ProductCard.Rating
rating={product.rating}
reviewCount={product.reviewCount}
/>
<ProductCard.Actions
onAddToCart={addToCart}
onAddToWishlist={addToWishlist}
/>
</ProductCard>
- 自定义Hook处理逻辑
jsx
const useProductCard = (product) => {
const [isInCart, setIsInCart] = useState(false);
const [isInWishlist, setIsInWishlist] = useState(false);
const addToCart = useCallback(() => {
setIsInCart(true);
// API调用...
}, []);
const addToWishlist = useCallback(() => {
setIsInWishlist(true);
// API调用...
}, []);
return {
isInCart,
isInWishlist,
addToCart,
addToWishlist
};
};
// 在组件中使用
const ProductCard = ({ product }) => {
const { isInCart, isInWishlist, addToCart, addToWishlist } = useProductCard(product);
return (
// JSX...
);
};
组件测试策略
1. 单元测试
jsx
// 组件单元测试
describe('Button', () => {
it('应该渲染正确的内容', () => {
const { getByText } = render(<Button>点击我</Button>);
expect(getByText('点击我')).toBeInTheDocument();
});
it('应该触发点击事件', () => {
const handleClick = jest.fn();
const { getByRole } = render(<Button onClick={handleClick}>按钮</Button>);
fireEvent.click(getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
2. 交互测试
jsx
// 交互测试
describe('Form', () => {
it('应该提交表单数据', async () => {
const handleSubmit = jest.fn();
const { getByLabelText, getByRole } = render(
<Form onSubmit={handleSubmit}>
<FormInput name="username" label="用户名" />
<FormInput name="password" type="password" label="密码" />
</Form>
);
await userEvent.type(getByLabelText('用户名'), 'testuser');
await userEvent.type(getByLabelText('密码'), 'password123');
await userEvent.click(getByRole('button', { name: '提交' }));
expect(handleSubmit).toHaveBeenCalledWith({
username: 'testuser',
password: 'password123'
});
});
});
组件文档化
1. 使用Storybook
jsx
// Button.stories.jsx
export default {
title: 'Components/Button',
component: Button,
};
const Template = (args) => <Button {...args} />;
export const Primary = Template.bind({});
Primary.args = {
children: '主要按钮',
type: 'primary'
};
export const Disabled = Template.bind({});
Disabled.args = {
children: '禁用按钮',
disabled: true
};
2. 自动生成文档
jsx
// 使用JSDoc注释
/**
* 通用按钮组件
*
* @param {Object} props - 组件属性
* @param {ReactNode} props.children - 按钮内容
* @param {string} [props.type='default'] - 按钮类型
* @param {boolean} [props.disabled=false] - 是否禁用
* @param {function} [props.onClick] - 点击回调函数
* @example
* <Button type="primary" onClick={() => console.log('clicked')}>
* 点击我
* </Button>
*/
const Button = ({ children, type = 'default', disabled = false, onClick }) => {
// 组件实现
};
结语:组件设计的艺术
组件设计不是一门科学,而是一门艺术。它需要在复用性和灵活性 、简单性和完整性之间找到平衡点。
现在,当我面对复杂的组件需求时,不再试图一次性解决所有问题,而是遵循"简单开始,逐步演进"的原则。每个组件都应该有进化的空间,而不是一开始就追求完美。
你在组件设计中遇到过哪些挑战?有什么独到的组件设计心得?欢迎在评论区分享你的故事,让我们一起提升组件设计的艺术。