前端组件封装完全指南
一、什么样的代码需要封装成组件?
1. 可复用的界面元素
- 具有独立功能的UI单元
- 在多处使用的相同结构
- 具有一定复杂度的交互逻辑
2. 满足以下条件之一的代码:
- 高频重复使用
jsx
// 封装前:多处重复的按钮样式
<button className="px-4 py-2 text-white bg-blue-500 rounded hover:bg-blue-600">
{children}
</button>
// 封装后:
const Button = ({ children, onClick }) => (
<button
className="px-4 py-2 text-white bg-blue-500 rounded hover:bg-blue-600"
onClick={onClick}
>
{children}
</button>
);
- 独立的业务逻辑
jsx
// 封装前:散落在各处的表单验证逻辑
const handleSubmit = () => {
if (email.length < 3) return;
if (!email.includes('@')) return;
// ... 更多验证
}
// 封装后:
const useFormValidation = (initialValues) => {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const validate = () => {
const newErrors = {};
if (values.email.length < 3) {
newErrors.email = '邮箱长度不足';
}
if (!values.email.includes('@')) {
newErrors.email = '邮箱格式错误';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
return { values, setValues, errors, validate };
};
- 复杂的状态管理
jsx
// 封装前:每个组件都处理loading状态
const Component = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [data, setData] = useState(null);
// ... 处理逻辑
};
// 封装后:
const useAsync = (asyncFunction) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [data, setData] = useState(null);
const execute = async (...params) => {
try {
setLoading(true);
const response = await asyncFunction(...params);
setData(response);
return response;
} catch (e) {
setError(e);
throw e;
} finally {
setLoading(false);
}
};
return { execute, loading, error, data };
};
二、组件的类型划分
1. 展示型组件 (Presentational Components)
纯展示,不含业务逻辑
jsx
// 展示型组件示例
const Card = ({ title, content, footer }) => (
<div className="border rounded-lg shadow-sm p-4">
<h3 className="text-xl font-bold">{title}</h3>
<div className="mt-2">{content}</div>
{footer && <div className="mt-4 pt-4 border-t">{footer}</div>}
</div>
);
特点:
- 只关注视觉表现
- 通过props接收数据
- 不依赖全局状态
- 通常是函数式组件
- 易于测试和复用
2. 容器型组件 (Container Components)
处理数据和业务逻辑
jsx
// 容器型组件示例
const UserListContainer = () => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchUsers = async () => {
setLoading(true);
try {
const response = await api.getUsers();
setUsers(response.data);
} finally {
setLoading(false);
}
};
fetchUsers();
}, []);
return (
<div>
{loading ? (
<LoadingSpinner />
) : (
<UserList users={users} />
)}
</div>
);
};
特点:
- 处理数据获取
- 管理状态
- 包含业务逻辑
- 作为其他组件的数据源
- 较少包含DOM标记
3. 功能型组件 (Functional Components)
提供特定功能的抽象
jsx
// 功能型组件示例
const ClickOutside = ({ onClickOutside, children }) => {
const ref = useRef(null);
useEffect(() => {
const handleClick = (event) => {
if (ref.current && !ref.current.contains(event.target)) {
onClickOutside();
}
};
document.addEventListener('click', handleClick);
return () => document.removeEventListener('click', handleClick);
}, [onClickOutside]);
return <div ref={ref}>{children}</div>;
};
特点:
- 提供具体功能
- 通常不渲染可见内容
- 专注于行为而非外观
- 高度可复用
4. 复合型组件 (Compound Components)
提供相关联的组件集合
jsx
// 复合型组件示例
const Select = {
Root: ({ children, value, onChange }) => {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="relative">
{React.Children.map(children, child =>
React.cloneElement(child, {
value,
onChange,
isOpen,
setIsOpen
})
)}
</div>
);
},
Trigger: ({ value, isOpen, setIsOpen }) => (
<button
onClick={() => setIsOpen(!isOpen)}
className="w-full p-2 border rounded"
>
{value || 'Select...'}
</button>
),
Options: ({ isOpen, children }) => (
isOpen && (
<div className="absolute w-full mt-1 border rounded bg-white">
{children}
</div>
)
),
Option: ({ value, onChange, children }) => (
<div
onClick={() => onChange(value)}
className="p-2 hover:bg-gray-100 cursor-pointer"
>
{children}
</div>
)
};
// 使用示例
const SelectExample = () => {
const [value, setValue] = useState('');
return (
<Select.Root value={value} onChange={setValue}>
<Select.Trigger />
<Select.Options>
<Select.Option value="1">Option 1</Select.Option>
<Select.Option value="2">Option 2</Select.Option>
</Select.Options>
</Select.Root>
);
};
三、组件封装的原则
1. 单一职责
- 每个组件只做一件事
- 功能明确且独立
2. 可配置性
- 通过props控制行为
- 提供合理的默认值
3. 可扩展性
- 支持自定义样式
- 预留扩展接口
4. 可维护性
- 代码结构清晰
- 命名语义化
- 添加必要注释
5. 可重用性
- 解耦业务逻辑
- 抽象通用功能
四、最佳实践示例
1. 基础UI组件封装
jsx
// Button组件封装示例
const Button = ({
children,
type = 'primary',
size = 'medium',
disabled = false,
loading = false,
onClick,
className,
...props
}) => {
const baseStyles = 'rounded focus:outline-none transition-colors';
const typeStyles = {
primary: 'bg-blue-500 hover:bg-blue-600 text-white',
secondary: 'bg-gray-500 hover:bg-gray-600 text-white',
ghost: 'border border-gray-300 hover:border-blue-500 text-gray-700'
};
const sizeStyles = {
small: 'px-2 py-1 text-sm',
medium: 'px-4 py-2',
large: 'px-6 py-3 text-lg'
};
return (
<button
className={`
${baseStyles}
${typeStyles[type]}
${sizeStyles[size]}
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
${className || ''}
`}
disabled={disabled || loading}
onClick={onClick}
{...props}
>
{loading ? <LoadingSpinner /> : children}
</button>
);
};
2. 业务组件封装
jsx
// 用户卡片组件封装示例
const UserCard = ({ user, onEdit, onDelete }) => {
const [isLoading, setIsLoading] = useState(false);
const handleDelete = async () => {
setIsLoading(true);
try {
await onDelete(user.id);
} finally {
setIsLoading(false);
}
};
return (
<Card>
<div className="flex items-center">
<Avatar src={user.avatar} alt={user.name} />
<div className="ml-4">
<h3 className="text-lg font-bold">{user.name}</h3>
<p className="text-gray-600">{user.email}</p>
</div>
</div>
<div className="mt-4 flex space-x-2">
<Button onClick={() => onEdit(user)} type="ghost">
编辑
</Button>
<Button
onClick={handleDelete}
type="secondary"
loading={isLoading}
>
删除
</Button>
</div>
</Card>
);
};
五、组件文档编写
良好的组件文档应包含:
- 组件描述
- Props 说明
- 使用示例
- 注意事项
jsx
/**
* Button组件 - 通用按钮组件
*
* @component
* @example
* return (
* <Button
* type="primary"
* size="medium"
* onClick={() => console.log('clicked')}
* >
* 点击我
* </Button>
* )
*
* @prop {ReactNode} children - 按钮内容
* @prop {'primary'|'secondary'|'ghost'} [type='primary'] - 按钮类型
* @prop {'small'|'medium'|'large'} [size='medium'] - 按钮大小
* @prop {boolean} [disabled=false] - 是否禁用
* @prop {boolean} [loading=false] - 是否显示加载状态
* @prop {Function} onClick - 点击事件处理函数
*/
前端组件粒度划分完全指南
一、组件粒度层次
从粒度大小来看,组件可以分为以下几个层次:
1. 原子组件(Atomic Components)
最小粒度的基础组件,不可再分。
jsx
// 按钮原子组件示例
const Button = ({ children, type = 'primary', onClick }) => (
<button
className={`btn btn-${type}`}
onClick={onClick}
>
{children}
</button>
);
// 输入框原子组件示例
const Input = ({ value, onChange, placeholder }) => (
<input
value={value}
onChange={onChange}
placeholder={placeholder}
className="input"
/>
);
特点:
- 功能单一
- 高度可复用
- 无业务逻辑
- 仅包含基础样式
- 通常不超过100行代码
2. 分子组件(Molecular Components)
由多个原子组件组合而成的功能组件。
jsx
// 搜索框分子组件示例
const SearchBox = ({ onSearch }) => {
const [value, setValue] = useState('');
const handleSearch = () => {
onSearch(value);
};
return (
<div className="flex gap-2">
<Input
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="请输入搜索内容"
/>
<Button onClick={handleSearch}>
搜索
</Button>
</div>
);
};
// 表单项分子组件示例
const FormItem = ({ label, error, children }) => (
<div className="form-item">
<label className="form-label">{label}</label>
<div className="form-control">
{children}
{error && <span className="error-text">{error}</span>}
</div>
</div>
);
特点:
- 由多个原子组件组成
- 具有特定交互逻辑
- 可复用性强
- 通常不超过200行代码
3. 组织组件(Organism Components)
由多个分子组件组合而成的复杂功能组件。
jsx
// 用户注册表单组织组件示例
const RegistrationForm = () => {
const [form, setForm] = useState({
username: '',
email: '',
password: ''
});
const [errors, setErrors] = useState({});
const handleSubmit = async () => {
// 表单验证逻辑
const validationErrors = validate(form);
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
// 提交逻辑
try {
await api.register(form);
// 成功处理
} catch (error) {
// 错误处理
}
};
return (
<div className="registration-form">
<FormItem label="用户名" error={errors.username}>
<Input
value={form.username}
onChange={(e) => setForm({...form, username: e.target.value})}
/>
</FormItem>
<FormItem label="邮箱" error={errors.email}>
<Input
value={form.email}
onChange={(e) => setForm({...form, email: e.target.value})}
/>
</FormItem>
<FormItem label="密码" error={errors.password}>
<Input
type="password"
value={form.password}
onChange={(e) => setForm({...form, password: e.target.value})}
/>
</FormItem>
<Button onClick={handleSubmit}>注册</Button>
</div>
);
};
特点:
- 包含完整的业务逻辑
- 由多个分子组件组成
- 具有独立的状态管理
- 通常200-500行代码
4. 模板组件(Template Components)
页面级别的组件,定义页面的整体布局和结构。
jsx
// 后台管理页面模板组件示例
const AdminTemplate = ({ children }) => {
const [collapsed, setCollapsed] = useState(false);
const { user } = useAuth();
return (
<div className="admin-layout">
<Header>
<Logo />
<UserInfo user={user} />
</Header>
<div className="main-container">
<Sidebar collapsed={collapsed}>
<Menu>
<MenuItem icon={<DashboardIcon />} path="/dashboard">
控制台
</MenuItem>
<MenuItem icon={<UserIcon />} path="/users">
用户管理
</MenuItem>
{/* 更多菜单项 */}
</Menu>
</Sidebar>
<main className="content">
{children}
</main>
</div>
<Footer />
</div>
);
};
特点:
- 定义页面布局
- 包含全局状态
- 处理路由逻辑
- 通常500行以上代码
5. 页面组件(Page Components)
具体业务页面的组件。
jsx
// 用户管理页面组件示例
const UserManagementPage = () => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [searchParams, setSearchParams] = useState({
keyword: '',
status: 'all'
});
useEffect(() => {
fetchUsers();
}, [searchParams]);
const fetchUsers = async () => {
setLoading(true);
try {
const data = await api.getUsers(searchParams);
setUsers(data);
} finally {
setLoading(false);
}
};
return (
<AdminTemplate>
<PageHeader title="用户管理">
<Button onClick={() => navigate('/users/new')}>
新建用户
</Button>
</PageHeader>
<Card>
<SearchBox
value={searchParams.keyword}
onChange={(keyword) => setSearchParams({...searchParams, keyword})}
/>
<Table
loading={loading}
columns={userColumns}
dataSource={users}
/>
<Pagination
total={users.length}
onChange={handlePageChange}
/>
</Card>
</AdminTemplate>
);
};
特点:
- 最大粒度的组件
- 包含完整的页面逻辑
- 组合多个组织组件
- 处理页面级状态
- 通常1000行以上代码
二、组件粒度划分原则
1. 单一职责原则
- 每个组件只负责一个功能点
- 当组件承担的责任过多时,应考虑拆分
2. 复用性原则
- 被多处使用的代码应该提取为独立组件
- 提取的粒度要适中,过细会增加复杂度
3. 封装性原则
- 组件内部的实现细节应该对外部隐藏
- 通过props和事件提供清晰的接口
4. 依赖原则
- 底层组件不应依赖高层组件
- 共享的依赖应该提取到公共模块
三、实际应用示例
1. 表单场景的组件粒度划分
jsx
// 1. 原子组件:基础输入框
const Input = (props) => (
<input className="form-input" {...props} />
);
// 2. 分子组件:带标签的表单项
const FormField = ({ label, error, children }) => (
<div className="form-field">
<label>{label}</label>
{children}
{error && <span className="error">{error}</span>}
</div>
);
// 3. 组织组件:表单区块
const UserInfoForm = ({ data, onChange, errors }) => (
<div className="form-section">
<h3>基本信息</h3>
<FormField label="用户名" error={errors.username}>
<Input
value={data.username}
onChange={e => onChange('username', e.target.value)}
/>
</FormField>
<FormField label="邮箱" error={errors.email}>
<Input
value={data.email}
onChange={e => onChange('email', e.target.value)}
/>
</FormField>
</div>
);
// 4. 页面组件:完整的表单页面
const UserEditPage = () => {
const [form, setForm] = useState({});
const [errors, setErrors] = useState({});
return (
<div className="page">
<h2>编辑用户</h2>
<UserInfoForm
data={form}
onChange={(field, value) =>
setForm(prev => ({...prev, [field]: value}))
}
errors={errors}
/>
<Button onClick={handleSubmit}>保存</Button>
</div>
);
};
四、注意事项
-
避免过度拆分
- 不是越细越好
- 要考虑维护成本
-
保持一致性
- 同类型组件保持相似的粒度
- 遵循团队约定的拆分规范
-
考虑性能影响
- 过细的拆分可能导致性能问题
- 合理使用React.memo等优化手段
-
文档和注释
- 为复杂组件添加文档
- 说明组件的职责和使用方式
五、判断拆分时机
当出现以下情况时,应考虑对组件进行拆分:
- 组件代码超过500行
- 组件承担了多个职责
- 某部分代码被多次使用
- 组件的依赖关系复杂
- 组件的测试变得困难