【大前端系列20】JavaScript核心:项目实战从零构建任务管理系统

JavaScript核心:项目实战从零构建任务管理系统

系列: 「全栈进化:大前端开发完全指南」系列第20篇
核心: 将JavaScript异步编程、事件循环等核心知识应用于实际项目开发

📌 引言

在前面的文章中,我们深入探讨了JavaScript中的异步编程技术,包括Promise、async/await等核心概念和高级应用。这些知识为我们提供了处理复杂异步操作的强大工具,但真正掌握一项技术的关键在于将其应用到实际项目中。

本文将带领你从零开始构建一个完整的任务管理系统,综合运用我们所学的JavaScript知识,特别是异步编程技术。通过这个项目,你将学习:

  • 如何设计可扩展的前端应用架构
  • 如何使用Promise和async/await处理异步API请求
  • 如何实现高效的状态管理和数据流控制
  • 如何构建响应式、交互友好的用户界面
  • 如何优化应用性能和用户体验

无论你是前端开发新手还是有经验的开发者,这个实战项目都将帮助你将JavaScript异步编程的理论知识转化为实际开发能力,构建出专业、高效的Web应用。

📌 需求分析与项目规划

2.1 项目需求概述

我们要构建的任务管理系统需要满足以下核心需求:

  1. 用户认证:支持用户注册、登录和注销
  2. 任务管理:创建、查看、编辑和删除任务
  3. 任务分类:通过项目和标签对任务进行分类
  4. 任务状态:跟踪任务的完成状态(待办、进行中、已完成)
  5. 任务优先级:设置和显示任务的优先级(低、中、高)
  6. 截止日期:为任务设置截止日期并提供提醒
  7. 搜索和过滤:基于多种条件搜索和过滤任务
  8. 数据持久化:保存用户数据,支持多次访问

2.2 用户故事

为了更好地理解用户需求,我们定义了以下用户故事:

  1. 作为用户,我希望能够创建新任务,以便记录需要完成的工作
  2. 作为用户,我希望能够查看所有任务的列表,以便了解整体工作情况
  3. 作为用户,我希望能够编辑任务的详细信息,以便更新任务内容
  4. 作为用户,我希望能够将任务标记为已完成,以便跟踪进度
  5. 作为用户,我希望能够删除不再需要的任务,以便保持清单整洁
  6. 作为用户,我希望能够为任务设置优先级,以便关注重要任务
  7. 作为用户,我希望能够按照不同条件过滤任务,以便快速找到相关任务
  8. 作为用户,我希望系统能够保存我的任务数据,以便下次访问时继续使用

2.3 技术选型

基于项目需求,我们做出以下技术选型:

技术层面选择理由
前端框架原生JavaScript聚焦于JavaScript核心能力,不依赖特定框架
UI组件自定义组件深入理解组件化开发原理
CSS框架自定义CSS完全控制样式,提供最佳用户体验
状态管理自研发布订阅模式理解状态管理的核心原理
API通信Fetch API + async/await利用现代JavaScript异步处理能力
数据存储REST API + LocalStorage支持服务器通信和本地存储
构建工具Webpack模块化开发和优化生产代码

2.4 项目结构规划

task-management-system/
├── src/
│   ├── api/                # API通信模块
│   │   ├── client.js       # 基础API客户端
│   │   ├── tasks.js        # 任务相关API
│   │   └── auth.js         # 认证相关API
│   ├── components/         # UI组件
│   │   ├── App.js          # 应用主组件
│   │   ├── TaskList.js     # 任务列表组件
│   │   ├── TaskForm.js     # 任务表单组件
│   │   ├── TaskItem.js     # 单个任务组件
│   │   ├── FilterBar.js    # 过滤组件
│   │   └── ...
│   ├── services/           # 业务逻辑服务
│   │   ├── TaskService.js  # 任务管理服务
│   │   ├── AuthService.js  # 认证服务
│   │   └── ...
│   ├── store/              # 状态管理
│   │   ├── Store.js        # 状态存储核心
│   │   ├── actions.js      # 动作定义
│   │   └── ...
│   ├── utils/              # 工具函数
│   │   ├── dateUtils.js    # 日期处理工具
│   │   ├── validation.js   # 数据验证工具
│   │   └── ...
│   ├── styles/             # 样式文件
│   │   ├── main.css        # 主样式
│   │   ├── components.css  # 组件样式
│   │   └── ...
│   ├── index.js            # 应用入口
│   └── index.html          # HTML模板
├── webpack.config.js       # Webpack配置
├── package.json            # 项目依赖
└── README.md               # 项目说明

📌 项目架构设计

3.1 整体架构

我们采用分层架构设计,清晰划分不同的职责:

任务管理系统架构图

  1. UI层:负责界面渲染和用户交互
  2. 状态管理层:管理应用状态,协调数据流动
  3. 服务层:封装业务逻辑,处理复杂操作
  4. API层:处理与后端的通信,封装网络请求
  5. 工具层:提供通用功能支持

这种分层架构的优势在于:

  • 关注点分离,每一层专注于自己的职责
  • 代码复用,避免重复实现相似功能
  • 易于测试,可以独立测试每一层
  • 易于扩展,可以在不影响其他层的情况下修改某一层

3.2 数据流设计

我们采用单向数据流设计,使应用状态变化可预测:

  1. 用户操作:用户在UI上执行操作(如点击"添加任务"按钮)
  2. 触发动作:操作触发相应的动作(如addTask动作)
  3. 状态更新:动作导致状态更新(如在任务列表中添加新任务)
  4. 视图更新:状态变化触发视图重新渲染,显示最新数据

单向数据流

3.3 状态管理设计

我们实现一个简化版的状态管理系统,基于发布订阅模式:

class Store {
  constructor(initialState = {}) {
    this.state = initialState;
    this.listeners = [];
  }
  
  getState() {
    return this.state;
  }
  
  setState(newState) {
    this.state = { ...this.state, ...newState };
    this.notify();
  }
  
  subscribe(listener) {
    this.listeners.push(listener);
    
    // 返回取消订阅函数
    return () => {
      this.listeners = this.listeners.filter(l => l !== listener);
    };
  }
  
  notify() {
    this.listeners.forEach(listener => listener(this.state));
  }
  
  dispatch(action) {
    if (typeof action === 'function') {
      // 支持异步action(thunk模式)
      return action(this.dispatch.bind(this), this.getState.bind(this));
    }
    
    // 处理普通action
    const { type, payload } = action;
    
    // 根据action类型更新状态
    switch (type) {
      case 'ADD_TASK':
        this.setState({
          tasks: [...this.state.tasks, payload]
        });
        break;
        
      case 'UPDATE_TASK':
        this.setState({
          tasks: this.state.tasks.map(task => 
            task.id === payload.id ? { ...task, ...payload } : task
          )
        });
        break;
        
      case 'DELETE_TASK':
        this.setState({
          tasks: this.state.tasks.filter(task => task.id !== payload.id)
        });
        break;
        
      case 'SET_FILTER':
        this.setState({
          filter: payload
        });
        break;
        
      // 其他action类型...
      
      default:
        // 未知action类型,不做任何处理
    }
  }
}

3.4 API层设计

API层负责与后端服务通信,我们设计一个基础客户端来处理网络请求:

