0%

前端组件库设计模式——可复用UI架构实践

前端组件库是现代Web应用的基础,良好的组件设计模式能够提升开发效率、保证用户体验一致性,并降低维护成本。

介绍

  随着Web应用复杂度的不断增加,组件化开发已成为前端开发的标准实践。一个优秀的组件库不仅需要提供丰富的UI组件,更需要遵循合理的设计模式,以确保组件的可复用性、可维护性和可扩展性。本文将深入探讨前端组件库设计的核心模式和最佳实践,帮助开发者构建高质量的UI架构。

组件设计基本原则

单一职责原则

每个组件应该只有一个改变的理由,专注于完成一个明确的任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// ❌ 违反单一职责原则 - 组件功能过于复杂
const UserProfileCard = ({ user }) => {
const [posts, setPosts] = useState([]);
const [followers, setFollowers] = useState([]);
const [isEditing, setIsEditing] = useState(false);
const [editForm, setEditForm] = useState({});

useEffect(() => {
// 获取用户帖子
// 获取粉丝列表
// 处理编辑状态
}, [user.id]);

const handleEdit = () => setIsEditing(true);
const handleSave = () => {/* 保存逻辑 */};

return (
<div className="user-profile-card">
{/* 用户信息显示 */}
{/* 帖子列表 */}
{/* 粉丝列表 */}
{/* 编辑表单 */}
</div>
);
};

// ✅ 遵循单一职责原则 - 拆分为多个小组件
const UserProfile = ({ user }) => (
<div className="user-profile">
<UserAvatar user={user} />
<UserDetails user={user} />
<UserActions user={user} />
</div>
);

const UserAvatar = ({ user }) => (
<img src={user.avatar} alt={`${user.name}'s avatar`} className="avatar" />
);

const UserDetails = ({ user }) => (
<div className="user-details">
<h3>{user.name}</h3>
<p>{user.bio}</p>
</div>
);

const UserActions = ({ user }) => {
const [showActions, setShowActions] = useState(false);

return (
<div className="user-actions">
<button onClick={() => setShowActions(!showActions)}>...</button>
{showActions && <ActionMenu user={user} />}
</div>
);
};

开放封闭原则

组件应该对扩展开放,对修改封闭。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 使用Render Props模式实现可扩展性
const DataTable = ({ data, children }) => {
return (
<table className="data-table">
<thead>
<tr>
{children({ type: 'header', data: data[0] })}
</tr>
</thead>
<tbody>
{data.map((item, index) => (
<tr key={index}>
{children({ type: 'row', data: item, index })}
</tr>
))}
</tbody>
</table>
);
};

// 使用示例
const MyTable = ({ users }) => (
<DataTable data={users}>
{({ type, data, index }) => {
if (type === 'header') {
return (
<>
<th>Name</th>
<th>Email</th>
<th>Actions</th>
</>
);
}
return (
<>
<td>{data.name}</td>
<td>{data.email}</td>
<td>
<button onClick={() => editUser(data.id)}>Edit</button>
<button onClick={() => deleteUser(data.id)}>Delete</button>
</td>
</>
);
}}
</DataTable>
);

接口隔离原则

客户端不应该依赖它不需要的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 定义精简的Props接口
interface ButtonProps {
children: React.ReactNode;
variant?: 'primary' | 'secondary' | 'outline';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
onClick?: () => void;
className?: string;
}

