04-前端架构模式

11 阅读16分钟

第四部分:前端架构模式

前端特有的架构模式,帮助你构建大型可维护的应用

目录


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 对比

特性MVCMVVM
数据流View → Controller → ModelView ↔ ViewModel ↔ Model
数据绑定手动更新自动双向绑定
典型框架Backbone.js、Angular 1.xVue、Angular 2+、Knockout
View 和逻辑通过 Controller 连接通过数据绑定连接
适用场景后端渲染、传统 Web 应用现代 SPA 应用

React 是什么架构?

React 不是严格的 MVC 或 MVVM,而是:

  • 组件化架构:一切皆组件
  • 单向数据流:数据从上到下流动
  • View 层库:专注于 UI 渲染

可以说 React 是 View 层 + Flux/Redux(数据流) 的组合。


⚖️ 优缺点分析

✅ 优点
  1. 分层清晰:职责分明,易于理解
  2. 易于测试:业务逻辑可独立测试
  3. 易于维护:修改一层不影响其他层
  4. 代码复用:Model 可以在多个 View 中复用
❌ 缺点
  1. 学习曲线:需要理解分层概念
  2. 代码量增加:简单应用也要分层
  3. 过度设计:小项目可能不需要

📋 何时使用 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>
  );
}

⚖️ 优缺点分析

✅ 优点
  1. 代码复用:组件可以在多处使用
  2. 易于维护:修改一个组件影响所有使用处
  3. 独立测试:可以单独测试组件
  4. 团队协作:不同人开发不同组件
  5. 可组合:小组件组合成大组件
❌ 缺点
  1. 学习成本:需要理解组件设计原则
  2. 过度拆分:可能导致组件过多
  3. 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';

⚖️ 优缺点分析

✅ 优点
  1. 避免全局污染:每个模块独立作用域
  2. 依赖明确:清晰的导入导出
  3. 按需加载:只加载需要的代码
  4. 易于维护:模块职责单一
  5. Tree Shaking:删除未使用代码
❌ 缺点
  1. 配置复杂:需要构建工具(Webpack、Vite)
  2. 兼容性:旧浏览器不支持 ES6 Module
  3. 学习成本:需要理解模块系统

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  │
                          └────────┘
  1. View 触发 Action:用户点击按钮 → dispatch(addToCart(product))
  2. Action 传递给 Reducer:Reducer 接收 action 和当前 state
  3. Reducer 返回新 State:根据 action.type 计算新状态
  4. Store 更新:Store 保存新状态
  5. 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>
  );
}

⚖️ 优缺点分析

✅ 优点
  1. 单一数据源:状态集中管理
  2. 可预测:状态变化可追踪
  3. 易于调试:Redux DevTools
  4. 时间旅行:可以回溯状态
  5. 中间件支持:Redux Thunk、Redux Saga
❌ 缺点
  1. 样板代码多:Action、Reducer、Store
  2. 学习曲线:概念较多
  3. 小项目过度:简单应用不需要
  4. 性能开销:所有状态在一个对象

📋 何时使用 Redux

✅ 适合使用的场景
  • 大型应用,状态复杂
  • 多个组件共享状态
  • 需要撤销/重做
  • 需要状态持久化
  • 需要时间旅行调试
❌ 不适合使用的场景
  • 小型应用
  • 状态简单,只在少数组件使用
  • 使用 Context API 就够了

📝 总结

前端架构模式对比

模式核心目的使用场景优先级
MVC/MVVM分层架构中大型应用⭐⭐⭐⭐
组件化UI 复用所有 React/Vue 项目⭐⭐⭐⭐⭐
模块化代码组织所有项目⭐⭐⭐⭐⭐
Flux/Redux状态管理复杂状态共享⭐⭐⭐⭐

学习建议

  1. MVC/MVVM:理解框架设计思想,Vue 是天生的 MVVM
  2. 组件化:React/Vue 开发必备,掌握组件设计原则
  3. 模块化:ES6 Module 是标准,必须掌握
  4. 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+ 个代码示例

下一步

  1. 实践:在实际项目中应用这些模式
  2. 重构:用设计模式重构现有代码
  3. 深入:阅读框架源码,理解模式应用
  4. 分享:教别人是最好的学习方式

记住:设计模式不是银弹,不要为了用模式而用模式。根据实际场景选择合适的模式,才能写出优雅、可维护的代码。

祝你在前端开发的道路上越走越远!🚀