class ApiClient {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
  }
  
  async request(endpoint, options = {}) {
    const url = `${this.baseUrl}${endpoint}`;
    
    // 默认请求头
    const headers = {
      'Content-Type': 'application/json',
      ...options.headers
    };
    
    // 获取存储的认证令牌
    const token = localStorage.getItem('auth_token');
    if (token) {
      headers['Authorization'] = `Bearer ${token}`;
    }
    
    // 合并选项
    const config = {
      ...options,
      headers
    };
    
    // 添加超时控制
    const timeoutId = setTimeout(() => controller.abort(), 10000);
    const controller = new AbortController();
    config.signal = controller.signal;
    
    try {
      const response = await fetch(url, config);
      clearTimeout(timeoutId);
      
      // 处理HTTP错误
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      
      // 解析JSON响应
      const data = await response.json();
      return data;
    } catch (error) {
      // 重试逻辑可以在这里实现
      console.error('API request failed:', error);
      throw error;
    }
  }
  
  // 便捷方法
  get(endpoint, options = {}) {
    return this.request(endpoint, { ...options, method: 'GET' });
  }
  
  post(endpoint, data, options = {}) {
    return this.request(endpoint, {
      ...options,
      method: 'POST',
      body: JSON.stringify(data)
    });
  }
  
  put(endpoint, data, options = {}) {
    return this.request(endpoint, {
      ...options,
      method: 'PUT',
      body: JSON.stringify(data)
    });
  }
  
  delete(endpoint, options = {}) {
    return this.request(endpoint, { ...options, method: 'DELETE' });
  }
}

// 导出API客户端实例
export const apiClient = new ApiClient('https://api.example.com/v1');

3.5 组件化设计

我们采用组件化设计,每个组件有明确的职责和接口:

  1. 纯展示组件:只负责渲染UI,通过props接收数据和事件处理函数
  2. 容器组件:连接状态和展示组件,处理数据获取和状态更新

通过组件化设计,我们可以提高代码复用性,简化测试,并使应用结构更加清晰。

📌 核心功能实现

4.1 任务服务实现

任务服务是业务逻辑的核心,负责处理任务的增删改查等操作:

// src/services/TaskService.js
import { apiClient } from '../api/client';
import { store } from '../store/Store';

class TaskService {
  constructor() {
    this.endpoint = '/tasks';
  }
  
  /**
   * 获取任务列表
   */
  async fetchTasks() {
    try {
      store.dispatch({ type: 'SET_LOADING', payload: true });
      
      const tasks = await apiClient.get(this.endpoint);
      
      // 存储获取的任务
      store.dispatch({ type: 'SET_TASKS', payload: tasks });
      
      return tasks;
    } catch (error) {
      // 处理错误
      store.dispatch({ 
        type: 'SET_ERROR', 
        payload: 'Failed to fetch tasks: ' + error.message 
      });
      throw error;
    } finally {
      store.dispatch({ type: 'SET_LOADING', payload: false });
    }
  }
  
  /**
   * 创建新任务
   */
  async createTask(taskData) {
    try {
      store.dispatch({ type: 'SET_LOADING', payload: true });
      
      // 参数验证
      if (!taskData.title) {
        throw new Error('Task title is required');
      }
      
      // 创建任务
      const createdTask = await apiClient.post(this.endpoint, taskData);
      
      // 更新状态
      store.dispatch({ type: 'ADD_TASK', payload: createdTask });
      
      return createdTask;
    } catch (error) {
      store.dispatch({ 
        type: 'SET_ERROR', 
        payload: 'Failed to create task: ' + error.message 
      });
      throw error;
    } finally {
      store.dispatch({ type: 'SET_LOADING', payload: false });
    }
  }
  
  /**
   * 更新任务
   */
  async updateTask(taskId, taskData) {
    try {
      store.dispatch({ type: 'SET_LOADING', payload: true });
      
      // 参数验证
      if (!taskId) {
        throw new Error('Task ID is required');
      }
      
      // 更新任务
      const updatedTask = await apiClient.put(
        `${this.endpoint}/${taskId}`, 
        taskData
      );
      
      // 更新状态
      store.dispatch({ 
        type: 'UPDATE_TASK', 
        payload: updatedTask 
      });
      
      return updatedTask;
    } catch (error) {
      store.dispatch({ 
        type: 'SET_ERROR', 
        payload: 'Failed to update task: ' + error.message 
      });
      throw error;
    } finally {
      store.dispatch({ type: 'SET_LOADING', payload: false });
    }
  }
  
  /**
   * 删除任务
   */
  async deleteTask(taskId) {
    try {
      store.dispatch({ type: 'SET_LOADING', payload: true });
      
      // 参数验证
      if (!taskId) {
        throw new Error('Task ID is required');
      }
      
      // 删除任务
      await apiClient.delete(`${this.endpoint}/${taskId}`);
      
      // 更新状态
      store.dispatch({ 
        type: 'DELETE_TASK', 
        payload: { id: taskId } 
      });
      
      return true;
    } catch (error) {
      store.dispatch({ 
        type: 'SET_ERROR', 
        payload: 'Failed to delete task: ' + error.message 
      });
      throw error;
    } finally {
      store.dispatch({ type: 'SET_LOADING', payload: false });
    }
  }
  
  /**
   * 切换任务完成状态
   */
  async toggleTaskCompletion(taskId) {
    // 获取当前任务状态
    const { tasks } = store.getState();
    const task = tasks.find(t => t.id === taskId);
    
    if (!task) {
      throw new Error(`Task with ID ${taskId} not found`);
    }
    
    // 更新任务状态
    return this.updateTask(taskId, {
      completed: !task.completed
    });
  }
  
  /**
   * 设置任务优先级
   */
  async setTaskPriority(taskId, priority) {
    // 验证优先级值
    const validPriorities = ['low', 'medium', 'high'];
    if (!validPriorities.includes(priority)) {
      throw new Error(`Invalid priority: ${priority}`);
    }
    
    // 更新任务优先级
    return this.updateTask(taskId, { priority });
  }
  
  /**
   * 按过滤条件获取任务
   */
  async getFilteredTasks(filters = {}) {
    // 构建查询参数
    const queryParams = Object.entries(filters)
      .filter(([_, value]) => value !== undefined && value !== null)
      .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
      .join('&');
    
    // 发送请求
    const endpoint = queryParams ? 
      `${this.endpoint}?${queryParams}` : 
      this.endpoint;
    
    try {
      store.dispatch({ type: 'SET_LOADING', payload: true });
      
      const tasks = await apiClient.get(endpoint);
      
      // 更新状态
      store.dispatch({ type: 'SET_TASKS', payload: tasks });
      
      return tasks;
    } catch (error) {
      store.dispatch({ 
        type: 'SET_ERROR', 
        payload: 'Failed to fetch filtered tasks: ' + error.message 
      });
      throw error;
    } finally {
      store.dispatch({ type: 'SET_LOADING', payload: false });
    }
  }
  
