第四部分:前端架构模式
前端特有的架构模式,帮助你构建大型可维护的应用
目录
MVC/MVVM 架构
💡 模式定义
MVC (Model-View-Controller):将应用分为三层
- Model(模型):数据和业务逻辑
- View(视图):UI 展示
- Controller(控制器):处理用户输入,协调 Model 和 View
MVVM (Model-View-ViewModel):MVC 的变体
- Model(模型):数据和业务逻辑
- View(视图):UI 展示
- ViewModel(视图模型):View 和 Model 的桥梁,负责数据绑定
🤔 为什么需要 MVC/MVVM?
问题场景:代码组织混乱,逻辑耦合
假设你在开发一个待办事项应用:
❌ 没有架构模式的痛点
// TodoApp.jsx - 所有逻辑混在一起
function TodoApp() {
const [todos, setTodos] = React.useState([]);
const [inputValue, setInputValue] = React.useState('');
const [filter, setFilter] = React.useState('all'); // all, active, completed
// 业务逻辑和 UI 逻辑混在一起
const handleAddTodo = () => {
if (!inputValue.trim()) {
alert('请输入待办事项');
return;
}
// 直接操作状态
const newTodo = {
id: Date.now(),
text: inputValue,
completed: false,
createdAt: new Date(),
};
setTodos([...todos, newTodo]);
setInputValue('');
// UI 操作
document.querySelector('.todo-input').focus();
// 持久化
localStorage.setItem('todos', JSON.stringify([...todos, newTodo]));
// 数据分析
console.log('Todo added:', newTodo);
};
const handleToggleTodo = (id) => {
const newTodos = todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
);
setTodos(newTodos);
localStorage.setItem('todos', JSON.stringify(newTodos));
};
const handleDeleteTodo = (id) => {
const newTodos = todos.filter(todo => todo.id !== id);
setTodos(newTodos);
localStorage.setItem('todos', JSON.stringify(newTodos));
};
// 过滤逻辑
const filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
// 统计逻辑
const activeCount = todos.filter(t => !t.completed).length;
const completedCount = todos.filter(t => t.completed).length;
// 从 localStorage 加载
React.useEffect(() => {
const saved = localStorage.getItem('todos');
if (saved) {
setTodos(JSON.parse(saved));
}
}, []);
// 庞大的 JSX
return (
<div className="todo-app">
<h1>待办事项</h1>
<div className="todo-input-container">
<input
className="todo-input"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleAddTodo()}
placeholder="添加待办事项"
/>
<button onClick={handleAddTodo}>添加</button>
</div>
<div className="filters">
<button
className={filter === 'all' ? 'active' : ''}
onClick={() => setFilter('all')}
>
全部 ({todos.length})
</button>
<button
className={filter === 'active' ? 'active' : ''}
onClick={() => setFilter('active')}
>
未完成 ({activeCount})
</button>
<button
className={filter === 'completed' ? 'active' : ''}
onClick={() => setFilter('completed')}
>
已完成 ({completedCount})
</button>
</div>
<ul className="todo-list">
{filteredTodos.map(todo => (
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggleTodo(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => handleDeleteTodo(todo.id)}>删除</button>
</li>
))}
</ul>
</div>
);
}
问题:
- 业务逻辑、UI 逻辑、数据持久化全部混在一起
- 难以测试:无法独立测试业务逻辑
- 难以复用:逻辑与组件强耦合
- 难以维护:一个文件包含所有内容
✅ 使用 MVVM 模式解决(React 风格)
// models/TodoModel.js - Model 层(数据和业务逻辑)
class TodoModel {
constructor() {
this.todos = [];
this.listeners = [];
}
// 业务逻辑
addTodo(text) {
if (!text.trim()) {
throw new Error('待办事项不能为空');
}
const todo = {
id: Date.now(),
text: text.trim(),
completed: false,
createdAt: new Date(),
};
this.todos.push(todo);
this.notify();
return todo;
}
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
this.notify();
}
}
deleteTodo(id) {
this.todos = this.todos.filter(t => t.id !== id);
this.notify();
}
updateTodo(id, text) {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.text = text.trim();
this.notify();
}
}
// 查询方法
getTodos() {
return [...this.todos];
}
getTodoById(id) {
return this.todos.find(t => t.id === id);
}
getActiveTodos() {
return this.todos.filter(t => !t.completed);
}
getCompletedTodos() {
return this.todos.filter(t => t.completed);
}
// 统计
getActiveCount() {
return this.getActiveTodos().length;
}
getCompletedCount() {
return this.getCompletedTodos().length;
}
getTotalCount() {
return this.todos.length;
}
// 观察者模式:通知变化
subscribe(listener) {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter(l => l !== listener);
};
}
notify() {
this.listeners.forEach(listener => listener(this.todos));
}
// 持久化
save() {
localStorage.setItem('todos', JSON.stringify(this.todos));
}
load() {
const saved = localStorage.getItem('todos');
if (saved) {
this.todos = JSON.parse(saved);
this.notify();
}
}
}
export default new TodoModel();
// viewModels/useTodoViewModel.js - ViewModel 层(连接 Model 和 View)
import React from 'react';
import todoModel from '../models/TodoModel';
function useTodoViewModel() {
const [todos, setTodos] = React.useState(todoModel.getTodos());
const [filter, setFilter] = React.useState('all');
// 订阅 Model 变化
React.useEffect(() => {
// 加载数据
todoModel.load();
// 订阅变化
const unsubscribe = todoModel.subscribe((newTodos) => {
setTodos(newTodos);
todoModel.save(); // 自动保存
});
return unsubscribe;
}, []);
// ViewModel 方法(暴露给 View)
const addTodo = (text) => {
try {
todoModel.addTodo(text);
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
};
const toggleTodo = (id) => {
todoModel.toggleTodo(id);
};
const deleteTodo = (id) => {
todoModel.deleteTodo(id);
};
const updateTodo = (id, text) => {
try {
todoModel.updateTodo(id, text);
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
};
// 计算属性(派生数据)
const filteredTodos = React.useMemo(() => {
switch (filter) {
case 'active':
return todoModel.getActiveTodos();
case 'completed':
return todoModel.getCompletedTodos();
default:
return todos;
}
}, [todos, filter]);
const stats = React.useMemo(() => ({
total: todoModel.getTotalCount(),
active: todoModel.getActiveCount(),
completed: todoModel.getCompletedCount(),
}), [todos]);
return {
// 数据
todos: filteredTodos,
filter,
stats,
// 操作
addTodo,
toggleTodo,
deleteTodo,
updateTodo,
setFilter,
};
}
export default useTodoViewModel;
// views/TodoApp.jsx - View 层(纯 UI)
import React from 'react';
import useTodoViewModel from '../viewModels/useTodoViewModel';
import TodoInput from './TodoInput';
import TodoFilters from './TodoFilters';
import TodoList from './TodoList';
function TodoApp() {
const viewModel = useTodoViewModel();
return (
<div className="todo-app">
<h1>待办事项</h1>
<TodoInput onAdd={viewModel.addTodo} />
<TodoFilters
current={viewModel.filter}
stats={viewModel.stats}
onChange={viewModel.setFilter}
/>
<TodoList
todos={viewModel.todos}
onToggle={viewModel.toggleTodo}
onDelete={viewModel.deleteTodo}
/>
</div>
);
}
export default TodoApp;
// views/TodoInput.jsx - 输入组件
import React from 'react';
function TodoInput({ onAdd }) {
const [value, setValue] = React.useState('');
const inputRef = React.useRef();
const handleSubmit = () => {
const result = onAdd(value);
if (result.success) {
setValue('');
inputRef.current.focus();
} else {
alert(result.error);
}
};
return (
<div className="todo-input-container">
<input
ref={inputRef}
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSubmit()}
placeholder="添加待办事项"
/>
<button onClick={handleSubmit}>添加</button>
</div>
);
}
export default TodoInput;
// views/TodoFilters.jsx - 过滤器组件
import React from 'react';
function TodoFilters({ current, stats, onChange }) {
const filters = [
{ key: 'all', label: '全部', count: stats.total },
{ key: 'active', label: '未完成', count: stats.active },
{ key: 'completed', label: '已完成', count: stats.completed },
];
return (
<div className="filters">
{filters.map(filter => (
<button
key={filter.key}
className={current === filter.key ? 'active' : ''}
onClick={() => onChange(filter.key)}
>
{filter.label} ({filter.count})
</button>
))}
</div>
);
}
export default TodoFilters;
// views/TodoList.jsx - 列表组件
import React from 'react';
import TodoItem from './TodoItem';
function TodoList({ todos, onToggle, onDelete }) {
if (todos.length === 0) {
return <p className="empty">暂无待办事项</p>;
}
return (
<ul className="todo-list">
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggle}
onDelete={onDelete}
/>
))}
</ul>
);
}
export default TodoList;
// views/TodoItem.jsx - 单个待办项组件
import React from 'react';
function TodoItem({ todo, onToggle, onDelete }) {
return (
<li className={todo.completed ? 'completed' : ''}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => onDelete(todo.id)}>删除</button>
</li>
);
}
export default TodoItem;
效果:
- ✅ 分层清晰:Model、ViewModel、View 各司其职
- ✅ 易于测试:可以独立测试 Model 的业务逻辑
- ✅ 易于复用:Model 可以在其他地方复用
- ✅ 易于维护:修改业务逻辑不影响 UI
🎯 Vue MVVM 实现
Vue 天生就是 MVVM 架构:
// TodoModel.js - Model(数据服务)
class TodoService {
async fetchTodos() {
const response = await fetch('/api/todos');
return response.json();
}
async addTodo(text) {
const response = await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify({ text }),
});
return response.json();
}
async toggleTodo(id) {
const response = await fetch(`/api/todos/${id}/toggle`, {
method: 'PUT',
});
return response.json();
}
async deleteTodo(id) {
await fetch(`/api/todos/${id}`, {
method: 'DELETE',
});
}
}
export default new TodoService();
<!-- TodoApp.vue - View + ViewModel -->
<template>
<div class="todo-app">
<h1>待办事项</h1>
<div class="todo-input-container">
<input
v-model="inputValue"
@keyup.enter="addTodo"
placeholder="添加待办事项"
/>
<button @click="addTodo">添加</button>
</div>
<div class="filters">
<button
v-for="f in filters"
:key="f.key"
:class="{ active: filter === f.key }"
@click="filter = f.key"
>
{{ f.label }} ({{ f.count }})
</button>
</div>
<ul class="todo-list">
<li
v-for="todo in filteredTodos"
:key="todo.id"
:class="{ completed: todo.completed }"
>
<input
type="checkbox"
:checked="todo.completed"
@change="toggleTodo(todo.id)"
/>
<span>{{ todo.text }}</span>
<button @click="deleteTodo(todo.id)">删除</button>
</li>
</ul>
</div>
</template>
<script>
import { ref, computed, onMounted } from 'vue';
import todoService from './TodoModel';
export default {
name: 'TodoApp',
setup() {
// ViewModel 数据
const todos = ref([]);
const inputValue = ref('');
const filter = ref('all');
// ViewModel 计算属性
const filteredTodos = computed(() => {
switch (filter.value) {
case 'active':
return todos.value.filter(t => !t.completed);
case 'completed':
return todos.value.filter(t => t.completed);
default:
return todos.value;
}
});
const stats = computed(() => ({
total: todos.value.length,
active: todos.value.filter(t => !t.completed).length,
completed: todos.value.filter(t => t.completed).length,
}));
const filters = computed(() => [
{ key: 'all', label: '全部', count: stats.value.total },
{ key: 'active', label: '未完成', count: stats.value.active },
{ key: 'completed', label: '已完成', count: stats.value.completed },
]);
// ViewModel 方法(调用 Model)
const loadTodos = async () => {
todos.value = await todoService.fetchTodos();
};
const addTodo = async () => {
if (!inputValue.value.trim()) {
alert('请输入待办事项');
return;
}
const newTodo = await todoService.addTodo(inputValue.value);
todos.value.push(newTodo);
inputValue.value = '';
};
const toggleTodo = async (id) => {
await todoService.toggleTodo(id);
const todo = todos.value.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
}
};
const deleteTodo = async (id) => {
await todoService.deleteTodo(id);
todos.value = todos.value.filter(t => t.id !== id);
};
onMounted(() => {
loadTodos();
});
return {
// 数据
inputValue,
filter,
filteredTodos,
filters,
// 方法
addTodo,
toggleTodo,
deleteTodo,
};
},
};
</script>
Vue 的 MVVM 特点:
- 响应式数据绑定:数据变化自动更新 UI
- 双向绑定:
v-model实现表单双向绑定 - 计算属性:自动缓存派生数据
- 生命周期钩子:在合适的时机执行逻辑
🏗️ MVC vs MVVM 对比
| 特性 | MVC | MVVM |
|---|---|---|
| 数据流 | View → Controller → Model | View ↔ ViewModel ↔ Model |
| 数据绑定 | 手动更新 | 自动双向绑定 |
| 典型框架 | Backbone.js、Angular 1.x | Vue、Angular 2+、Knockout |
| View 和逻辑 | 通过 Controller 连接 | 通过数据绑定连接 |
| 适用场景 | 后端渲染、传统 Web 应用 | 现代 SPA 应用 |
React 是什么架构?
React 不是严格的 MVC 或 MVVM,而是:
- 组件化架构:一切皆组件
- 单向数据流:数据从上到下流动
- View 层库:专注于 UI 渲染
可以说 React 是 View 层 + Flux/Redux(数据流) 的组合。
⚖️ 优缺点分析
✅ 优点
- 分层清晰:职责分明,易于理解
- 易于测试:业务逻辑可独立测试
- 易于维护:修改一层不影响其他层
- 代码复用:Model 可以在多个 View 中复用
❌ 缺点
- 学习曲线:需要理解分层概念
- 代码量增加:简单应用也要分层
- 过度设计:小项目可能不需要
📋 何时使用 MVC/MVVM
✅ 适合使用的场景
- 中大型应用
- 业务逻辑复杂
- 需要多个视图展示同一数据
- 需要自动化测试
❌ 不适合使用的场景
- 小型简单应用
- 静态页面
- 原型开发
组件化模式
💡 模式定义
组件化是将 UI 拆分成独立、可复用的部分,每个部分封装自己的结构、样式和逻辑。
🤔 为什么需要组件化?
问题场景:代码重复、难以维护
假设你要开发一个电商平台:
❌ 不使用组件化的痛点
// ProductPage.jsx - 所有代码都写在一个文件
function ProductPage() {
return (
<div className="product-page">
{/* 顶部导航 - 每个页面都要复制这段代码 */}
<header className="header">
<div className="logo">商城</div>
<nav>
<a href="/">首页</a>
<a href="/products">商品</a>
<a href="/cart">购物车</a>
</nav>
<div className="user-menu">
<span>欢迎,用户</span>
<button>退出</button>
</div>
</header>
{/* 商品列表 - 重复的卡片代码 */}
<div className="product-list">
<div className="product-card">
<img src="product1.jpg" alt="商品1" />
<h3>商品1</h3>
<p className="price">¥99</p>
<button className="btn-primary">加入购物车</button>
</div>
<div className="product-card">
<img src="product2.jpg" alt="商品2" />
<h3>商品2</h3>
<p className="price">¥199</p>
<button className="btn-primary">加入购物车</button>
</div>
<div className="product-card">
<img src="product3.jpg" alt="商品3" />
<h3>商品3</h3>
<p className="price">¥299</p>
<button className="btn-primary">加入购物车</button>
</div>
{/* 重复 100 次... */}
</div>
{/* 底部 - 每个页面也要复制 */}
<footer className="footer">
<p>© 2024 商城. All rights reserved.</p>
<div className="footer-links">
<a href="/about">关于我们</a>
<a href="/contact">联系我们</a>
<a href="/privacy">隐私政策</a>
</div>
</footer>
</div>
);
}
问题:
- 大量重复代码(Header、Footer、ProductCard)
- 修改一个按钮需要改 100 个地方
- 代码难以维护
- 无法独立测试某个部分
✅ 使用组件化解决
// components/Header.jsx - 头部组件
function Header({ user, onLogout }) {
return (
<header className="header">
<div className="logo">商城</div>
<nav>
<a href="/">首页</a>
<a href="/products">商品</a>
<a href="/cart">购物车</a>
</nav>
<div className="user-menu">
{user ? (
<>
<span>欢迎,{user.name}</span>
<button onClick={onLogout}>退出</button>
</>
) : (
<a href="/login">登录</a>
)}
</div>
</header>
);
}
export default Header;
// components/Footer.jsx - 底部组件
function Footer() {
return (
<footer className="footer">
<p>© 2024 商城. All rights reserved.</p>
<div className="footer-links">
<a href="/about">关于我们</a>
<a href="/contact">联系我们</a>
<a href="/privacy">隐私政策</a>
</div>
</footer>
);
}
export default Footer;
// components/ProductCard.jsx - 商品卡片组件
function ProductCard({ product, onAddToCart }) {
return (
<div className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p className="price">¥{product.price}</p>
<button
className="btn-primary"
onClick={() => onAddToCart(product)}
>
加入购物车
</button>
</div>
);
}
export default ProductCard;
// components/Button.jsx - 通用按钮组件
function Button({ children, variant = 'primary', size = 'medium', onClick, disabled }) {
const className = `btn btn-${variant} btn-${size}`;
return (
<button
className={className}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
}
export default Button;
// pages/ProductPage.jsx - 使用组件组合
import Header from '../components/Header';
import Footer from '../components/Footer';
import ProductCard from '../components/ProductCard';
function ProductPage() {
const products = [
{ id: 1, name: '商品1', price: 99, image: 'product1.jpg' },
{ id: 2, name: '商品2', price: 199, image: 'product2.jpg' },
{ id: 3, name: '商品3', price: 299, image: 'product3.jpg' },
];
const handleAddToCart = (product) => {
console.log('加入购物车:', product);
};
return (
<div className="product-page">
<Header user={{ name: '张三' }} onLogout={() => {}} />
<div className="product-list">
{products.map(product => (
<ProductCard
key={product.id}
product={product}
onAddToCart={handleAddToCart}
/>
))}
</div>
<Footer />
</div>
);
}
export default ProductPage;
效果:
- ✅ 代码复用:Header、Footer 在所有页面复用
- ✅ 易于维护:修改按钮只需改 Button 组件
- ✅ 独立测试:可以单独测试 ProductCard
- ✅ 易于理解:每个组件职责单一
🎯 组件设计原则
1. 单一职责原则
// ❌ 坏的设计:一个组件做太多事
function UserProfile() {
// 获取用户数据
// 处理表单验证
// 上传头像
// 修改密码
// 显示订单历史
// ...
}
// ✅ 好的设计:拆分成多个组件
function UserProfile() {
return (
<div>
<UserAvatar />
<UserInfo />
<PasswordChange />
<OrderHistory />
</div>
);
}
2. 容器组件 vs 展示组件
// 展示组件(Presentational Component)- 只负责 UI
function UserList({ users, onUserClick }) {
return (
<ul>
{users.map(user => (
<li key={user.id} onClick={() => onUserClick(user)}>
{user.name}
</li>
))}
</ul>
);
}
// 容器组件(Container Component)- 负责数据和逻辑
function UserListContainer() {
const [users, setUsers] = React.useState([]);
React.useEffect(() => {
fetchUsers().then(setUsers);
}, []);
const handleUserClick = (user) => {
console.log('Clicked:', user);
};
return <UserList users={users} onUserClick={handleUserClick} />;
}
3. 组件组合(Composition)
// 使用 children 实现组件组合
function Card({ title, children, footer }) {
return (
<div className="card">
<div className="card-header">
<h3>{title}</h3>
</div>
<div className="card-body">
{children}
</div>
{footer && (
<div className="card-footer">
{footer}
</div>
)}
</div>
);
}
// 使用
function UserCard({ user }) {
return (
<Card
title={user.name}
footer={<Button>查看详情</Button>}
>
<p>邮箱: {user.email}</p>
<p>年龄: {user.age}</p>
</Card>
);
}
4. 高阶组件(HOC)
// 高阶组件:给组件添加额外功能
function withLoading(Component) {
return function WithLoadingComponent({ isLoading, ...props }) {
if (isLoading) {
return <div>加载中...</div>;
}
return <Component {...props} />;
};
}
// 使用
const UserListWithLoading = withLoading(UserList);
function App() {
const [users, setUsers] = React.useState([]);
const [isLoading, setIsLoading] = React.useState(true);
return (
<UserListWithLoading
users={users}
isLoading={isLoading}
/>
);
}
5. Render Props
// 使用 Render Props 共享逻辑
function MouseTracker({ render }) {
const [position, setPosition] = React.useState({ x: 0, y: 0 });
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
React.useEffect(() => {
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
return render(position);
}
// 使用
function App() {
return (
<MouseTracker
render={({ x, y }) => (
<div>鼠标位置: {x}, {y}</div>
)}
/>
);
}
🏗️ 组件库设计
设计一个 Button 组件库
// Button.jsx - 完整的按钮组件
import React from 'react';
import PropTypes from 'prop-types';
import './Button.css';
function Button({
children,
variant = 'primary',
size = 'medium',
disabled = false,
loading = false,
icon,
iconPosition = 'left',
onClick,
...rest
}) {
const className = [
'btn',
`btn-${variant}`,
`btn-${size}`,
disabled && 'btn-disabled',
loading && 'btn-loading',
].filter(Boolean).join(' ');
const handleClick = (e) => {
if (disabled || loading) return;
onClick?.(e);
};
return (
<button
className={className}
onClick={handleClick}
disabled={disabled || loading}
{...rest}
>
{loading && <span className="btn-spinner"></span>}
{!loading && icon && iconPosition === 'left' && (
<span className="btn-icon">{icon}</span>
)}
<span className="btn-text">{children}</span>
{!loading && icon && iconPosition === 'right' && (
<span className="btn-icon">{icon}</span>
)}
</button>
);
}
Button.propTypes = {
children: PropTypes.node.isRequired,
variant: PropTypes.oneOf(['primary', 'secondary', 'danger', 'ghost']),
size: PropTypes.oneOf(['small', 'medium', 'large']),
disabled: PropTypes.bool,
loading: PropTypes.bool,
icon: PropTypes.node,
iconPosition: PropTypes.oneOf(['left', 'right']),
onClick: PropTypes.func,
};
export default Button;
// 使用 Button 组件
function App() {
return (
<div>
<Button variant="primary">主要按钮</Button>
<Button variant="secondary" size="small">次要按钮</Button>
<Button variant="danger" icon="🗑️">删除</Button>
<Button loading>加载中</Button>
<Button disabled>禁用按钮</Button>
</div>
);
}
⚖️ 优缺点分析
✅ 优点
- 代码复用:组件可以在多处使用
- 易于维护:修改一个组件影响所有使用处
- 独立测试:可以单独测试组件
- 团队协作:不同人开发不同组件
- 可组合:小组件组合成大组件
❌ 缺点
- 学习成本:需要理解组件设计原则
- 过度拆分:可能导致组件过多
- props 传递:深层嵌套时 props 传递复杂
📋 何时使用组件化
✅ 适合使用的场景
- 任何 React/Vue 项目(必用)
- 需要代码复用
- 多人协作开发
- 需要独立测试
❌ 不适合使用的场景
- 静态 HTML 页面
- 一次性代码
模块化模式
💡 模式定义
模块化是将代码拆分成独立的模块,每个模块封装特定功能,通过导入导出进行交互。
🤔 为什么需要模块化?
问题场景:全局变量污染、依赖混乱
❌ 不使用模块化的痛点
<!-- index.html - 所有脚本都在全局 -->
<!DOCTYPE html>
<html>
<head>
<title>应用</title>
</head>
<body>
<div id="app"></div>
<!-- 脚本顺序很重要! -->
<script src="jquery.js"></script>
<script src="lodash.js"></script>
<script src="utils.js"></script>
<script src="api.js"></script>
<script src="app.js"></script>
</body>
</html>
// utils.js - 全局变量污染
var formatDate = function(date) {
// 格式化日期
};
var formatPrice = function(price) {
// 格式化价格
};
// 污染全局命名空间
// api.js - 依赖不明确
// 这个文件依赖 jQuery,但不知道
$.get('/api/users', function(users) {
// 使用 formatDate,但不知道从哪来
users.forEach(function(user) {
console.log(formatDate(user.createdAt));
});
});
// app.js - 依赖混乱
// 依赖 utils.js、api.js,但顺序错了会报错
function initApp() {
// 使用全局变量
var price = formatPrice(100);
}
initApp();
问题:
- 全局变量污染:所有变量都在全局
- 依赖不明确:不知道依赖哪些模块
- 顺序依赖:脚本加载顺序错误会报错
- 难以维护:修改一个文件可能影响其他文件
✅ 使用 ES6 模块化解决
// utils/date.js - 日期工具模块
export function formatDate(date, format = 'YYYY-MM-DD') {
// 格式化日期
return new Date(date).toLocaleDateString();
}
export function parseDate(dateStr) {
// 解析日期
return new Date(dateStr);
}
export function getDaysAgo(days) {
const date = new Date();
date.setDate(date.getDate() - days);
return date;
}
// utils/price.js - 价格工具模块
export function formatPrice(price, currency = '¥') {
return `${currency}${price.toFixed(2)}`;
}
export function calculateDiscount(price, discount) {
return price * (1 - discount);
}
// api/userApi.js - 用户 API 模块
import { formatDate } from '../utils/date.js';
export async function getUsers() {
const response = await fetch('/api/users');
const users = await response.json();
// 格式化日期
return users.map(user => ({
...user,
createdAt: formatDate(user.createdAt),
}));
}
export async function getUserById(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
export async function createUser(userData) {
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(userData),
});
return response.json();
}
// app.js - 主应用
import { getUsers } from './api/userApi.js';
import { formatPrice } from './utils/price.js';
async function initApp() {
// 依赖明确,不会污染全局
const users = await getUsers();
console.log('Users:', users);
const price = formatPrice(100);
console.log('Price:', price);
}
initApp();
<!-- index.html - 只需引入入口文件 -->
<!DOCTYPE html>
<html>
<head>
<title>应用</title>
</head>
<body>
<div id="app"></div>
<!-- 使用 type="module" -->
<script type="module" src="app.js"></script>
</body>
</html>
效果:
- ✅ 无全局污染:每个模块有自己的作用域
- ✅ 依赖明确:import 声明依赖关系
- ✅ 按需加载:只加载需要的模块
- ✅ 易于维护:模块职责单一
🎯 模块化规范对比
1. ES6 Module(推荐)
// 导出
export const PI = 3.14;
export function add(a, b) {
return a + b;
}
// 默认导出
export default class Calculator {
// ...
}
// 导入
import Calculator, { PI, add } from './calculator.js';
特点:
- 静态导入(编译时确定依赖)
- Tree Shaking(未使用的代码会被删除)
- 浏览器原生支持
2. CommonJS(Node.js)
// 导出
module.exports = {
PI: 3.14,
add: function(a, b) {
return a + b;
}
};
// 或者
exports.PI = 3.14;
exports.add = function(a, b) {
return a + b;
};
// 导入
const { PI, add } = require('./calculator');
特点:
- 动态导入(运行时加载)
- 同步加载
- Node.js 标准
3. AMD(已过时)
// 定义模块
define(['jquery', 'lodash'], function($, _) {
return {
init: function() {
// ...
}
};
});
// 使用模块
require(['app'], function(app) {
app.init();
});
特点:
- 异步加载
- 主要用于浏览器
- RequireJS 实现
🏗️ 模块组织最佳实践
按功能模块组织
src/
├── modules/
│ ├── auth/ # 认证模块
│ │ ├── api/
│ │ │ └── authApi.js
│ │ ├── components/
│ │ │ ├── LoginForm.jsx
│ │ │ └── RegisterForm.jsx
│ │ ├── hooks/
│ │ │ └── useAuth.js
│ │ └── index.js # 模块入口
│ │
│ ├── products/ # 商品模块
│ │ ├── api/
│ │ ├── components/
│ │ ├── hooks/
│ │ └── index.js
│ │
│ └── cart/ # 购物车模块
│ ├── api/
│ ├── components/
│ ├── hooks/
│ └── index.js
│
├── shared/ # 共享代码
│ ├── components/ # 通用组件
│ ├── hooks/ # 通用 Hooks
│ └── utils/ # 工具函数
│
└── App.jsx
模块入口文件
// modules/auth/index.js - 统一导出接口
export { default as LoginForm } from './components/LoginForm';
export { default as RegisterForm } from './components/RegisterForm';
export { useAuth } from './hooks/useAuth';
export * as authApi from './api/authApi';
// 在其他地方使用
import { LoginForm, useAuth, authApi } from './modules/auth';
⚖️ 优缺点分析
✅ 优点
- 避免全局污染:每个模块独立作用域
- 依赖明确:清晰的导入导出
- 按需加载:只加载需要的代码
- 易于维护:模块职责单一
- Tree Shaking:删除未使用代码
❌ 缺点
- 配置复杂:需要构建工具(Webpack、Vite)
- 兼容性:旧浏览器不支持 ES6 Module
- 学习成本:需要理解模块系统
Flux/Redux 单向数据流
💡 模式定义
Flux 是 Facebook 提出的应用架构,强调单向数据流。Redux 是 Flux 的实现,通过单一状态树管理应用状态。
🤔 为什么需要 Flux/Redux?
问题场景:复杂的状态管理
在大型应用中,组件间状态共享变得复杂:
❌ 不使用状态管理的痛点
// App.jsx - Props 层层传递(Props Drilling)
function App() {
const [user, setUser] = React.useState(null);
const [cart, setCart] = React.useState([]);
const [notifications, setNotifications] = React.useState([]);
return (
<div>
<Header
user={user}
cartCount={cart.length}
notifications={notifications}
onLogout={() => setUser(null)}
/>
<ProductList
user={user}
cart={cart}
onAddToCart={(product) => setCart([...cart, product])}
/>
<CartSidebar
cart={cart}
onRemove={(id) => setCart(cart.filter(p => p.id !== id))}
/>
</div>
);
}
// Header 需要传递给子组件
function Header({ user, cartCount, notifications, onLogout }) {
return (
<header>
<UserMenu user={user} onLogout={onLogout} />
<CartBadge count={cartCount} />
<Notifications items={notifications} />
</header>
);
}
// 层层传递,非常繁琐!
问题:
- Props Drilling:状态层层传递
- 难以追踪:不知道状态从哪来
- 难以维护:修改状态影响多个组件
- 状态同步:多个组件使用同一状态很困难
✅ 使用 Redux 解决
// store/store.js - 创建 Store
import { createStore, combineReducers } from 'redux';
import userReducer from './reducers/userReducer';
import cartReducer from './reducers/cartReducer';
import notificationsReducer from './reducers/notificationsReducer';
const rootReducer = combineReducers({
user: userReducer,
cart: cartReducer,
notifications: notificationsReducer,
});
const store = createStore(rootReducer);
export default store;
// store/reducers/cartReducer.js - Cart Reducer
const initialState = {
items: [],
};
function cartReducer(state = initialState, action) {
switch (action.type) {
case 'CART_ADD_ITEM':
return {
...state,
items: [...state.items, action.payload],
};
case 'CART_REMOVE_ITEM':
return {
...state,
items: state.items.filter(item => item.id !== action.payload),
};
case 'CART_CLEAR':
return {
...state,
items: [],
};
default:
return state;
}
}
export default cartReducer;
// store/actions/cartActions.js - Action Creators
export const addToCart = (product) => ({
type: 'CART_ADD_ITEM',
payload: product,
});
export const removeFromCart = (productId) => ({
type: 'CART_REMOVE_ITEM',
payload: productId,
});
export const clearCart = () => ({
type: 'CART_CLEAR',
});
// App.jsx - 使用 Provider
import { Provider } from 'react-redux';
import store from './store/store';
function App() {
return (
<Provider store={store}>
<Header />
<ProductList />
<CartSidebar />
</Provider>
);
}
export default App;
// components/ProductCard.jsx - 使用 useDispatch
import { useDispatch } from 'react-redux';
import { addToCart } from '../store/actions/cartActions';
function ProductCard({ product }) {
const dispatch = useDispatch();
const handleAddToCart = () => {
dispatch(addToCart(product));
};
return (
<div className="product-card">
<h3>{product.name}</h3>
<p>¥{product.price}</p>
<button onClick={handleAddToCart}>加入购物车</button>
</div>
);
}
// components/CartBadge.jsx - 使用 useSelector
import { useSelector } from 'react-redux';
function CartBadge() {
const cartCount = useSelector(state => state.cart.items.length);
return (
<div className="cart-badge">
🛒 {cartCount > 0 && <span>{cartCount}</span>}
</div>
);
}
效果:
- ✅ 单一数据源:所有状态在一个 Store
- ✅ 状态只读:只能通过 Action 修改
- ✅ 纯函数更新:Reducer 是纯函数
- ✅ 可预测:相同输入总是相同输出
- ✅ 易于调试:Redux DevTools 时间旅行
🎯 Redux 数据流
┌─────────┐
│ View │ ──dispatch──> ┌────────┐
└─────────┘ │ Action │
↑ └────────┘
│ │
│ ↓
│ ┌────────┐
│ │Reducer │
│ └────────┘
│ │
│ ↓
└───────subscribe── ┌────────┐
│ Store │
└────────┘
- View 触发 Action:用户点击按钮 → dispatch(addToCart(product))
- Action 传递给 Reducer:Reducer 接收 action 和当前 state
- Reducer 返回新 State:根据 action.type 计算新状态
- Store 更新:Store 保存新状态
- View 更新:订阅了 Store 的组件自动重新渲染
🏗️ Redux Toolkit(推荐)
Redux Toolkit 是官方推荐的 Redux 工具集,简化了 Redux 的使用:
// store/slices/cartSlice.js
import { createSlice } from '@reduxjs/toolkit';
const cartSlice = createSlice({
name: 'cart',
initialState: {
items: [],
},
reducers: {
addItem: (state, action) => {
// Immer 支持直接修改 state
state.items.push(action.payload);
},
removeItem: (state, action) => {
state.items = state.items.filter(item => item.id !== action.payload);
},
clearCart: (state) => {
state.items = [];
},
},
});
export const { addItem, removeItem, clearCart } = cartSlice.actions;
export default cartSlice.reducer;
// store/store.js
import { configureStore } from '@reduxjs/toolkit';
import cartReducer from './slices/cartSlice';
import userReducer from './slices/userSlice';
const store = configureStore({
reducer: {
cart: cartReducer,
user: userReducer,
},
});
export default store;
// 使用
import { useDispatch, useSelector } from 'react-redux';
import { addItem } from './store/slices/cartSlice';
function ProductCard({ product }) {
const dispatch = useDispatch();
const cartItems = useSelector(state => state.cart.items);
return (
<button onClick={() => dispatch(addItem(product))}>
加入购物车 ({cartItems.length})
</button>
);
}
⚖️ 优缺点分析
✅ 优点
- 单一数据源:状态集中管理
- 可预测:状态变化可追踪
- 易于调试:Redux DevTools
- 时间旅行:可以回溯状态
- 中间件支持:Redux Thunk、Redux Saga
❌ 缺点
- 样板代码多:Action、Reducer、Store
- 学习曲线:概念较多
- 小项目过度:简单应用不需要
- 性能开销:所有状态在一个对象
📋 何时使用 Redux
✅ 适合使用的场景
- 大型应用,状态复杂
- 多个组件共享状态
- 需要撤销/重做
- 需要状态持久化
- 需要时间旅行调试
❌ 不适合使用的场景
- 小型应用
- 状态简单,只在少数组件使用
- 使用 Context API 就够了
📝 总结
前端架构模式对比
| 模式 | 核心目的 | 使用场景 | 优先级 |
|---|---|---|---|
| MVC/MVVM | 分层架构 | 中大型应用 | ⭐⭐⭐⭐ |
| 组件化 | UI 复用 | 所有 React/Vue 项目 | ⭐⭐⭐⭐⭐ |
| 模块化 | 代码组织 | 所有项目 | ⭐⭐⭐⭐⭐ |
| Flux/Redux | 状态管理 | 复杂状态共享 | ⭐⭐⭐⭐ |
学习建议
- MVC/MVVM:理解框架设计思想,Vue 是天生的 MVVM
- 组件化:React/Vue 开发必备,掌握组件设计原则
- 模块化:ES6 Module 是标准,必须掌握
- Redux:大型应用必备,但小项目用 Context 就够
架构选型建议
小型项目(< 10 个页面)
- React + Hooks + Context API
- Vue 3 + Composition API
- 简单的目录结构
中型项目(10-50 个页面)
- React + Redux Toolkit
- Vue 3 + Pinia
- 按功能模块组织
大型项目(> 50 个页面)
- React + Redux Toolkit + TypeScript
- Vue 3 + Pinia + TypeScript
- 微前端架构
- Monorepo 管理
🎉 恭喜你完成所有设计模式的学习!
你已经掌握了:
- ✅ 12 个经典设计模式
- ✅ 4 个前端架构模式
- ✅ 40+ 个真实业务场景
- ✅ 180+ 个代码示例
下一步
- 实践:在实际项目中应用这些模式
- 重构:用设计模式重构现有代码
- 深入:阅读框架源码,理解模式应用
- 分享:教别人是最好的学习方式
记住:设计模式不是银弹,不要为了用模式而用模式。根据实际场景选择合适的模式,才能写出优雅、可维护的代码。
祝你在前端开发的道路上越走越远!🚀