概览
交互式Todo列表应用是一个经典的入门级实战项目,旨在综合运用jQuery的核心功能。本项目将实现任务的添加、删除、编辑、状态切换以及本地数据持久化。通过构建这个应用,可以深入理解jQuery选择器、DOM操作、事件处理、数据存储以及动画效果的综合应用,体会如何用简洁的代码构建完整的交互功能。jQuery在其中扮演了简化DOM操作和事件管理的核心角色,使开发者能够专注于业务逻辑的实现。
项目功能模块
一个完整的交互式Todo列表通常包含以下核心功能模块。
| 功能模块 | 描述 | 涉及的核心jQuery方法 |
|---|---|---|
| 任务展示 | 将任务列表动态渲染到页面上 | $(container).html()、$.each() |
| 添加任务 | 通过输入框和按钮向列表中添加新任务 | $(input).val()、$(container).append() |
| 删除任务 | 删除单个任务,可配合动画效果 | $(element).remove()、.fadeOut() |
| 编辑任务 | 双击任务文本进入编辑模式,回车或失焦保存 | $(element).on('dblclick')、.replaceWith() |
| 状态切换 | 通过复选框标记任务为已完成或未完成,并更新样式 | $(checkbox).on('change')、.toggleClass() |
| 本地存储 | 使用 localStorage 保存任务数据,页面刷新后自动加载 | JSON.stringify()、JSON.parse() |
| 批量操作 | 一键标记所有任务为已完成,或清除所有已完成任务 | $.each()、$(selector).prop()、.remove() |
项目初始化
构建Todo应用的第一步是搭建基础的HTML结构,并引入jQuery库。
示例:HTML结构与引入jQuery
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>jQuery 交互式 Todo 列表</title>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<style>
body { font-family: Arial, sans-serif; max-width: 500px; margin: 20px auto; }
#todo-app { border: 1px solid #ddd; padding: 20px; border-radius: 5px; }
#new-task { width: 70%; padding: 8px; margin-right: 5px; }
#add-btn { padding: 8px 15px; cursor: pointer; }
#task-list { list-style: none; padding: 0; margin-top: 20px; }
#task-list li { padding: 10px; border-bottom: 1px solid #eee; display: flex; align-items: center; }
#task-list li.completed span { text-decoration: line-through; color: #888; }
.task-checkbox { margin-right: 10px; }
.task-text { flex-grow: 1; cursor: pointer; }
.delete-btn { background: red; color: white; border: none; padding: 5px 10px; cursor: pointer; border-radius: 3px; }
.edit-input { flex-grow: 1; padding: 5px; margin-right: 5px; }
.save-btn { background: green; color: white; border: none; padding: 5px 10px; cursor: pointer; border-radius: 3px; }
</style>
</head>
<body>
<div id="todo-app">
<h2>我的任务清单</h2>
<div>
<input type="text" id="new-task" placeholder="添加新任务...">
<button id="add-btn">添加</button>
<button id="mark-all-btn">全部完成</button>
<button id="clear-completed-btn">清除已完成</button>
</div>
<ul id="task-list">
<!-- 任务列表将由 jQuery 动态生成 -->
</ul>
</div>
<script>
// jQuery 代码将在此处编写
</script>
</body>
</html>任务数据管理
使用JavaScript数组存储任务对象,每个对象包含 id、text 和 completed 属性。id 用于唯一标识每个任务,便于后续的删除和更新操作。
语法
let tasks = [
{ id: 1, text: '学习 jQuery', completed: false },
{ id: 2, text: '编写 Todo 应用', completed: true }
];渲染任务列表
封装一个 renderTasks 函数,根据 tasks 数组动态生成HTML并插入到 #task-list 中。每次数据变化(添加、删除、切换状态)后调用此函数刷新列表。
示例
<script>
let tasks = [
{ id: 1, text: '学习 jQuery 选择器', completed: false },
{ id: 2, text: '练习事件处理', completed: true },
{ id: 3, text: '实现本地存储', completed: false }
];
function renderTasks() {
const $taskList = $('#task-list');
$taskList.empty(); // 清空列表
$.each(tasks, function(index, task) {
const listItem = $('<li>').attr('data-id', task.id);
if (task.completed) {
listItem.addClass('completed');
}
const checkbox = $('<input>', {
type: 'checkbox',
class: 'task-checkbox',
prop: { checked: task.completed }
});
const taskText = $('<span>', {
class: 'task-text',
text: task.text
});
const deleteBtn = $('<button>', {
class: 'delete-btn',
text: '删除'
});
listItem.append(checkbox, taskText, deleteBtn);
$taskList.append(listItem);
});
}
renderTasks(); // 初始渲染
</script>添加新任务
监听“添加”按钮的点击事件,获取输入框的值,创建新任务对象并添加到 tasks 数组,然后重新渲染列表并清空输入框。
示例
<script>
// ... 前面的代码保持不变
$('#add-btn').on('click', function() {
const newTaskText = $('#new-task').val().trim();
if (newTaskText === '') {
alert('任务内容不能为空!');
return;
}
const newTask = {
id: Date.now(), // 使用时间戳作为临时ID
text: newTaskText,
completed: false
};
tasks.push(newTask);
renderTasks();
$('#new-task').val('').focus(); // 清空输入并获取焦点
});
// 支持回车键添加
$('#new-task').on('keypress', function(e) {
if (e.which === 13) { // 回车键
$('#add-btn').trigger('click');
}
});
</script>删除任务
由于任务列表是动态生成的,需要使用事件委托将删除事件绑定到其父元素 #task-list 上。点击删除按钮时,获取对应 li 的 data-id 属性,从 tasks 数组中移除该任务,并重新渲染列表。可配合淡出动画提升体验。
示例
<script>
// ... 前面的代码保持不变
$('#task-list').on('click', '.delete-btn', function() {
const $listItem = $(this).closest('li');
const taskId = $listItem.data('id');
// 添加淡出动画后移除
$listItem.fadeOut(300, function() {
tasks = tasks.filter(function(task) {
return task.id !== taskId;
});
renderTasks(); // 重新渲染以恢复显示(但数据已删除)
});
});
</script>切换任务状态
通过事件委托监听复选框的 change 事件,更新对应任务的 completed 属性,并切换父 li 的 completed 类以更新样式。
示例
<script>
// ... 前面的代码保持不变
$('#task-list').on('change', '.task-checkbox', function() {
const $listItem = $(this).closest('li');
const taskId = $listItem.data('id');
const isChecked = $(this).prop('checked');
tasks = tasks.map(function(task) {
if (task.id === taskId) {
task.completed = isChecked;
}
return task;
});
$listItem.toggleClass('completed', isChecked);
// 注意:这里直接修改了样式,没有重新渲染全部,以提高性能
// 但为了保持数据一致,也可以调用 renderTasks()
});
</script>编辑任务
双击任务文本时,将其替换为一个输入框和保存按钮,允许用户修改任务内容。保存时更新数据并重新渲染该项。
示例
<script>
// ... 前面的代码保持不变
$('#task-list').on('dblclick', '.task-text', function() {
const $listItem = $(this).closest('li');
const taskId = $listItem.data('id');
const currentText = $(this).text();
// 找到对应的任务对象
const task = tasks.find(t => t.id === taskId);
if (!task) return;
// 创建编辑界面
const $editInput = $('<input>', {
type: 'text',
class: 'edit-input',
value: currentText
});
const $saveBtn = $('<button>', {
class: 'save-btn',
text: '保存'
});
// 临时替换内容
$listItem.find('.task-text').replaceWith($editInput);
$listItem.find('.delete-btn').before($saveBtn); // 在删除按钮前插入保存按钮
$editInput.focus();
});
// 保存编辑(通过事件委托监听保存按钮)
$('#task-list').on('click', '.save-btn', function() {
const $listItem = $(this).closest('li');
const taskId = $listItem.data('id');
const $editInput = $listItem.find('.edit-input');
const newText = $editInput.val().trim();
if (newText === '') {
alert('任务内容不能为空!');
return;
}
tasks = tasks.map(function(task) {
if (task.id === taskId) {
task.text = newText;
}
return task;
});
renderTasks(); // 重新渲染整个列表以恢复显示
});
// 保存编辑(通过回车键)
$('#task-list').on('keypress', '.edit-input', function(e) {
if (e.which === 13) {
$(this).siblings('.save-btn').trigger('click');
}
});
// 失焦保存(可选,但注意与保存按钮的冲突处理)
// $('#task-list').on('blur', '.edit-input', function() {
// $(this).siblings('.save-btn').trigger('click');
// });
</script>本地存储
利用 localStorage 实现数据的持久化。在添加、删除、编辑、切换状态后,将 tasks 数组保存到 localStorage。页面加载时,从 localStorage 读取数据并初始化。
示例
<script>
// 封装存储函数
function saveTasksToLocalStorage() {
localStorage.setItem('todoTasks', JSON.stringify(tasks));
}
// 封装加载函数
function loadTasksFromLocalStorage() {
const storedTasks = localStorage.getItem('todoTasks');
if (storedTasks) {
tasks = JSON.parse(storedTasks);
} else {
tasks = []; // 默认空数组
}
renderTasks();
}
// 修改所有涉及 tasks 变化的地方,在变化后调用 saveTasksToLocalStorage()
// 例如在添加任务后:
$('#add-btn').on('click', function() {
// ... 添加逻辑 ...
saveTasksToLocalStorage();
});
// 在删除任务后:
$('#task-list').on('click', '.delete-btn', function() {
$listItem.fadeOut(300, function() {
// ... 删除逻辑 ...
saveTasksToLocalStorage();
renderTasks(); // 重新渲染
});
});
// 在切换状态后:
$('#task-list').on('change', '.task-checkbox', function() {
// ... 更新逻辑 ...
saveTasksToLocalStorage();
});
// 在编辑保存后:
$('#task-list').on('click', '.save-btn', function() {
// ... 编辑逻辑 ...
saveTasksToLocalStorage();
renderTasks();
});
// 初始化时加载
loadTasksFromLocalStorage();
</script>批量操作
实现“全部完成”和“清除已完成”两个批量操作功能。
全部完成
将所有任务的 completed 属性设置为 true,更新本地存储并重新渲染。
清除已完成
从 tasks 数组中过滤掉所有 completed 为 true 的任务,更新本地存储并重新渲染。
示例
<script>
$('#mark-all-btn').on('click', function() {
tasks = tasks.map(function(task) {
task.completed = true;
return task;
});
saveTasksToLocalStorage();
renderTasks();
});
$('#clear-completed-btn').on('click', function() {
tasks = tasks.filter(function(task) {
return !task.completed;
});
saveTasksToLocalStorage();
renderTasks();
});
</script>完整项目代码
将上述所有功能整合,形成一个完整的交互式Todo列表应用。以下为整合后的完整HTML文件。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>jQuery 交互式 Todo 列表 - 完整版</title>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<style>
body { font-family: Arial, sans-serif; max-width: 500px; margin: 20px auto; background: #f5f5f5; }
#todo-app { background: white; border: 1px solid #ddd; padding: 20px; border-radius: 5px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
h2 { margin-top: 0; color: #333; }
.input-area { display: flex; gap: 5px; margin-bottom: 15px; flex-wrap: wrap; }
#new-task { flex: 1; padding: 8px; border: 1px solid #ccc; border-radius: 3px; min-width: 200px; }
.btn { padding: 8px 15px; border: none; border-radius: 3px; cursor: pointer; background: #007bff; color: white; }
.btn:hover { opacity: 0.9; }
#mark-all-btn { background: #28a745; }
#clear-completed-btn { background: #dc3545; }
#task-list { list-style: none; padding: 0; margin: 0; }
#task-list li { padding: 10px; border-bottom: 1px solid #eee; display: flex; align-items: center; animation: fadeIn 0.3s; }
#task-list li:last-child { border-bottom: none; }
#task-list li.completed .task-text { text-decoration: line-through; color: #888; }
.task-checkbox { margin-right: 10px; cursor: pointer; }
.task-text { flex-grow: 1; cursor: pointer; padding: 5px; }
.delete-btn { background: #dc3545; color: white; border: none; padding: 5px 10px; cursor: pointer; border-radius: 3px; margin-left: 5px; }
.edit-input { flex-grow: 1; padding: 5px; margin-right: 5px; border: 1px solid #ccc; border-radius: 3px; }
.save-btn { background: #28a745; color: white; border: none; padding: 5px 10px; cursor: pointer; border-radius: 3px; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
</style>
</head>
<body>
<div id="todo-app">
<h2>📋 我的任务清单</h2>
<div class="input-area">
<input type="text" id="new-task" placeholder="添加新任务...">
<button id="add-btn" class="btn">添加</button>
<button id="mark-all-btn" class="btn">全部完成</button>
<button id="clear-completed-btn" class="btn">清除已完成</button>
</div>
<ul id="task-list"></ul>
</div>
<script>
let tasks = [];
function saveTasksToLocalStorage() {
localStorage.setItem('todoTasks', JSON.stringify(tasks));
}
function loadTasksFromLocalStorage() {
const storedTasks = localStorage.getItem('todoTasks');
if (storedTasks) {
tasks = JSON.parse(storedTasks);
} else {
tasks = [];
}
renderTasks();
}
function renderTasks() {
const $taskList = $('#task-list');
$taskList.empty();
$.each(tasks, function(index, task) {
const listItem = $('<li>').attr('data-id', task.id).addClass(task.completed ? 'completed' : '');
const checkbox = $('<input>', { type: 'checkbox', class: 'task-checkbox', prop: { checked: task.completed } });
const taskText = $('<span>', { class: 'task-text', text: task.text });
const deleteBtn = $('<button>', { class: 'delete-btn', text: '删除' });
listItem.append(checkbox, taskText, deleteBtn);
$taskList.append(listItem);
});
}
// 添加任务
$('#add-btn').on('click', function() {
const newTaskText = $('#new-task').val().trim();
if (newTaskText === '') {
alert('任务内容不能为空!');
return;
}
const newTask = { id: Date.now(), text: newTaskText, completed: false };
tasks.push(newTask);
saveTasksToLocalStorage();
renderTasks();
$('#new-task').val('').focus();
});
// 回车添加
$('#new-task').on('keypress', function(e) {
if (e.which === 13) $('#add-btn').trigger('click');
});
// 删除任务(带动画)
$('#task-list').on('click', '.delete-btn', function() {
const $listItem = $(this).closest('li');
const taskId = $listItem.data('id');
$listItem.fadeOut(300, function() {
tasks = tasks.filter(task => task.id !== taskId);
saveTasksToLocalStorage();
renderTasks(); // 重新渲染(但动画效果已过,此处为保持数据一致)
});
});
// 切换任务状态
$('#task-list').on('change', '.task-checkbox', function() {
const $listItem = $(this).closest('li');
const taskId = $listItem.data('id');
const isChecked = $(this).prop('checked');
tasks = tasks.map(task => {
if (task.id === taskId) task.completed = isChecked;
return task;
});
$listItem.toggleClass('completed', isChecked);
saveTasksToLocalStorage();
});
// 双击编辑
$('#task-list').on('dblclick', '.task-text', function() {
const $listItem = $(this).closest('li');
const taskId = $listItem.data('id');
const currentText = $(this).text();
const task = tasks.find(t => t.id === taskId);
if (!task) return;
const $editInput = $('<input>', { type: 'text', class: 'edit-input', value: currentText });
const $saveBtn = $('<button>', { class: 'save-btn', text: '保存' });
$listItem.find('.task-text').replaceWith($editInput);
$listItem.find('.delete-btn').before($saveBtn);
$editInput.focus();
});
// 保存编辑(点击保存按钮)
$('#task-list').on('click', '.save-btn', function() {
const $listItem = $(this).closest('li');
const taskId = $listItem.data('id');
const $editInput = $listItem.find('.edit-input');
const newText = $editInput.val().trim();
if (newText === '') {
alert('任务内容不能为空!');
return;
}
tasks = tasks.map(task => {
if (task.id === taskId) task.text = newText;
return task;
});
saveTasksToLocalStorage();
renderTasks();
});
// 保存编辑(回车键)
$('#task-list').on('keypress', '.edit-input', function(e) {
if (e.which === 13) $(this).siblings('.save-btn').trigger('click');
});
// 全部完成
$('#mark-all-btn').on('click', function() {
tasks = tasks.map(task => { task.completed = true; return task; });
saveTasksToLocalStorage();
renderTasks();
});
// 清除已完成
$('#clear-completed-btn').on('click', function() {
tasks = tasks.filter(task => !task.completed);
saveTasksToLocalStorage();
renderTasks();
});
// 初始化
loadTasksFromLocalStorage();
</script>
</body>
</html>版本变更记录
| 版本 | 对 Todo 应用开发的影响 |
|---|---|
| 1.x | 基础的事件绑定(如 click())和 DOM 操作方法,适用于简单应用。 |
| 1.7+ | 引入 .on() 和 .off() 方法,事件委托更为统一和高效,是构建动态列表的核心。 |
| 1.9+ | 移除了一些已弃用的方法(如 .live()),推荐使用 .on() 进行事件委托。 |
| 3.x | Promise 和 Deferred 的完善,可用于处理更复杂的异步流程(如与本地存储结合的回调)。 |
| 4.0 | 移除对 IE < 11 的支持,使得可以使用更现代的 CSS 和 JavaScript 特性;源码迁移至 ES 模块,便于在现代构建工具中使用。 |
浏览器兼容性
基于 jQuery 3.x 或 4.x 版本,交互式Todo列表应用的兼容性如下。jQuery 4.x 已移除对 IE 10 及以下版本的支持。
| 浏览器类型 | 最低兼容版本(基于 jQuery 3.x/4.x) |
|---|---|
| PC 端 | |
| Chrome | 60+ (当前及先前主要版本) |
| Edge | 15+ (基于 Chromium) |
| Firefox | 55+ (当前及先前主要版本) |
| Opera | 47+ (当前及先前主要版本) |
| Safari | 10+ (当前及先前主要版本) |
| 移动端 | |
| Chrome Android | 100+ (当前版本) |
| Firefox for Android | 100+ (当前版本) |
| Opera Android | 64+ (当前版本) |
| Safari on iOS | 10+ |
| Samsung Internet | 6.2+ |
| WebView Android | 100+ (当前版本) |
| WebView on iOS | 10+ |
通过构建这个交互式Todo列表应用,可以系统性地实践jQuery的核心功能。从数据管理到动态渲染,从事件委托到本地存储,每个环节都体现了jQuery在简化DOM操作和事件处理方面的优势。此项目不仅巩固了基础语法,也为开发更复杂的交互功能奠定了坚实的实践基础。