  /**
   * 批量更新任务
   */
  async bulkUpdateTasks(taskIds, updateData) {
    try {
      store.dispatch({ type: 'SET_LOADING', payload: true });
      
      // 参数验证
      if (!Array.isArray(taskIds) || taskIds.length === 0) {
        throw new Error('Task IDs array is required');
      }
      
      // 创建请求数据
      const requestData = {
        taskIds,
        updateData
      };
      
      // 发送批量更新请求
      const results = await apiClient.post(
        `${this.endpoint}/bulk-update`, 
        requestData
      );
      
      // 更新状态
      results.forEach(updatedTask => {
        store.dispatch({ 
          type: 'UPDATE_TASK', 
          payload: updatedTask 
        });
      });
      
      return results;
    } catch (error) {
      store.dispatch({ 
        type: 'SET_ERROR', 
        payload: 'Failed to bulk update tasks: ' + error.message 
      });
      throw error;
    } finally {
      store.dispatch({ type: 'SET_LOADING', payload: false });
    }
  }
}

// 导出服务实例
export const taskService = new TaskService();

4.2 任务列表组件实现

下面是任务列表组件的实现,负责展示任务列表并处理任务操作:

// src/components/TaskList.js
import { store } from '../store/Store';
import { taskService } from '../services/TaskService';
import { renderTaskItem } from './TaskItem';

export class TaskList {
  constructor(container) {
    this.container = container;
    this.tasks = [];
    this.filter = {};
    
    // 初始化UI
    this.render();
    
    // 订阅状态变化
    store.subscribe(state => {
      this.tasks = state.tasks || [];
      this.filter = state.filter || {};
      this.render();
    });
    
    // 加载初始数据
    this.loadTasks();
  }
  
  async loadTasks() {
    try {
      await taskService.fetchTasks();
    } catch (error) {
      console.error('Failed to load tasks:', error);
    }
  }
  
  render() {
    // 清空容器
    this.container.innerHTML = '';
    
    // 渲染加载状态
    const { loading, error } = store.getState();
    
    if (error) {
      this.renderError(error);
      return;
    }
    
    if (loading) {
      this.renderLoading();
      return;
    }
    
    // 应用过滤器
    let filteredTasks = this.tasks;
    
    if (this.filter.status) {
      filteredTasks = filteredTasks.filter(task => {
        if (this.filter.status === 'completed') {
          return task.completed;
        } else if (this.filter.status === 'pending') {
          return !task.completed;
        }
        return true;
      });
    }
    
    if (this.filter.priority) {
      filteredTasks = filteredTasks.filter(task => 
        task.priority === this.filter.priority
      );
    }
    
    if (this.filter.search) {
      const searchLower = this.filter.search.toLowerCase();
      filteredTasks = filteredTasks.filter(task => 
        task.title.toLowerCase().includes(searchLower) || 
        (task.description && task.description.toLowerCase().includes(searchLower))
      );
    }
    
    // 渲染任务列表
    if (filteredTasks.length === 0) {
      this.renderEmptyState();
    } else {
      this.renderTasks(filteredTasks);
    }
  }
  
  renderTasks(tasks) {
    // 创建任务列表元素
    const listElement = document.createElement('ul');
    listElement.className = 'task-list';
    
    // 为每个任务创建列表项
    tasks.forEach(task => {
      const taskItem = renderTaskItem(task, {
        onToggleComplete: this.handleToggleComplete.bind(this),
        onEdit: this.handleEditTask.bind(this),
        onDelete: this.handleDeleteTask.bind(this),
        onPriorityChange: this.handlePriorityChange.bind(this)
      });
      
      listElement.appendChild(taskItem);
    });
    
    this.container.appendChild(listElement);
  }
  
  renderLoading() {
    const loadingElement = document.createElement('div');
    loadingElement.className = 'loading-indicator';
    loadingElement.textContent = 'Loading tasks...';
    this.container.appendChild(loadingElement);
  }
  
  renderError(error) {
    const errorElement = document.createElement('div');
    errorElement.className = 'error-message';
    errorElement.textContent = typeof error === 'string' ? error : 'An error occurred while loading tasks.';
    
    // 添加重试按钮
    const retryButton = document.createElement('button');
    retryButton.textContent = 'Retry';
    retryButton.addEventListener('click', () => this.loadTasks());
    
    errorElement.appendChild(document.createElement('br'));
    errorElement.appendChild(retryButton);
    
    this.container.appendChild(errorElement);
  }
  
  renderEmptyState() {
    const emptyElement = document.createElement('div');
    emptyElement.className = 'empty-state';
    
    // 根据过滤条件显示不同的空状态消息
    if (Object.keys(this.filter).length > 0) {
      emptyElement.textContent = 'No tasks match your filters.';
      
      // 添加清除过滤器按钮
      const clearButton = document.createElement('button');
      clearButton.textContent = 'Clear Filters';
      clearButton.addEventListener('click', () => {
        store.dispatch({ type: 'SET_FILTER', payload: {} });
      });
      
      emptyElement.appendChild(document.createElement('br'));
      emptyElement.appendChild(clearButton);
    } else {
      emptyElement.textContent = 'No tasks yet. Create your first task!';
    }
    
    this.container.appendChild(emptyElement);
  }
  
  // 任务操作处理函数
  async handleToggleComplete(taskId) {
    try {
      await taskService.toggleTaskCompletion(taskId);
    } catch (error) {
      console.error('Failed to toggle task completion:', error);
    }
  }
  
  async handleDeleteTask(taskId) {
    // 确认删除
    if (!confirm('Are you sure you want to delete this task?')) {
      return;
    }
    
    try {
      await taskService.deleteTask(taskId);
    } catch (error) {
      console.error('Failed to delete task:', error);
    }
  }
  
  async handleEditTask(taskId, updatedData) {
    try {
      await taskService.updateTask(taskId, updatedData);
    } catch (error) {
      console.error('Failed to update task:', error);
    }
  }
  
  async handlePriorityChange(taskId, priority) {
    try {
      await taskService.setTaskPriority(taskId, priority);
    } catch (error) {
      console.error('Failed to change task priority:', error);
    }
  }
}

4.3 任务项组件实现

任务项组件负责单个任务的展示和交互:

