针对JavaScript初学者的实战项目教程,系统讲解如何将变量、数组、函数、DOM操作、事件监听与本地存储等语法融会贯通,从零构建一个完整的待办事项(To-Do List)应用。
怎么构建第一个JavaScript项目:整合语法完成待办事项应用
时间:2026-10-30
浏览:498
文章目录
一、项目设计:理解“数据驱动”的核心运行闭环
初学者在刚开始写 JavaScript 页面时,最容易走入的弯路是“直接把数据写死在 HTML 元素里”。例如每当用户输入新待办时,立刻拼接一段 HTML 字符串插入到列表中;当需要删除时,又通过复杂的父子节点查找(如 parentNode.parentNode.remove())直接抹除节点。这种做法在代码行数增加后,极易导致界面显示与底层数据脱节。
构建任何前端项目,推荐优先确立“数据驱动视图”的设计思想:
- 状态(State):在内存中声明一个数组
todos,所有待办事项的内容、是否完成、唯一标识都存储在该数组中,它作为整个应用的“唯一可信数据源”。 - 渲染(Render):编写一个纯粹的渲染函数,其职责仅仅是读取
todos数组,并根据数据生成对应的 DOM 节点并放入列表。 - 交互更新(Update):用户的点击或提交操作不直接改动 HTML,而是直接修改
todos数组,随后触发渲染函数刷新界面,并同步保存到localStorage。
二、基础准备:搭建干净的目录与页面骨架
我们不需要任何打包构建工具或外部依赖库,只需在本地新建一个文件夹,包含三个标准文件:
index.html:定义页面的语义结构与元素容器。style.css:提供清晰易读的界面排版与交互反馈样式。app.js:承载所有核心业务逻辑与数据存储。
1. 编写 HTML 语义结构
HTML 文件重点包含一个表单输入区、一个待办列表挂载节点 ul,以及底部的统计信息区:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>简易待办事项</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="todo-app">
<h1>我的待办清单</h1>
<form id="todo-form">
<input type="text" id="todo-input" placeholder="输入待办事项后回车..." autocomplete="off">
<button type="submit">添加</button>
</form>
<ul id="todo-list"></ul>
<footer class="todo-footer">
<span id="todo-count">剩余待办:0</span>
<button id="clear-completed">清空已完成</button>
</footer>
</main>
<script src="app.js"></script>
</body>
</html>
2. 编写基础 CSS 样式
在 style.css 中,通过简单的 flex 布局让列表清晰整洁,并重点为已完成状态设置删除线与浅灰色字:
.todo-app {
max-width: 480px;
margin: 40px auto;
padding: 24px;
background: #ffffff;
border: 1px solid #e5e5e5;
border-radius: 8px;
}
#todo-form {
display: flex;
gap: 8px;
margin-bottom: 20px;
}
#todo-input {
flex: 1;
padding: 10px 12px;
border: 1px solid #ccc;
border-radius: 4px;
}
#todo-list {
list-style: none;
padding: 0;
}
.todo-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid #f0f0f0;
}
.todo-item.completed .todo-text {
text-decoration: line-through;
color: #999;
}
.delete-btn {
background: #e74c3c;
color: #fff;
border: none;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
}
三、关键步骤:分步实现 JavaScript 核心逻辑
编写逻辑时,严格按照“步骤分解”依次递进,每一个步骤都只做一件明确的事情:
步骤 1:初始化状态与获取 DOM 节点
在 app.js 顶部,我们通过 document.querySelector 缓存需要的界面节点,并声明 todos 数组:
// 1. 获取界面核心 DOM 元素
const todoForm = document.querySelector('#todo-form');
const todoInput = document.querySelector('#todo-input');
const todoList = document.querySelector('#todo-list');
const todoCount = document.querySelector('#todo-count');
const clearCompletedBtn = document.querySelector('#clear-completed');
// 2. 初始化核心数据状态
let todos = [];
步骤 2:捕获表单输入并新增待办
监听表单的 submit 事件,而不是按钮的 click 事件。这样用户在输入框按回车键或点击按钮时均能正常提交。使用 event.preventDefault() 阻止表单默认的刷新行为:
todoForm.addEventListener('submit', function (event) {
event.preventDefault(); // 阻止页面默认提交刷新
const text = todoInput.value.trim();
if (text === '') {
return; // 输入内容为空时不执行操作
}
// 创建待办对象
const newTodo = {
id: Date.now(), // 使用时间戳作为简易唯一ID
title: text,
completed: false
};
todos.push(newTodo);
todoInput.value = ''; // 清空输入框
saveAndRender(); // 保存并重新渲染
});
步骤 3:编写视图渲染函数
渲染函数负责清空旧的列表内容,将 todos 数组逐个转为 HTML 片段并写回,同时更新剩余未完成待办的计数:
function render() {
// 清空旧的列表节点
todoList.innerHTML = '';
todos.forEach(function (todo) {
const li = document.createElement('li');
li.className = `todo-item ${todo.completed ? 'completed' : ''}`;
li.dataset.id = todo.id; // 在 DOM 节点上记录数据 ID
li.innerHTML = `
<input type="checkbox" class="toggle-checkbox" ${todo.completed ? 'checked' : ''}>
<span class="todo-text">${escapeHTML(todo.title)}</span>
<button class="delete-btn">删除</button>
`;
todoList.appendChild(li);
});
// 更新未完成计数
const uncompletedCount = todos.filter(t => !t.completed).length;
todoCount.textContent = `剩余待办:${uncompletedCount}`;
}
// 辅助函数:防止用户输入特殊字符引起HTML解析异常
function escapeHTML(str) {
return str.replace(/[&<>"']/g, function (match) {
const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
return map[match];
});
}
步骤 4:使用“事件委托”处理切换完成与删除
如果每次渲染都为每一个待办事项中的复选框和删除按钮绑定 addEventListener,不仅代码繁琐,还会随着频繁重新渲染造成内存占用上升。
正确的做法是事件委托(Event Delegation):只在父级容器 todoList 上绑定一个点击事件监听器。当点击发生时,利用事件冒泡机制通过 e.target 识别点击的具体是哪个子元素:
todoList.addEventListener('click', function (event) {
const target = event.target;
const parentLi = target.closest('.todo-item');
if (!parentLi) return;
const id = Number(parentLi.dataset.id);
// 1. 点击了复选框:切换完成状态
if (target.classList.contains('toggle-checkbox')) {
const todo = todos.find(item => item.id === id);
if (todo) {
todo.completed = !todo.completed;
saveAndRender();
}
}
// 2. 点击了删除按钮:移除该条待办
if (target.classList.contains('delete-btn')) {
todos = todos.filter(item => item.id !== id);
saveAndRender();
}
});
步骤 5:数据持久化与页面初始化加载
为了在浏览器刷新或重新打开页面后数据不丢失,利用浏览器自带的 localStorage API。在修改数据后调用 JSON.stringify 存入本地;页面打开时调用 JSON.parse 读回:
const STORAGE_KEY = 'my_todo_list_data';
// 集中保存并刷新界面
function saveAndRender() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
render();
}
// 页面加载时的初始化函数
function init() {
const rawData = localStorage.getItem(STORAGE_KEY);
if (rawData) {
try {
todos = JSON.parse(rawData);
} catch (e) {
console.error('本地缓存数据格式解析失败,已重置为空', e);
todos = [];
}
}
render();
}
// 绑定“清空已完成”按钮事件
clearCompletedBtn.addEventListener('click', function () {
todos = todos.filter(item => !item.completed);
saveAndRender();
});
// 启动程序
init();
四、概念辨析:直接拼接 DOM 与数据驱动的区别
初学者常常觉得“直接操作 DOM”最直观:点一下添加就追加一行,点一下删除就删掉对应的 HTML 节点。这种写法在功能极简时确实能工作,但随着需求增加(例如加上“过滤:全部/未完成/已完成”、“一键清空完成项”、“批量排序”等),直接操作 DOM 会让逻辑迅速失控,状态四处分散。
五、实战注意事项与常见问题排查
在实际编写和测试项目时,有几个新手极易忽视的边界条件与细节:
-
输入空值与空白字符过滤:必须使用
text.trim()进行截断检查。如果用户只按了几次空格键就提交,不应产生空白待办项。 -
XSS 跨站脚本防御:如果直接使用模板字符串
<span>${todo.title}</span>且未转义,用户若输入类似<img src=x onerror=alert(1)>的文本,浏览器就会执行潜在脚本。使用escapeHTML或直接赋值span.textContent = todo.title能从根源避免该问题。 -
数据集(dataset)类型的自动转换:通过
element.dataset.id取出来的值永远是字符串类型。而在我们生成id: Date.now()时它是数字类型。因此在find或filter比较前,务必使用Number(parentLi.dataset.id)进行显式类型转换,否则===比较会永远返回false。 -
缓存解析的防御性捕获:
localStorage中的数据可能被用户从开发者工具手动篡改或残留脏数据。在执行JSON.parse()时包裹try...catch,能避免整个页面脚本因一条损坏的本地缓存而彻底崩溃。
六、全景复盘:知识点在项目中的整合
通过这个简单的待办事项项目,你实际上已经把 JavaScript 核心知识体系中的几大模块全部串联在了一起:
完成了这套逻辑后,你的第一个项目就已经具备了完整的产品级闭环。如果想要进一步提升,可以在现有架构之上尝试增加:按“全部/未完成/已完成”过滤列表、双击待办标题进行原位编辑,或者通过 HTML5 Drag and Drop API 实现拖拽排序。无论功能如何扩展,只要坚持“修改数据状态 -> 重新渲染视图”的主线,你的代码就会始终保持清晰与健壮。
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。