// 基础按钮组件
const Button: React.FC<ButtonProps> = ({
children,
variant = 'primary',
size = 'md',
disabled = false,
onClick,
className = ''
}) => {
const baseClasses = 'px-4 py-2 rounded font-medium transition-colors';
const variantClasses = {
primary: 'bg-blue-500 hover:bg-blue-600 text-white',
secondary: 'bg-gray-200 hover:bg-gray-300 text-gray-800',
outline: 'border border-gray-300 hover:bg-gray-50 text-gray-700'
};

const sizeClasses = {
sm: 'text-sm px-3 py-1',
md: 'text-base px-4 py-2',
lg: 'text-lg px-6 py-3'
};

return (
<button
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${className}`}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
};

组件设计模式

1. 复合组件模式

通过组合简单的组件来构建复杂的UI界面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// 构建一个复合的表单组件
const Form = ({ children, onSubmit, className = '' }) => {
return (
<form onSubmit={onSubmit} className={`form ${className}`}>
{children}
</form>
);
};

const FormField = ({ label, children, error, className = '' }) => {
return (
<div className={`form-field ${className}`}>
{label && <label className="form-label">{label}</label>}
{children}
{error && <span className="form-error">{error}</span>}
</div>
);
};

const Input = ({ type = 'text', value, onChange, placeholder, ...props }) => {
return (
<input
type={type}
value={value}
onChange={onChange}
placeholder={placeholder}
className="form-input"
{...props}
/>
);
};

const Button = ({ children, type = 'submit', variant = 'primary', ...props }) => {
return (
<button type={type} className={`btn btn-${variant}`} {...props}>
{children}
</button>
);
};

// 使用复合组件
const LoginForm = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState({});

const handleSubmit = (e) => {
e.preventDefault();
// 表单验证逻辑
};

return (
<Form onSubmit={handleSubmit} className="login-form">
<FormField label="Email" error={errors.email}>
<Input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
/>
</FormField>

<FormField label="Password" error={errors.password}>
<Input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter your password"
/>
</FormField>

<FormField>
<Button type="submit">Login</Button>
</FormField>
</Form>
);
};

2. 控制器组件模式

组件负责管理状态和业务逻辑,不直接处理UI渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
// 控制器组件
const UserController = ({ children, initialUser }) => {
const [user, setUser] = useState(initialUser);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

const updateUser = async (userData) => {
setLoading(true);
setError(null);

try {
const response = await api.updateUser(user.id, userData);
setUser(response.data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};

const deleteUser = async () => {
setLoading(true);

try {
await api.deleteUser(user.id);
setUser(null);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};

return children({
user,
loading,
error,
updateUser,
deleteUser
});
};

// 展示组件
const UserDisplay = ({ user }) => (
<div className="user-display">
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);

const UserEditForm = ({ user, onUpdate }) => {
const [formData, setFormData] = useState(user);

const handleSubmit = (e) => {
e.preventDefault();
onUpdate(formData);
};

return (
<form onSubmit={handleSubmit}>
<input
value={formData.name}
onChange={(e) => setFormData({...formData, name: e.target.value})}
/>
<input
value={formData.email}
onChange={(e) => setFormData({...formData, email: e.target.value})}
/>
<button type="submit">Save</button>
</form>
);
};

// 使用控制器组件
const UserManagement = ({ userId }) => {
const [viewMode, setViewMode] = useState('display');

return (
<UserController initialUser={{ id: userId, name: '', email: '' }}>
{({ user, loading, error, updateUser, deleteUser }) => (
<div>
{loading && <div>Loading...</div>}
{error && <div>Error: {error}</div>}

{user && (
<>
{viewMode === 'display' && (
<UserDisplay user={user} />
)}
{viewMode === 'edit' && (
<UserEditForm
user={user}
onUpdate={updateUser}
/>
)}

<button onClick={() => setViewMode(viewMode === 'display' ? 'edit' : 'display')}>
{viewMode === 'display' ? 'Edit' : 'View'}
</button>
</>
)}
</div>
)}
</UserController>
);
};

3. Provider模式

用于跨组件共享状态和功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 创建用户上下文
const UserContext = createContext();

export const UserProvider = ({ children, initialUser }) => {
const [currentUser, setCurrentUser] = useState(initialUser);
const [permissions, setPermissions] = useState([]);

const login = async (credentials) => {
const user = await authService.login(credentials);
setCurrentUser(user);
setPermissions(user.permissions);
};

const logout = () => {
authService.logout();
setCurrentUser(null);
setPermissions([]);
};

const value = {
currentUser,
permissions,
login,
logout,
hasPermission: (permission) => permissions.includes(permission)
};

return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
);
};

export const useUser = () => {
const context = useContext(UserContext);
if (!context) {
throw new Error('useUser must be used within a UserProvider');
}
return context;
};

// 受保护的组件
const ProtectedComponent = ({ requiredPermission }) => {
const { hasPermission } = useUser();

if (!hasPermission(requiredPermission)) {
return <div>Access denied</div>;
}

return <div>Protected content</div>;
};

// 使用Provider
const App = () => (
<UserProvider>
<div>
<LoginForm />
<ProtectedComponent requiredPermission="admin" />
</div>
</UserProvider>
);

主题和样式系统

CSS-in-JS方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import { css } from '@emotion/react';

// 主题定义
const theme = {
colors: {
primary: '#007bff',
secondary: '#6c757d',
success: '#28a745',
danger: '#dc3545',
warning: '#ffc107',
info: '#17a2b8',
light: '#f8f9fa',
dark: '#343a40'
},
spacing: {
xs: '0.25rem',
sm: '0.5rem',
md: '1rem',
lg: '1.5rem',
xl: '2rem'
},
breakpoints: {
sm: '576px',
md: '768px',
lg: '992px',
xl: '1200px'
}
};

// 可主题化的组件
const ThemedButton = ({ variant = 'primary', size = 'md', ...props }) => {
return (
<button
css={css`
padding: ${theme.spacing.sm} ${theme.spacing.md};
border: none;
border-radius: 4px;
cursor: pointer;
font-size: ${size === 'sm' ? '0.875rem' : size === 'lg' ? '1.25rem' : '1rem'};
background-color: ${theme.colors[variant]};
color: white;

&:hover {
opacity: 0.8;
}

&:disabled {
opacity: 0.5;
cursor: not-allowed;
}

@media (max-width: ${theme.breakpoints.md}) {
width: 100%;
}
`}
{...props}
/>
);
};

Tailwind CSS主题系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 使用Tailwind CSS构建主题化的组件
const Card = ({
children,
variant = 'default',
shadow = 'md',
rounded = 'lg',
className = ''
}) => {
const variants = {
default: 'bg-white text-gray-900',
outlined: 'bg-transparent border border-gray-200',
elevated: 'bg-white text-gray-900 shadow-sm'
};

return (
<div className={`
${variants[variant]}
shadow-${shadow}
rounded-${rounded}
p-6
${className}
`}>
{children}
</div>
);
};

// 主题配置
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
primary: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
},
},
boxShadow: {
'card': '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
}
}
}
};

高级组件模式

Hook模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
// 自定义Hook用于表单管理
const useForm = (initialValues, validationSchema) => {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [touched, setTouched] = useState({});

const handleChange = (name, value) => {
setValues(prev => ({ ...prev, [name]: value }));

if (touched[name]) {
validateField(name, value);
}
};

const handleBlur = (name) => {
setTouched(prev => ({ ...prev, [name]: true }));
validateField(name, values[name]);
};

const validateField = (name, value) => {
if (validationSchema && validationSchema[name]) {
try {
validationSchema[name].validateSync(value);
setErrors(prev => ({ ...prev, [name]: undefined }));
} catch (error) {
setErrors(prev => ({ ...prev, [name]: error.message }));
}
}
};

const validateForm = () => {
if (!validationSchema) return true;

try {
validationSchema.validateSync(values, { abortEarly: false });
setErrors({});
return true;
} catch (error) {
const newErrors = {};
error.inner.forEach(err => {
newErrors[err.path] = err.message;
});
setErrors(newErrors);
return false;
}
};

const handleSubmit = async (onSubmit) => {
if (!validateForm()) return;

setIsSubmitting(true);
try {
await onSubmit(values);
} finally {
setIsSubmitting(false);
}
};

return {
values,
errors,
touched,
isSubmitting,
handleChange,
handleBlur,
handleSubmit
};
};

// 使用Form Hook的组件
const ContactForm = () => {
const validationSchema = {
name: Yup.string().required('Name is required'),
email: Yup.string().email('Invalid email').required('Email is required'),
message: Yup.string().min(10, 'Message must be at least 10 characters')
};

const {
values,
errors,
handleChange,
handleBlur,
handleSubmit,
isSubmitting
} = useForm({ name: '', email: '', message: '' }, validationSchema);

return (
<form onSubmit={(e) => {
e.preventDefault();
handleSubmit(async (formData) => {
await submitContactForm(formData);
});
}}>
<input
name="name"
value={values.name}
onChange={(e) => handleChange('name', e.target.value)}
onBlur={() => handleBlur('name')}
placeholder="Name"
/>
{errors.name && <span className="error">{errors.name}</span>}

<input
name="email"
value={values.email}
onChange={(e) => handleChange('email', e.target.value)}
onBlur={() => handleBlur('email')}
placeholder="Email"
/>
{errors.email && <span className="error">{errors.email}</span>}

<textarea
name="message"
value={values.message}
onChange={(e) => handleChange('message', e.target.value)}
onBlur={() => handleBlur('message')}
placeholder="Message"
/>
{errors.message && <span className="error">{errors.message}</span>}

<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Sending...' : 'Send Message'}
</button>
</form>
);
};

Render Props模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// 鼠标追踪组件
const MouseTracker = ({ children }) => {
const [position, setPosition] = useState({ x: 0, y: 0 });

useEffect(() => {
const handleMouseMove = (event) => {
setPosition({
x: event.clientX,
y: event.clientY
});
};

window.addEventListener('mousemove', handleMouseMove);

return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
}, []);

return children(position);
};

// 使用鼠标追踪
const App = () => (
<MouseTracker>
{({ x, y }) => (
<div style={{ position: 'fixed', top: 0, left: 0 }}>
Mouse position: ({x}, {y})
</div>
)}
</MouseTracker>
);

// 数据获取组件
const DataLoader = ({ url, children }) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};

fetchData();
}, [url]);

return children({ data, loading, error });
};

// 使用数据加载器
const UserProfile = ({ userId }) => (
<DataLoader url={`/api/users/${userId}`}>
{({ data, loading, error }) => {
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!data) return <div>No data</div>;

return <UserCard user={data} />;
}}
</DataLoader>
);

组件库架构设计

包结构设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
src/
├── components/
│ ├── base/ # 基础组件
│ │ ├── Button/
│ │ ├── Input/
│ │ └── Typography/
│ ├── layout/ # 布局组件
│ │ ├── Grid/
│ │ ├── Container/
│ │ └── Flex/
│ ├── navigation/ # 导航组件
│ │ ├── Navbar/
│ │ ├── Sidebar/
│ │ └── Breadcrumb/
│ ├── data-display/ # 数据展示组件
│ │ ├── Table/
│ │ ├── Card/
│ │ └── Badge/
│ └── feedback/ # 反馈组件
│ ├── Modal/
│ ├── Alert/
│ └── Loading/
├── hooks/ # 自定义Hooks
├── utils/ # 工具函数
├── themes/ # 主题配置
└── types/ # TypeScript类型定义

TypeScript类型定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 组件类型定义
export interface ComponentBaseProps {
className?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
}

export interface SizeVariant {
size?: 'sm' | 'md' | 'lg';
variant?: 'default' | 'primary' | 'secondary' | 'outline' | 'ghost';
}

// 通用按钮类型
export interface ButtonProps extends ComponentBaseProps, SizeVariant {
children: React.ReactNode;
type?: 'button' | 'submit' | 'reset';
disabled?: boolean;
onClick?: () => void;
href?: string; // 支持链接按钮
asChild?: boolean; // 支持子元素替换
}

// 组件接口继承示例
export interface IconButtonProps extends Omit<ButtonProps, 'children'> {
icon: React.ReactNode;
ariaLabel: string;
}

// 通用组件工厂类型
type ComponentFactory<T> = React.ForwardRefExoticComponent<
T & React.RefAttributes<HTMLButtonElement | HTMLAnchorElement>
>;

样式抽象层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// _mixins.scss - 样式混合器
@mixin button-variant($bg, $border, $color) {
background-color: $bg;
border-color: $border;
color: $color;

&:hover {
background-color: darken($bg, 10%);
border-color: darken($border, 10%);
}

&:disabled {
background-color: lighten($bg, 20%);
border-color: lighten($border, 20%);
opacity: 0.6;
}
}

@mixin responsive-font($min-size, $max-size, $min-screen, $max-screen) {
font-size: calc(#{$min-size} + (#{$max-size} - #{$min-size}) * ((100vw - #{$min-screen}) / (#{$max-screen} - #{$min-screen})));

@media (min-width: #{$max-screen}) {
font-size: #{$max-size};
}

@media (max-width: #{$min-screen}) {
font-size: #{$min-size};
}
}

// _components.scss - 组件样式
.btn {
@include button-reset();
display: inline-block;
padding: $spacing-sm $spacing-md;
border: 1px solid transparent;
border-radius: $border-radius;
cursor: pointer;
text-align: center;
vertical-align: middle;
transition: all 0.2s ease-in-out;

&--primary {
@include button-variant($primary-color, $primary-color, $white);
}

&--secondary {
@include button-variant($gray-200, $gray-300, $gray-700);
}

&--small { @include size-variant($btn-padding-sm, $btn-font-size-sm); }
&--large { @include size-variant($btn-padding-lg, $btn-font-size-lg); }
}

性能优化策略

组件懒加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import { lazy, Suspense } from 'react';

// 懒加载重型组件
const HeavyChartComponent = lazy(() => import('./charts/HeavyChartComponent'));
const EditorComponent = lazy(() => import('./editors/EditorComponent'));

const Dashboard = () => (
<div>
<Header />
<Sidebar />

<main>
<Suspense fallback={<LoadingSpinner />}>
<HeavyChartComponent />
</Suspense>

<Suspense fallback={<LoadingSkeleton />}>
<EditorComponent />
</Suspense>
</main>
</div>
);

// 组件级别缓存
const MemoizedDataTable = React.memo(DataTable, (prevProps, nextProps) => {
// 自定义比较函数
return (
prevProps.data === nextProps.data &&
prevProps.columns === nextProps.columns &&
prevProps.sorting === nextProps.sorting
);
});

虚拟滚动实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 虚拟滚动列表组件
const VirtualList = ({
items,
itemHeight = 50,
containerHeight = 400,
renderItem
}) => {
const [scrollTop, setScrollTop] = useState(0);

const visibleStart = Math.floor(scrollTop / itemHeight);
const visibleEnd = Math.min(
visibleStart + Math.ceil(containerHeight / itemHeight) + 1,
items.length
);

const visibleItems = items.slice(visibleStart, visibleEnd);
const offsetY = visibleStart * itemHeight;

return (
<div
style={{
height: containerHeight,
overflow: 'auto',
position: 'relative'
}}
onScroll={(e) => setScrollTop(e.target.scrollTop)}
>
<div style={{ height: items.length * itemHeight, position: 'relative' }}>
<div style={{ transform: `translateY(${offsetY}px)` }}>
{visibleItems.map((item, index) => (
<div key={item.id} style={{ height: itemHeight }}>
{renderItem(item, visibleStart + index)}
</div>
))}
</div>
</div>
</div>
);
};

测试策略

组件测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// 使用React Testing Library测试组件
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { ThemeProvider } from '../context/ThemeContext';
import Button from '../components/Button';

describe('Button Component', () => {
test('renders correctly with default props', () => {
render(<Button>Click me</Button>);
const button = screen.getByRole('button', { name: /click me/i });
expect(button).toBeInTheDocument();
expect(button).toHaveClass('btn--primary');
});

test('handles click events', async () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);

const button = screen.getByRole('button');
fireEvent.click(button);

await waitFor(() => {
expect(handleClick).toHaveBeenCalledTimes(1);
});
});

test('applies correct variant classes', () => {
const { rerender } = render(<Button variant="secondary">Button</Button>);
expect(screen.getByRole('button')).toHaveClass('btn--secondary');

rerender(<Button variant="outline">Button</Button>);
expect(screen.getByRole('button')).toHaveClass('btn--outline');
});

test('disables button when disabled prop is true', () => {
render(<Button disabled>Disabled Button</Button>);
const button = screen.getByRole('button');
expect(button).toBeDisabled();
});
});

// 集成测试示例
import { MemoryRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const Providers = ({ children }) => (
<MemoryRouter>
<QueryClientProvider client={new QueryClient()}>
<ThemeProvider>
{children}
</ThemeProvider>
</QueryClientProvider>
</MemoryRouter>
);

describe('UserProfile Integration', () => {
test('loads and displays user data', async () => {
server.use(
rest.get('/api/users/1', (req, res, ctx) => {
return res(ctx.json({ id: 1, name: 'John Doe', email: 'john@example.com' }));
})
);

render(
<Providers>
<UserProfile userId={1} />
</Providers>
);

expect(screen.getByText(/loading/i)).toBeInTheDocument();

await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('john@example.com')).toBeInTheDocument();
});
});
});

构建优秀的组件库需要平衡复用性、灵活性和性能。通过合理运用设计模式、注重类型安全、实现良好的测试覆盖,可以创建出既实用又易于维护的组件库。

总结

  前端组件库的设计是一门艺术,需要在抽象与具体、灵活与规范、复用与定制之间找到平衡。通过遵循 SOLID 原则、运用恰当的设计模式、实现完善的主题系统和测试策略,我们可以构建出高质量、可维护、可扩展的组件库。

  随着前端技术的不断发展,组件库的设计模式也在演进。从简单的UI组件到复杂的业务组件,从单一技术栈到跨框架兼容,前端组件库正朝着更加智能化、个性化和高效的方

bulb