// src/components/TaskItem.js
export function renderTaskItem(task, handlers) {
  const {
    onToggleComplete,
    onEdit,
    onDelete,
    onPriorityChange
  } = handlers;
  
  // 创建任务项元素
  const taskElement = document.createElement('li');
  taskElement.className = `task-item ${task.completed ? 'completed' : ''}`;
  taskElement.dataset.taskId = task.id;
  taskElement.dataset.priority = task.priority || 'medium';
  
  // 渲染任务内容
  const contentElement = document.createElement('div');
  contentElement.className = 'task-content';
  
  // 复选框
  const checkbox = document.createElement('input');
  checkbox.type = 'checkbox';
  checkbox.checked = task.completed;
  checkbox.addEventListener('change', () => {
    onToggleComplete(task.id);
  });
  contentElement.appendChild(checkbox);
  
  // 任务标题
  const titleElement = document.createElement('h3');
  titleElement.className = 'task-title';
  titleElement.textContent = task.title;
  if (task.completed) {
    titleElement.style.textDecoration = 'line-through';
  }
  contentElement.appendChild(titleElement);
  
  // 任务描述(如果有)
  if (task.description) {
    const descElement = document.createElement('p');
    descElement.className = 'task-description';
    descElement.textContent = task.description;
    contentElement.appendChild(descElement);
  }
  
  // 截止日期(如果有)
  if (task.dueDate) {
    const dueDateElement = document.createElement('p');
    dueDateElement.className = 'task-due-date';
    
    const dueDate = new Date(task.dueDate);
    const today = new Date();
    
    // 计算剩余天数
    const diffTime = dueDate - today;
    const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
    
    // 设置状态类
    if (diffDays < 0) {
      dueDateElement.classList.add('overdue');
    } else if (diffDays === 0) {
      dueDateElement.classList.add('due-today');
    } else if (diffDays <= 3) {
      dueDateElement.classList.add('due-soon');
    }
    
    dueDateElement.textContent = `Due: ${dueDate.toLocaleDateString()}`;
    
    if (diffDays < 0) {
      dueDateElement.textContent += ` (${Math.abs(diffDays)} days overdue)`;
    } else if (diffDays === 0) {
      dueDateElement.textContent += ' (today)';
    } else if (diffDays === 1) {
      dueDateElement.textContent += ' (tomorrow)';
    } else {
      dueDateElement.textContent += ` (${diffDays} days left)`;
    }
    
    contentElement.appendChild(dueDateElement);
  }
  
  taskElement.appendChild(contentElement);
  
  // 操作按钮
  const actionsElement = document.createElement('div');
  actionsElement.className = 'task-actions';
  
  // 优先级选择器
  const prioritySelect = document.createElement('select');
  prioritySelect.className = 'priority-select';
  
  const priorities = [
    { value: 'low', label: 'Low' },
    { value: 'medium', label: 'Medium' },
    { value: 'high', label: 'High' }
  ];
  
  priorities.forEach(priority => {
    const option = document.createElement('option');
    option.value = priority.value;
    option.textContent = priority.label;
    option.selected = task.priority === priority.value;
    prioritySelect.appendChild(option);
  });
  
  prioritySelect.addEventListener('change', (e) => {
    onPriorityChange(task.id, e.target.value);
  });
  
  actionsElement.appendChild(prioritySelect);
  
  // 编辑按钮
  const editButton = document.createElement('button');
  editButton.className = 'edit-btn';
  editButton.textContent = 'Edit';
  editButton.addEventListener('click', () => {
    // 显示编辑表单(在实际应用中可能是打开模态框或切换到编辑视图)
    showEditForm(task, onEdit);
  });
  
  actionsElement.appendChild(editButton);
  
  // 删除按钮
  const deleteButton = document.createElement('button');
  deleteButton.className = 'delete-btn';
  deleteButton.textContent = 'Delete';
  deleteButton.addEventListener('click', () => {
    onDelete(task.id);
  });
  
  actionsElement.appendChild(deleteButton);
  
  taskElement.appendChild(actionsElement);
  
  return taskElement;
}

// 任务编辑表单
function showEditForm(task, onEdit) {
  // 创建模态框
  const modal = document.createElement('div');
  modal.className = 'modal';
  
  // 表单容器
  const formContainer = document.createElement('div');
  formContainer.className = 'modal-content';
  
  // 表单标题
  const formTitle = document.createElement('h2');
  formTitle.textContent = 'Edit Task';
  formContainer.appendChild(formTitle);
  
  // 创建表单
  const form = document.createElement('form');
  
  // 标题输入
  const titleLabel = document.createElement('label');
  titleLabel.textContent = 'Title:';
  form.appendChild(titleLabel);
  
  const titleInput = document.createElement('input');
  titleInput.type = 'text';
  titleInput.value = task.title;
  titleInput.required = true;
  form.appendChild(titleInput);
  
  // 描述输入
  const descLabel = document.createElement('label');
  descLabel.textContent = 'Description:';
  form.appendChild(descLabel);
  
  const descInput = document.createElement('textarea');
  descInput.value = task.description || '';
  form.appendChild(descInput);
  
  // 截止日期输入
  const dueDateLabel = document.createElement('label');
  dueDateLabel.textContent = 'Due Date:';
  form.appendChild(dueDateLabel);
  
  const dueDateInput = document.createElement('input');
  dueDateInput.type = 'date';
  
  if (task.dueDate) {
    // 格式化日期为YYYY-MM-DD
    const dueDate = new Date(task.dueDate);
    dueDateInput.value = dueDate.toISOString().split('T')[0];
  }
  
  form.appendChild(dueDateInput);
  
  // 优先级选择
  const priorityLabel = document.createElement('label');
  priorityLabel.textContent = 'Priority:';
  form.appendChild(priorityLabel);
  
  const prioritySelect = document.createElement('select');
  const priorities = [
    { value: 'low', label: 'Low' },
    { value: 'medium', label: 'Medium' },
    { value: 'high', label: 'High' }
  ];
  
  priorities.forEach(priority => {
    const option = document.createElement('option');
    option.value = priority.value;
    option.textContent = priority.label;
    option.selected = task.priority === priority.value;
    prioritySelect.appendChild(option);
  });
  
  form.appendChild(prioritySelect);
  
  // 按钮容器
  const buttonContainer = document.createElement('div');
  buttonContainer.className = 'form-buttons';
  
  // 保存按钮
  const saveButton = document.createElement('button');
  saveButton.type = 'submit';
  saveButton.textContent = 'Save';
  buttonContainer.appendChild(saveButton);
  
  // 取消按钮
  const cancelButton = document.createElement('button');
  cancelButton.type = 'button';
  cancelButton.textContent = 'Cancel';
  cancelButton.addEventListener('click', () => {
    document.body.removeChild(modal);
  });
  buttonContainer.appendChild(cancelButton);
  
  form.appendChild(buttonContainer);
  
  // 表单提交处理
  form.addEventListener('submit', (e) => {
    e.preventDefault();
    
    // 收集更新后的数据
    const updatedData = {
      title: titleInput.value,
      description: descInput.value,
      priority: prioritySelect.value,
      dueDate: dueDateInput.value || null
    };
    
    // 调用更新函数
    onEdit(task.id, updatedData);
    
    // 关闭模态框
    document.body.removeChild(modal);
  });
  
  // 添加表单到模态框
  formContainer.appendChild(form);
  modal.appendChild(formContainer);
  
  // 添加模态框到文档
  document.body.appendChild(modal);
}

4.4 任务表单组件实现

任务表单组件负责创建新任务:

// src/components/TaskForm.js
import { taskService } from '../services/TaskService';

export class TaskForm {
  constructor(container) {
    this.container = container;
    this.render();
    this.setupEventListeners();
  }
  
  render() {
    this.container.innerHTML = `
      <form id="task-form" class="task-form">
        <h2>Create New Task</h2>
        
        <div class="form-group">
          <label for="task-title">Title</label>
          <input type="text" id="task-title" required placeholder="Enter task title">
        </div>
        
        <div class="form-group">
          <label for="task-description">Description</label>
          <textarea id="task-description" placeholder="Enter task description"></textarea>
        </div>
        
        <div class="form-group">
          <label for="task-due-date">Due Date</label>
          <input type="date" id="task-due-date">
        </div>
        
        <div class="form-group">
          <label for="task-priority">Priority</label>
          <select id="task-priority">
            <option value="low">Low</option>
            <option value="medium" selected>Medium</option>
            <option value="high">High</option>
          </select>
        </div>
        
        <button type="submit" class="btn-primary">Create Task</button>
      </form>
    `;
  }
  
