JavaScript核心:项目实战从零构建任务管理系统
系列: 「全栈进化:大前端开发完全指南」系列第20篇
核心: 将JavaScript异步编程、事件循环等核心知识应用于实际项目开发
📌 引言
在前面的文章中,我们深入探讨了JavaScript中的异步编程技术,包括Promise、async/await等核心概念和高级应用。这些知识为我们提供了处理复杂异步操作的强大工具,但真正掌握一项技术的关键在于将其应用到实际项目中。
本文将带领你从零开始构建一个完整的任务管理系统,综合运用我们所学的JavaScript知识,特别是异步编程技术。通过这个项目,你将学习:
- 如何设计可扩展的前端应用架构
- 如何使用Promise和async/await处理异步API请求
- 如何实现高效的状态管理和数据流控制
- 如何构建响应式、交互友好的用户界面
- 如何优化应用性能和用户体验
无论你是前端开发新手还是有经验的开发者,这个实战项目都将帮助你将JavaScript异步编程的理论知识转化为实际开发能力,构建出专业、高效的Web应用。
📌 需求分析与项目规划
2.1 项目需求概述
我们要构建的任务管理系统需要满足以下核心需求:
- 用户认证:支持用户注册、登录和注销
- 任务管理:创建、查看、编辑和删除任务
- 任务分类:通过项目和标签对任务进行分类
- 任务状态:跟踪任务的完成状态(待办、进行中、已完成)
- 任务优先级:设置和显示任务的优先级(低、中、高)
- 截止日期:为任务设置截止日期并提供提醒
- 搜索和过滤:基于多种条件搜索和过滤任务
- 数据持久化:保存用户数据,支持多次访问
2.2 用户故事
为了更好地理解用户需求,我们定义了以下用户故事:
- 作为用户,我希望能够创建新任务,以便记录需要完成的工作
- 作为用户,我希望能够查看所有任务的列表,以便了解整体工作情况
- 作为用户,我希望能够编辑任务的详细信息,以便更新任务内容
- 作为用户,我希望能够将任务标记为已完成,以便跟踪进度
- 作为用户,我希望能够删除不再需要的任务,以便保持清单整洁
- 作为用户,我希望能够为任务设置优先级,以便关注重要任务
- 作为用户,我希望能够按照不同条件过滤任务,以便快速找到相关任务
- 作为用户,我希望系统能够保存我的任务数据,以便下次访问时继续使用
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 整体架构
我们采用分层架构设计,清晰划分不同的职责:
- UI层:负责界面渲染和用户交互
- 状态管理层:管理应用状态,协调数据流动
- 服务层:封装业务逻辑,处理复杂操作
- API层:处理与后端的通信,封装网络请求
- 工具层:提供通用功能支持
这种分层架构的优势在于:
- 关注点分离,每一层专注于自己的职责
- 代码复用,避免重复实现相似功能
- 易于测试,可以独立测试每一层
- 易于扩展,可以在不影响其他层的情况下修改某一层
3.2 数据流设计
我们采用单向数据流设计,使应用状态变化可预测:
- 用户操作:用户在UI上执行操作(如点击"添加任务"按钮)
- 触发动作:操作触发相应的动作(如
addTask
动作) - 状态更新:动作导致状态更新(如在任务列表中添加新任务)
- 视图更新:状态变化触发视图重新渲染,显示最新数据
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 组件化设计
我们采用组件化设计,每个组件有明确的职责和接口:
- 纯展示组件:只负责渲染UI,通过props接收数据和事件处理函数
- 容器组件:连接状态和展示组件,处理数据获取和状态更新
通过组件化设计,我们可以提高代码复用性,简化测试,并使应用结构更加清晰。
📌 核心功能实现
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>© 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 项目回顾
在这个任务管理系统项目中,我们从零开始完成了:
- 需求分析:明确了核心功能和用户需求
- 架构设计:设计了模块化的前端架构,实现数据与UI分离
- 核心功能实现:完成了任务的CRUD、分类、搜索、过滤等功能
- UI设计:打造了简洁直观的用户界面和良好的用户体验
- 性能优化:通过多种技术手段保证系统在大量任务时的流畅表现
- 测试与调试:确保系统稳定可靠运行
这个项目综合运用了我们前面学习的JavaScript核心知识,包括:
- 事件循环与异步编程
- Promise与async/await
- 原型链与面向对象设计
- 闭包与高阶函数
- DOM操作与事件处理
- 本地存储与数据持久化
7.2 扩展方向
这个任务管理系统还有很多可以扩展的方向:
- 用户认证与多用户支持:添加登录功能,支持多用户数据隔离
- 任务协作:实现任务分享和协作编辑功能
- 离线支持:使用Service Worker实现离线功能
- 数据同步:添加云端数据同步,确保多设备数据一致性
- 统计分析:添加任务完成率、时间分析等统计功能
- 导入导出:支持数据的导入导出,方便备份和迁移
- 自定义主题:允许用户自定义界面主题和布局
7.3 学习要点
通过这个项目,我们体验了一个完整的前端应用开发流程,锻炼了:
- 需求分析能力:如何将抽象需求转化为具体功能
- 架构设计思维:如何设计可扩展、可维护的前端架构
- 实现与优化技巧:如何编写高效、可靠的代码
- 用户体验考量:如何从用户角度优化产品体验
- 项目管理能力:如何规划和执行一个完整的开发流程
这些能力不仅适用于这个小型任务管理系统,也适用于更大规模的前端项目开发。
作者: 秦若宸 - 全栈工程师,擅长前端技术与架构设计,个人简历