HTML+CSS+JavaScript综合实战:从零制作一个待办事项列表
通过构建一个功能完备的待办事项列表(Todo List),系统串联HTML结构搭建、CSS排版美化与JavaScript状态驱动交互,深入掌握前端基础核心技能与本地持久化。
制作一个待办事项列表(Todo List)是检验前端三大核心技术掌握程度的标准实践项目。本文不跳跃任何关键细节,带你从空白工程开始,依次搭建语义化HTML结构、编写响应式CSS视觉样式,并使用基于“数据驱动状态”的JavaScript逻辑实现任务的新增、状态切换、删除与本地持久化存储,完成一个能在真实浏览器中稳定运行的完整作品。
一、项目设计与三层架构关系
很多初学者在编写交互项目时,习惯在触发按钮时直接用JavaScript操作DOM创建标签,这种“DOM即数据”的做法在逻辑变复杂后极易造成状态错乱。规范的做法是将应用拆解为三个紧密协作的层次:
- 结构层(HTML):只定义页面有哪些静态区域,包括表单输入区、任务列表容器以及状态过滤栏。
- 表现层(CSS):负责卡片居中排版、输入框聚焦反馈、任务完成时的划线灰显过渡动效。
- 逻辑与数据层(JavaScript):维护核心数组状态(State),用户所有操作均转化为对数据的增删改,最后通过统一的渲染函数将数据映射为视图,并与
localStorage保持双向同步。
二、四步渐进式实战步骤
为了让开发条理清晰,我们将整个构建过程划分为四个严密的步骤,每一步都有明确的交付产物与验证方法:
三、阶段一:编写语义化 HTML 结构
HTML 的职责是提供清晰、无冗余的内容节点。我们需要特别注意:
- 使用
<form>包裹输入框与提交按钮,利用表单原生机制支持移动端软键盘的“前往/完成”键以及回车提交,无需额外监听键盘按键码。 - 给输入框设置
required和autocomplete="off",防止空内容提交与浏览器历史自动填充干扰。 - 任务列表使用
<ul id="todo-list">,初始化时保持内部为空,由 JavaScript 动态挂载。
<!-- 待办事项主容器 -->
<div class="todo-app">
<header class="app-header">
<h1>我的待办事项</h1>
<span id="task-counter" class="counter-badge">0 项待办</span>
</header>
<!-- 输入表单区 -->
<form id="todo-form" class="todo-form">
<input
type="text"
id="todo-input"
class="todo-input"
placeholder="输入要做的事情,回车即可添加..."
required
autocomplete="off"
/>
<button type="submit" class="btn btn-primary">添加</button>
</form>
<!-- 状态过滤控制栏 -->
<div class="filter-group">
<button type="button" class="filter-btn active" data-filter="all">全部</button>
<button type="button" class="filter-btn" data-filter="active">进行中</button>
<button type="button" class="filter-btn" data-filter="completed">已完成</button>
</div>
<!-- 任务展示列表容器 -->
<ul id="todo-list" class="todo-list"></ul>
<!-- 底部清理操作区 -->
<footer class="app-footer">
<button type="button" id="clear-completed" class="btn-text">清理已完成项</button>
</footer>
</div>
四、阶段二:编写现代响应式 CSS 样式
样式设计的关键是保持视觉清晰、重点突出,同时处理好状态切换的过渡效果。任务完成时,通过给整行添加 completed 类名,同时让文字产生中划线与半透明变淡效果:
/* 基础变量与主容器 */
.todo-app {
max-width: 520px;
margin: 40px auto;
background: #FFFFFF;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
padding: 24px;
border: 1px solid #E2E8F0;
}
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.counter-badge {
font-size: 0.85rem;
background: #F1F5F9;
color: #64748B;
padding: 4px 10px;
border-radius: 20px;
font-weight: 500;
}
/* 输入表单 */
.todo-form {
display: flex;
gap: 10px;
margin-bottom: 16px;
}
.todo-input {
flex: 1;
padding: 12px 16px;
border: 1.5px solid #CBD5E1;
border-radius: 8px;
font-size: 0.95rem;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.todo-input:focus {
border-color: #345E82;
box-shadow: 0 0 0 3px rgba(52, 94, 130, 0.15);
}
.btn-primary {
padding: 0 20px;
background-color: #345E82;
color: #FFFFFF;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: background 0.2s;
}
.btn-primary:hover {
background-color: #274763;
}
/* 过滤按钮组 */
.filter-group {
display: flex;
gap: 8px;
margin-bottom: 16px;
border-bottom: 1px solid #F1F5F9;
padding-bottom: 12px;
}
.filter-btn {
background: transparent;
border: none;
padding: 6px 12px;
font-size: 0.88rem;
color: #64748B;
cursor: pointer;
border-radius: 6px;
transition: all 0.2s;
}
.filter-btn.active {
background: #F1F5F9;
color: #345E82;
font-weight: 600;
}
/* 待办列表项 */
.todo-list {
list-style: none;
margin-bottom: 16px;
}
.todo-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 14px;
background: #F8FAFC;
border: 1px solid #E2E8F0;
border-radius: 8px;
margin-bottom: 8px;
transition: transform 0.15s, opacity 0.2s;
}
.todo-content {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.todo-checkbox {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: #345E82;
}
.todo-text {
font-size: 0.95rem;
color: #1E293B;
word-break: break-all;
transition: color 0.2s;
}
/* 已完成状态的视觉处理 */
.todo-item.completed .todo-text {
text-decoration: line-through;
color: #94A3B8;
}
/* 删除按钮 */
.btn-delete {
background: transparent;
border: none;
color: #94A3B8;
font-size: 1.2rem;
cursor: pointer;
padding: 0 6px;
line-height: 1;
border-radius: 4px;
transition: color 0.2s;
}
.btn-delete:hover {
color: #EF4444;
}
.app-footer {
display: flex;
justify-content: flex-end;
}
.btn-text {
background: transparent;
border: none;
color: #64748B;
font-size: 0.85rem;
cursor: pointer;
}
.btn-text:hover {
color: #0F172A;
text-decoration: underline;
}
五、阶段三:深入核心 JavaScript 交互逻辑
很多初学者在编写列表时,往往每次生成新的 <li> 都为其内部的按钮绑定一次 addEventListener。当任务频繁新增、删除时,容易导致内存泄漏与代码难以维护。最优雅的高性能方案是事件委托(Event Delegation)。
完整 JavaScript 代码实现
以下为带详细注释的完整脚本逻辑。包含初始化加载、状态管理、渲染管道及本地存储写入:
// 1. 常量定义与 DOM 元素获取
const STORAGE_KEY = 'VANILLA_TODO_APP_DATA';
const form = document.getElementById('todo-form');
const input = document.getElementById('todo-input');
const todoList = document.getElementById('todo-list');
const taskCounter = document.getElementById('task-counter');
const filterButtons = document.querySelectorAll('.filter-btn');
const clearCompletedBtn = document.getElementById('clear-completed');
// 2. 应用内部响应式状态
let todos = loadFromStorage();
let currentFilter = 'all'; // 'all' | 'active' | 'completed'
// 3. 本地存储持久化处理
function loadFromStorage() {
try {
const rawData = localStorage.getItem(STORAGE_KEY);
return rawData ? JSON.parse(rawData) : [];
} catch (error) {
console.warn('本地存储数据解析失败,已重置为空列表', error);
return [];
}
}
function saveToStorage() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
}
// 4. 核心渲染函数:数据映射到视图
function render() {
// 根据当前过滤器筛选要渲染的数据子集
const filteredTodos = todos.filter(todo => {
if (currentFilter === 'active') return !todo.completed;
if (currentFilter === 'completed') return todo.completed;
return true;
});
// 生成 HTML 字符串片段
if (filteredTodos.length === 0) {
todoList.innerHTML = `
<li class="empty-tip" style="text-align:center;color:#94a3b8;padding:20px 0;font-size:0.9rem;">
暂无相关待办事项
</li>
`;
} else {
todoList.innerHTML = filteredTodos.map(todo => `
<li class="todo-item ${todo.completed ? 'completed' : ''}" data-id="${todo.id}">
<div class="todo-content">
<input
type="checkbox"
class="todo-checkbox"
${todo.completed ? 'checked' : ''}
/>
<span class="todo-text">${escapeHTML(todo.title)}</span>
</div>
<button type="button" class="btn-delete" title="删除任务">×</button>
</li>
`).join('');
}
// 更新未完成事项计数器
const activeCount = todos.filter(t => !t.completed).length;
taskCounter.textContent = `${activeCount} 项未完成`;
// 保存最新状态至持久化存储
saveToStorage();
}
// 5. 安全辅助:防止 XSS 攻击的转义工具
function escapeHTML(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// 6. 事件监听:提交新增任务
form.addEventListener('submit', function (e) {
e.preventDefault(); // 关键:阻止表单默认提交导致页面重新刷新
const title = input.value.trim();
if (!title) return;
const newTodo = {
id: Date.now().toString(), // 生成唯一时间戳标识符
title: title,
completed: false
};
todos.unshift(newTodo); // 新任务置顶
input.value = ''; // 清空输入框
render();
});
// 7. 事件监听:利用事件委托集中处理勾选与删除
todoList.addEventListener('click', function (e) {
const target = e.target;
const itemEl = target.closest('.todo-item');
if (!itemEl) return;
const todoId = itemEl.dataset.id;
// 处理勾选完成状态
if (target.matches('.todo-checkbox')) {
const targetTodo = todos.find(t => t.id === todoId);
if (targetTodo) {
targetTodo.completed = target.checked;
render();
}
}
// 处理点击删除按钮
if (target.matches('.btn-delete')) {
todos = todos.filter(t => t.id !== todoId);
render();
}
});
// 8. 事件监听:状态过滤器切换
filterButtons.forEach(btn => {
btn.addEventListener('click', function () {
filterButtons.forEach(b => b.classList.remove('active'));
this.classList.add('active');
currentFilter = this.dataset.filter;
render();
});
});
// 9. 事件监听:一键清理所有已完成任务
clearCompletedBtn.addEventListener('click', function () {
todos = todos.filter(t => !t.completed);
render();
});
// 10. 页面加载完成,执行初次渲染
render();
六、新手实战关键避坑指南
初学者容易忘记在 form 的 submit 监听器中调用 e.preventDefault()。如果不加此调用,点击“添加”或在输入框敲回车时,浏览器会直接发送 GET/POST 请求并刷新页面,导致刚刚填入的数据瞬时丢失,看起来像“代码完全没生效”。
在使用模板字符串拼接 todoList.innerHTML 时,若直接将用户输入的 todo.title 拼入 HTML,恶意用户若输入 <img src=x onerror=alert(1)> 就会导致脚本执行。通过本文提供的 escapeHTML() 预处理函数,能将尖括号与双引号转义为安全实体,彻底规避跨站脚本风险。
localStorage 只能存储纯文本字符串,因此在保存时必须使用 JSON.stringify() 序列化;在读取时必须使用 JSON.parse() 反序列化,并一定要加入 try...catch 容错,防止因用户手动修改了浏览器缓存而引发全局崩溃。
七、知识全景总结与能力拓扑
通过这篇实战教程,我们完成了从静态页面到交互式持久化单页应用的演进,以下是整个项目用到的关键知识地图:
通过本项目实战,我们体会到了现代前端框架(如 Vue、React)最底层的运作哲学:“UI = f(State)”。只要打牢这个基础,后续学习组件化开发、虚拟 DOM 与状态管理库就会更加得心应手。
怎么在 Vue 或 React 项目中正确集成 animate.js
2026-11-05 10:23:38
JS注册事件:addEventListener基础语法与参数详解
2026-11-05 08:44:56
JSP和JS的区别:运行环境与服务端/客户端职责解析
2026-11-04 23:34:36
pdf.js 官方下载地址:GitHub Release 获取预编译包
2026-11-04 23:07:27
前端学习路径:为什么建议先掌握HTML再学习JavaScript?
2026-11-04 18:56:08