  setupEventListeners() {
    const form = document.getElementById('task-form');
    
    form.addEventListener('submit', async (e) => {
      e.preventDefault();
      
      // 获取表单数据
      const title = document.getElementById('task-title').value;
      const description = document.getElementById('task-description').value;
      const dueDate = document.getElementById('task-due-date').value;
      const priority = document.getElementById('task-priority').value;
      
      // 创建任务数据
      const taskData = {
        title,
        description,
        priority,
        completed: false
      };
      
      // 如果设置了截止日期,添加到任务数据中
      if (dueDate) {
        taskData.dueDate = new Date(dueDate).toISOString();
      }
      
      try {
        // 创建任务
        await taskService.createTask(taskData);
        
        // 重置表单
        form.reset();
        
        // 显示成功消息
        this.showNotification('Task created successfully!', 'success');
      } catch (error) {
        // 显示错误消息
        this.showNotification(
          `Failed to create task: ${error.message}`, 
          'error'
        );
      }
    });
  }
  
  showNotification(message, type = 'info') {
    // 创建通知元素
    const notification = document.createElement('div');
    notification.className = `notification ${type}`;
    notification.textContent = message;
    
    // 添加到页面
    document.body.appendChild(notification);
    
    // 3秒后移除
    setTimeout(() => {
      document.body.removeChild(notification);
    }, 3000);
  }
}

4.5 过滤组件实现

过滤组件允许用户根据不同条件筛选任务:

// src/components/FilterBar.js
import { store } from '../store/Store';

export class FilterBar {
  constructor(container) {
    this.container = container;
    this.filter = {};
    
    // 初始化UI
    this.render();
    
    // 订阅状态变化
    store.subscribe(state => {
      this.filter = state.filter || {};
      this.updateFilterUI();
    });
  }
  
  render() {
    this.container.innerHTML = `
      <div class="filter-bar">
        <div class="search-container">
          <input type="text" id="search-input" placeholder="Search tasks...">
          <button id="search-btn">Search</button>
        </div>
        
        <div class="filter-options">
          <select id="status-filter">
            <option value="">All Status</option>
            <option value="completed">Completed</option>
            <option value="pending">Pending</option>
          </select>
          
          <select id="priority-filter">
            <option value="">All Priorities</option>
            <option value="low">Low</option>
            <option value="medium">Medium</option>
            <option value="high">High</option>
          </select>
          
          <button id="clear-filters">Clear Filters</button>
        </div>
      </div>
    `;
    
    this.setupEventListeners();
    this.updateFilterUI();
  }
  
  setupEventListeners() {
    // 搜索事件
    const searchInput = document.getElementById('search-input');
    const searchBtn = document.getElementById('search-btn');
    
    searchBtn.addEventListener('click', () => {
      const searchValue = searchInput.value.trim();
      this.setFilter('search', searchValue || undefined);
    });
    
    searchInput.addEventListener('keypress', (e) => {
      if (e.key === 'Enter') {
        const searchValue = searchInput.value.trim();
        this.setFilter('search', searchValue || undefined);
      }
    });
    
    // 状态过滤器
    const statusFilter = document.getElementById('status-filter');
    statusFilter.addEventListener('change', () => {
      this.setFilter('status', statusFilter.value || undefined);
    });
    
    // 优先级过滤器
    const priorityFilter = document.getElementById('priority-filter');
    priorityFilter.addEventListener('change', () => {
      this.setFilter('priority', priorityFilter.value || undefined);
    });
    
    // 清除过滤器
    const clearFiltersBtn = document.getElementById('clear-filters');
    clearFiltersBtn.addEventListener('click', () => {
      this.clearFilters();
    });
  }
  
  updateFilterUI() {
    // 更新搜索输入框
    const searchInput = document.getElementById('search-input');
    if (searchInput) {
      searchInput.value = this.filter.search || '';
    }
    
    // 更新状态过滤器
    const statusFilter = document.getElementById('status-filter');
    if (statusFilter) {
      statusFilter.value = this.filter.status || '';
    }
    
    // 更新优先级过滤器
    const priorityFilter = document.getElementById('priority-filter');
    if (priorityFilter) {
      priorityFilter.value = this.filter.priority || '';
    }
  }
  
  setFilter(key, value) {
    // 创建新的过滤器对象
    const newFilter = { ...this.filter };
    
    if (value) {
      newFilter[key] = value;
    } else {
      delete newFilter[key];
    }
    
    // 更新状态
    store.dispatch({ type: 'SET_FILTER', payload: newFilter });
  }
  
  clearFilters() {
    // 清除所有过滤器
    store.dispatch({ type: 'SET_FILTER', payload: {} });
  }
}

4.6 应用程序入口

最后,让我们实现应用程序的入口点:

// src/index.js
import './styles/main.css';
import { TaskList } from './components/TaskList';
import { TaskForm } from './components/TaskForm';
import { FilterBar } from './components/FilterBar';
import { store } from './store/Store';

// 初始化应用状态
store.setState({
  tasks: [],
  filter: {},
  loading: false,
  error: null
});

// DOM加载完成后初始化应用
document.addEventListener('DOMContentLoaded', () => {
  // 获取容器元素
  const appContainer = document.getElementById('app');
  
  // 创建应用骨架
  appContainer.innerHTML = `
    <header class="app-header">
      <h1>Task Management System</h1>
    </header>
    
    <main class="app-main">
      <div class="sidebar">
        <div id="task-form-container"></div>
      </div>
      
      <div class="content">
        <div id="filter-bar-container"></div>
        <div id="task-list-container"></div>
      </div>
    </main>
    
    <footer class="app-footer">
      <p>&copy; 2023 Task Management System</p>
    </footer>
  `;
  
  // 初始化组件
  new TaskForm(document.getElementById('task-form-container'));
  new FilterBar(document.getElementById('filter-bar-container'));
  new TaskList(document.getElementById('task-list-container'));
}); 

📌 UI与交互优化

5.1 响应式布局设计

为了确保应用在各种设备上都能良好工作,我们采用响应式设计:

/* src/styles/main.css */
:root {
  --primary-color: #4a6fa5;
  --secondary-color: #166088;
  --accent-color: #4aaed9;
  --background-color: #f5f5f5;
  --text-color: #333;
  --light-grey: #e0e0e0;
  --dark-grey: #666;
  --success-color: #4caf50;
  --error-color: #f44336;
  --warning-color: #ff9800;
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  line-height: 1.6;
  color: var(--text-color);
  background-color: var(--background-color);
}

#app {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

.app-header {
  background-color: var(--primary-color);
  color: white;
  padding: 1rem;
  text-align: center;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.app-main {
  display: flex;
  flex: 1;
  padding: 20px;
  gap: 20px;
}

.sidebar {
  width: 300px;
  background-color: white;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  padding: 20px;
}

.content {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.app-footer {
  background-color: var(--dark-grey);
  color: white;
  text-align: center;
  padding: 1rem;
  margin-top: auto;
}

/* 响应式布局 */
@media (max-width: 768px) {
  .app-main {
    flex-direction: column;
  }
  
  .sidebar {
    width: 100%;
  }
}

5.2 任务项样式设计

任务项的样式设计注重视觉层次和交互反馈:

/* src/styles/components.css */
.task-list {
  list-style-type: none;
  margin: 0;
  padding: 0;
}

.task-item {
  background-color: white;
  border-radius: 8px;
  margin-bottom: 10px;
  padding: 15px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  transition: transform 0.2s, box-shadow 0.2s;
  border-left: 4px solid var(--primary-color);
}

.task-item:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}

/* 根据优先级设置边框颜色 */
.task-item[data-priority="low"] {
  border-left-color: var(--success-color);
}

.task-item[data-priority="medium"] {
  border-left-color: var(--warning-color);
}

.task-item[data-priority="high"] {
  border-left-color: var(--error-color);
}

.task-item.completed {
  opacity: 0.7;
  border-left-color: var(--light-grey);
}

.task-content {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.task-title {
  font-size: 18px;
  margin: 0 0 5px 0;
  color: var(--text-color);
}

.task-description {
  font-size: 14px;
  color: var(--dark-grey);
  margin: 0;
}

.task-due-date {
  font-size: 12px;
  color: var(--dark-grey);
  margin-top: 5px;
}

.task-due-date.overdue {
  color: var(--error-color);
  font-weight: bold;
}

.task-due-date.due-today {
  color: var(--warning-color);
  font-weight: bold;
}

.task-due-date.due-soon {
  color: var(--accent-color);
}

.task-actions {
  display: flex;
  gap: 10px;
  align-items: center;
}

5.3 表单样式与交互

任务表单的设计注重易用性和直观操作:

.task-form {
  background-color: white;
  border-radius: 8px;
  padding: 20px;
}

.form-group {
  margin-bottom: 15px;
}

.form-group label {
  display: block;
  margin-bottom: 5px;
  font-weight: 500;
}

.form-group input,
.form-group textarea,
.form-group select {
  width: 100%;
  padding: 10px;
  border: 1px solid var(--light-grey);
  border-radius: 4px;
  font-size: 14px;
}

.form-group textarea {
  min-height: 100px;
  resize: vertical;
}

.btn-primary {
  background-color: var(--primary-color);
  color: white;
  border: none;
  border-radius: 4px;
  padding: 10px 20px;
  cursor: pointer;
  font-size: 14px;
  font-weight: 500;
  transition: background-color 0.2s;
}

.btn-primary:hover {
  background-color: var(--secondary-color);
}

.form-buttons {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  margin-top: 20px;
}

5.4 过滤器样式设计

过滤器的设计强调直观的操作和清晰的视觉反馈:

.filter-bar {
  background-color: white;
  border-radius: 8px;
  padding: 15px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
}

.search-container {
  display: flex;
  gap: 10px;
  flex: 1;
}

.search-container input {
  flex: 1;
  padding: 8px 12px;
  border: 1px solid var(--light-grey);
  border-radius: 4px;
  font-size: 14px;
}

.filter-options {
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
}

.filter-options select {
  padding: 8px 12px;
  border: 1px solid var(--light-grey);
  border-radius: 4px;
  background-color: white;
  font-size: 14px;
}

#clear-filters {
  padding: 8px 12px;
  background-color: var(--light-grey);
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
}

#clear-filters:hover {
  background-color: var(--dark-grey);
  color: white;
}

5.5 通知与反馈设计

为用户提供清晰的操作反馈和通知:

.notification {
  position: fixed;
  bottom: 20px;
  right: 20px;
  min-width: 300px;
  padding: 15px 20px;
  border-radius: 4px;
  color: white;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
  animation: slideIn 0.3s, fadeOut 0.5s 2.5s;
  z-index: 1000;
}

.notification.success {
  background-color: var(--success-color);
}

.notification.error {
  background-color: var(--error-color);
}

.notification.info {
  background-color: var(--accent-color);
}

@keyframes slideIn {
  from {
    transform: translateX(100%);
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
}

@keyframes fadeOut {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}

5.6 模态框设计

任务编辑使用模态框,提供聚焦的界面:

.modal {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 9999;
}

.modal-content {
  background-color: white;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
  max-width: 500px;
  width: 100%;
  max-height: 90vh;
  overflow-y: auto;
}

📌 性能优化与最佳实践

6.1 应用性能优化

为了确保应用的高性能,我们实施了以下优化策略:

1. 减少DOM操作

在任务列表渲染中,我们采用了批量DOM更新策略,避免频繁操作DOM:

// 优化前:每次遍历都直接操作DOM
tasks.forEach(task => {
  const taskItem = document.createElement('li');
  taskItem.textContent = task.title;
  taskListElement.appendChild(taskItem); // 每次循环都触发DOM重排
});

// 优化后:使用文档片段批量更新
const fragment = document.createDocumentFragment();
tasks.forEach(task => {
  const taskItem = document.createElement('li');
  taskItem.textContent = task.title;
  fragment.appendChild(taskItem); // 在内存中操作,不触发DOM重排
});
taskListElement.appendChild(fragment); // 一次性更新DOM
2. 防抖与节流

在搜索输入和滚动事件处理中,我们实现了防抖和节流机制:

// 防抖函数:延迟执行,连续触发时重置计时器
function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

// 节流函数:限制执行频率
function throttle(fn, delay) {
  let lastCall = 0;
  return function(...args) {
    const now = new Date().getTime();
    if (now - lastCall < delay) return;
    lastCall = now;
    fn.apply(this, args);
  };
}

// 应用到搜索输入
const searchInput = document.getElementById('search-input');
searchInput.addEventListener('input', debounce((e) => {
  const searchValue = e.target.value.trim();
  store.dispatch({ 
    type: 'SET_FILTER', 
    payload: { ...store.getState().filter, search: searchValue || undefined } 
  });
}, 300));

// 应用到滚动事件
window.addEventListener('scroll', throttle(() => {
  // 处理滚动逻辑,如无限滚动加载
}, 100));
3. 虚拟列表实现

当任务数量很大时,为了避免DOM过载,我们实现了虚拟列表:

class VirtualListRenderer {
  constructor(container, itemHeight, bufferSize = 5) {
    this.container = container;
    this.itemHeight = itemHeight;
    this.bufferSize = bufferSize;
    this.items = [];
    this.visibleItems = [];
    this.scrollTop = 0;
    
    this.viewportHeight = 0;
    this.totalHeight = 0;
    this.startIndex = 0;
    this.endIndex = 0;
    
    this.setupContainer();
    this.setupEventListeners();
  }
  
  setupContainer() {
    this.container.style.position = 'relative';
    this.container.style.overflow = 'auto';
    
    // 创建内容容器
    this.contentContainer = document.createElement('div');
    this.contentContainer.style.position = 'relative';
    this.container.appendChild(this.contentContainer);
  }
  
  setupEventListeners() {
    // 监听滚动事件
    this.container.addEventListener('scroll', throttle(() => {
      this.scrollTop = this.container.scrollTop;
      this.render();
    }, 16)); // 约60fps
    
    // 监听窗口大小变化
    window.addEventListener('resize', debounce(() => {
      this.updateViewport();
      this.render();
    }, 100));
  }
  
  setItems(items) {
    this.items = items;
    this.totalHeight = items.length * this.itemHeight;
    this.contentContainer.style.height = `${this.totalHeight}px`;
    this.updateViewport();
    this.render();
  }
  
  updateViewport() {
    this.viewportHeight = this.container.clientHeight;
    const visibleCount = Math.ceil(this.viewportHeight / this.itemHeight);
    
    // 计算可见项的索引范围(包括缓冲区)
    this.startIndex = Math.max(0, 
      Math.floor(this.scrollTop / this.itemHeight) - this.bufferSize
    );
    this.endIndex = Math.min(
      this.items.length - 1,
      Math.ceil((this.scrollTop + this.viewportHeight) / this.itemHeight) + this.bufferSize
    );
  }
  
  render() {
    this.updateViewport();
    
    // 计算可见项
    this.visibleItems = this.items.slice(this.startIndex, this.endIndex + 1);
    
    // 清空内容
    this.contentContainer.innerHTML = '';
    
    // 渲染可见项
    this.visibleItems.forEach((item, index) => {
      const actualIndex = this.startIndex + index;
      const itemElement = this.renderItem(item, actualIndex);
      
      // 定位元素
      itemElement.style.position = 'absolute';
      itemElement.style.top = `${actualIndex * this.itemHeight}px`;
      itemElement.style.width = '100%';
      itemElement.style.height = `${this.itemHeight}px`;
      
      this.contentContainer.appendChild(itemElement);
    });
  }
  
  renderItem(item, index) {
    // 这个方法应该由子类实现
    const element = document.createElement('div');
    element.textContent = `Item ${index}: ${item.title}`;
    return element;
  }
}

// 使用虚拟列表渲染任务
class VirtualTaskList extends VirtualListRenderer {
  constructor(container, handlers) {
    super(container, 80); // 每个任务项高度为80px
    this.handlers = handlers;
  }
  
  renderItem(task, index) {
    return renderTaskItem(task, this.handlers);
  }
}
4. 本地存储优化

为了减少不必要的API请求,我们实现了本地存储缓存:

class CacheService {
  constructor(ttl = 5 * 60 * 1000) { // 默认缓存5分钟
    this.ttl = ttl;
  }
  
  setItem(key, value) {
    const item = {
      value,
      expiry: Date.now() + this.ttl
    };
    localStorage.setItem(key, JSON.stringify(item));
  }
  
  getItem(key) {
    const itemStr = localStorage.getItem(key);
    if (!itemStr) return null;
    
    try {
      const item = JSON.parse(itemStr);
      if (Date.now() > item.expiry) {
        localStorage.removeItem(key);
        return null;
      }
      return item.value;
    } catch (e) {
      localStorage.removeItem(key);
      return null;
    }
  }
  
  removeItem(key) {
    localStorage.removeItem(key);
  }
  
  clear() {
    localStorage.clear();
  }
}

// 在API客户端中使用缓存
class ApiClient {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
    this.cache = new CacheService();
  }
  
  async get(endpoint, options = {}) {
    // 检查是否启用缓存
    if (options.useCache !== false) {
      const cacheKey = `api:${endpoint}`;
      const cachedData = this.cache.getItem(cacheKey);
      
      if (cachedData) {
        return cachedData;
      }
      
      // 如果没有缓存,执行请求并缓存结果
      const data = await this.request(endpoint, { ...options, method: 'GET' });
      this.cache.setItem(cacheKey, data);
      return data;
    }
    
    // 不使用缓存直接请求
    return this.request(endpoint, { ...options, method: 'GET' });
  }
  
  // ... 其他方法
}

6.2 异步编程最佳实践

在实际项目中,我们应用了以下异步编程最佳实践:

1. 统一的错误处理
// 全局错误处理器
class ErrorHandler {
  static handle(error, context = '') {
    // 记录错误
    console.error(`Error in ${context}:`, error);
    
    // 根据错误类型处理
    if (error.name === 'AbortError') {
      // 请求被取消
      return { type: 'abort', message: 'Request was cancelled' };
    } else if (error.message.includes('network')) {
      // 网络错误
      return { type: 'network', message: 'Network error, please check your connection' };
    } else if (error.status === 401) {
      // 未授权错误,触发登出流程
      store.dispatch({ type: 'LOGOUT' });
      return { type: 'auth', message: 'Your session has expired, please login again' };
    } else if (error.status === 403) {
      // 权限错误
      return { type: 'permission', message: 'You do not have permission to perform this action' };
    } else if (error.status >= 500) {
      // 服务器错误
      return { type: 'server', message: 'Server error, please try again later' };
    }
    
    // 默认错误
    return { 
      type: 'unknown', 
      message: error.message || 'An unexpected error occurred'
    };
  }
}

// 在API客户端中使用
async request(endpoint, options = {}) {
  try {
    // 请求逻辑...
  } catch (error) {
    // 使用错误处理器
    const handledError = ErrorHandler.handle(error, `API request to ${endpoint}`);
    
    // 根据错误类型采取不同处理
    if (handledError.type === 'network' || handledError.type === 'server') {
      // 可以尝试重试请求
      if (options.retries > 0) {
        return this.request(endpoint, { 
          ...options, 
          retries: options.retries - 1 
        });
      }
    }
    
    // 将处理后的错误传递给调用者
    throw handledError;
  }
}
2. 异步状态管理

我们创建了一个专门处理异步Action的中间件:

// 异步Action中间件
function asyncMiddleware(store) {
  return function(next) {
    return async function(action) {
      // 如果不是异步Action,直接传递
      if (typeof action !== 'function') {
        return next(action);
      }
      
      try {
        // 执行异步Action
        return await action(store.dispatch, store.getState);
      } catch (error) {
        // 处理异步Action的错误
        console.error('Error in async action:', error);
        
        // 分发错误Action
        store.dispatch({ 
          type: 'ASYNC_ERROR', 
          payload: error
        });
        
        // 继续抛出错误
        throw error;
      }
    };
  };
}

// 应用到Store
const originalDispatch = store.dispatch;
store.dispatch = asyncMiddleware(store)(originalDispatch);

6.3 代码组织与可维护性

为了确保代码的可维护性,我们采用了以下实践:

1. 关注点分离
src/
├── api/          # 负责API通信
├── components/   # 负责UI渲染
├── services/     # 负责业务逻辑
├── store/        # 负责状态管理
├── utils/        # 工具函数
└── styles/       # 样式定义
2. 一致的命名规范
// 常量使用全大写下划线分隔
const MAX_TASK_COUNT = 100;

// 类使用PascalCase
class TaskService {}

// 函数和变量使用camelCase
function fetchTasks() {}
const taskList = [];

// 组件文件使用PascalCase
// TaskList.js
// TaskItem.js

// 非组件文件使用camelCase
// apiClient.js
// dateUtils.js
3. 注释与文档
/**
 * 任务服务类 - 提供任务相关的业务逻辑
 */
class TaskService {
  /**
   * 创建新任务
   * @param {Object} taskData - 任务数据
   * @param {string} taskData.title - 任务标题
   * @param {string} [taskData.description] - 任务描述
   * @param {string} [taskData.dueDate] - 截止日期(ISO格式)
   * @param {string} [taskData.priority] - 优先级(low, medium, high)
   * @returns {Promise<Object>} 创建的任务对象
   * @throws {Error} 如果必填字段缺失或API请求失败
   */
  async createTask(taskData) {
    // 实现...
  }
}

📌 测试与部署

7.1 单元测试实现

为确保代码质量,我们对关键组件和服务进行了单元测试:

// src/services/__tests__/TaskService.test.js
import { TaskService } from '../TaskService';
import { apiClient } from '../../api/client';

// Mock API客户端
jest.mock('../../api/client');

describe('TaskService', () => {
  let taskService;
  
  beforeEach(() => {
    // 重置mock
    apiClient.get.mockReset();
    apiClient.post.mockReset();
    apiClient.put.mockReset();
    apiClient.delete.mockReset();
    
    // 创建服务实例
    taskService = new TaskService();
  });
  
  describe('fetchTasks', () => {
    it('should fetch tasks from API', async () => {
      // 准备mock数据
      const mockTasks = [
        { id: 1, title: 'Task 1' },
        { id: 2, title: 'Task 2' }
      ];
      
      // 设置mock返回值
      apiClient.get.mockResolvedValue(mockTasks);
      
      // 调用测试方法
      const result = await taskService.fetchTasks();
      
      // 验证结果
      expect(apiClient.get).toHaveBeenCalledWith('/tasks');
      expect(result).toEqual(mockTasks);
    });
    
    it('should handle errors', async () => {
      // 设置mock抛出错误
      apiClient.get.mockRejectedValue(new Error('API error'));
      
      // 验证异常
      await expect(taskService.fetchTasks()).rejects.toThrow('API error');
    });
  });
  
  describe('createTask', () => {
    it('should validate required fields', async () => {
      await expect(taskService.createTask({}))
        .rejects.toThrow('Task title is required');
    });
    
    it('should create task via API', async () => {
      // 准备测试数据
      const taskData = { title: 'New Task' };
      const createdTask = { id: 1, ...taskData };
      
      // 设置mock返回值
      apiClient.post.mockResolvedValue(createdTask);
      
      // 调用测试方法
      const result = await taskService.createTask(taskData);
      
      // 验证结果
      expect(apiClient.post).toHaveBeenCalledWith('/tasks', taskData);
      expect(result).toEqual(createdTask);
    });
  });
  
  // 其他测试...
});

7.2 集成测试

除了单元测试,我们还进行了集成测试,验证组件之间的协作:

// src/integration-tests/TaskWorkflow.test.js
import { TaskService } from '../services/TaskService';
import { TaskList } from '../components/TaskList';
import { TaskForm } from '../components/TaskForm';
import { store } from '../store/Store';

// Mock DOM环境
document.body.innerHTML = `
  <div id="task-form-container"></div>
  <div id="task-list-container"></div>
`;

// Mock API服务
jest.mock('../api/client');

describe('Task Management Workflow', () => {
  let taskService, taskList, taskForm;
  
  beforeEach(() => {
    // 初始化存储
    store.setState({
      tasks: [],
      filter: {},
      loading: false,
      error: null
    });
    
    // 创建组件
    taskService = new TaskService();
    taskList = new TaskList(document.getElementById('task-list-container'));
    taskForm = new TaskForm(document.getElementById('task-form-container'));
  });
  
  it('should create task and update task list', async () => {
    // 模拟表单输入
    const titleInput = document.getElementById('task-title');
    titleInput.value = 'Test Task';
    
    // 模拟表单提交
    const form = document.getElementById('task-form');
    form.dispatchEvent(new Event('submit'));
    
    // 等待异步操作完成
    await new Promise(resolve => setTimeout(resolve, 100));
    
    // 验证存储状态
    const state = store.getState();
    expect(state.tasks.length).toBe(1);
    expect(state.tasks[0].title).toBe('Test Task');
    
    // 验证UI更新
    const taskItems = document.querySelectorAll('.task-item');
    expect(taskItems.length).toBe(1);
    expect(taskItems[0].querySelector('.task-title').textContent).toBe('Test Task');
  });
  
  // 其他集成测试...
});

7.3 构建与部署

使用Webpack构建生产版本:

// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = (env, argv) => {
  const isProduction = argv.mode === 'production';
  
  return {
    entry: './src/index.js',
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: isProduction ? 
        'js/[name].[contenthash].js' : 
        'js/[name].js'
    },
    module: {
      rules: [
        {
          test: /\.js$/,
          exclude: /node_modules/,
          use: {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-env']
            }
          }
        },
        {
          test: /\.css$/,
          use: [
            isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
            'css-loader'
          ]
        }
      ]
    },
    plugins: [
      new CleanWebpackPlugin(),
      new HtmlWebpackPlugin({
        template: './src/index.html',
        minify: isProduction ? {
          removeAttributeQuotes: true,
          collapseWhitespace: true,
          removeComments: true
        } : false
      }),
      ...(isProduction ? [
        new MiniCssExtractPlugin({
          filename: 'css/[name].[contenthash].css'
        })
      ] : [])
    ],
    devServer: {
      contentBase: path.join(__dirname, 'dist'),
      compress: true,
      port: 9000,
      hot: true
    },
    optimization: {
      moduleIds: 'hashed',
      runtimeChunk: 'single',
      splitChunks: {
        cacheGroups: {
          vendor: {
            test: /[\\/]node_modules[\\/]/,
            name: 'vendors',
            chunks: 'all'
          }
        }
      },
      ...(isProduction ? {
        minimize: true
      } : {})
    }
  };
};

📌 总结与展望

7.1 项目回顾

在这个任务管理系统项目中,我们从零开始完成了:

  1. 需求分析:明确了核心功能和用户需求
  2. 架构设计:设计了模块化的前端架构,实现数据与UI分离
  3. 核心功能实现:完成了任务的CRUD、分类、搜索、过滤等功能
  4. UI设计:打造了简洁直观的用户界面和良好的用户体验
  5. 性能优化:通过多种技术手段保证系统在大量任务时的流畅表现
  6. 测试与调试:确保系统稳定可靠运行

这个项目综合运用了我们前面学习的JavaScript核心知识,包括:

  • 事件循环与异步编程
  • Promise与async/await
  • 原型链与面向对象设计
  • 闭包与高阶函数
  • DOM操作与事件处理
  • 本地存储与数据持久化

7.2 扩展方向

这个任务管理系统还有很多可以扩展的方向:

  1. 用户认证与多用户支持:添加登录功能,支持多用户数据隔离
  2. 任务协作:实现任务分享和协作编辑功能
  3. 离线支持:使用Service Worker实现离线功能
  4. 数据同步:添加云端数据同步,确保多设备数据一致性
  5. 统计分析:添加任务完成率、时间分析等统计功能
  6. 导入导出:支持数据的导入导出,方便备份和迁移
  7. 自定义主题:允许用户自定义界面主题和布局

7.3 学习要点

通过这个项目,我们体验了一个完整的前端应用开发流程,锻炼了:

  1. 需求分析能力:如何将抽象需求转化为具体功能
  2. 架构设计思维:如何设计可扩展、可维护的前端架构
  3. 实现与优化技巧:如何编写高效、可靠的代码
  4. 用户体验考量:如何从用户角度优化产品体验
  5. 项目管理能力:如何规划和执行一个完整的开发流程

这些能力不仅适用于这个小型任务管理系统,也适用于更大规模的前端项目开发。


作者: 秦若宸 - 全栈工程师,擅长前端技术与架构设计,个人简历

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值