HTML+CSS+JavaScript综合实战:从零制作一个待办事项列表

作者:流萤时间:2026-10-30 18:43:25浏览:265

通过构建一个功能完备的待办事项列表(Todo List),系统串联HTML结构搭建、CSS排版美化与JavaScript状态驱动交互,深入掌握前端基础核心技能与本地持久化。

制作一个待办事项列表(Todo List)是检验前端三大核心技术掌握程度的标准实践项目。本文不跳跃任何关键细节,带你从空白工程开始,依次搭建语义化HTML结构、编写响应式CSS视觉样式,并使用基于“数据驱动状态”的JavaScript逻辑实现任务的新增、状态切换、删除与本地持久化存储,完成一个能在真实浏览器中稳定运行的完整作品。

一、项目设计与三层架构关系

很多初学者在编写交互项目时,习惯在触发按钮时直接用JavaScript操作DOM创建标签,这种“DOM即数据”的做法在逻辑变复杂后极易造成状态错乱。规范的做法是将应用拆解为三个紧密协作的层次:

  • 结构层(HTML):只定义页面有哪些静态区域,包括表单输入区、任务列表容器以及状态过滤栏。
  • 表现层(CSS):负责卡片居中排版、输入框聚焦反馈、任务完成时的划线灰显过渡动效。
  • 逻辑与数据层(JavaScript):维护核心数组状态(State),用户所有操作均转化为对数据的增删改,最后通过统一的渲染函数将数据映射为视图,并与 localStorage 保持双向同步。
HTML+CSS+JavaScript综合实战:从零制作一个待办事项列表
图1:待办事项应用的分层架构与单向数据驱动闭环

二、四步渐进式实战步骤

为了让开发条理清晰,我们将整个构建过程划分为四个严密的步骤,每一步都有明确的交付产物与验证方法:

HTML+CSS+JavaScript综合实战:从零制作一个待办事项列表
图2:从零实现待办事项列表的标准工程操作路径

三、阶段一:编写语义化 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)。

HTML+CSS+JavaScript综合实战:从零制作一个待办事项列表
图3:利用事件委托与冒泡机制实现统一事件监听与分发

完整 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="删除任务">&times;</button>
      </li>
    `).join('');
  }

  // 更新未完成事项计数器
  const activeCount = todos.filter(t => !t.completed).length;
  taskCounter.textContent = `${activeCount} 项未完成`;

  // 保存最新状态至持久化存储
  saveToStorage();
}

// 5. 安全辅助:防止 XSS 攻击的转义工具
function escapeHTML(str) {
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

// 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();

六、新手实战关键避坑指南

1. 表单提交的页面刷新陷阱

初学者容易忘记在 form 的 submit 监听器中调用 e.preventDefault()。如果不加此调用,点击“添加”或在输入框敲回车时,浏览器会直接发送 GET/POST 请求并刷新页面,导致刚刚填入的数据瞬时丢失,看起来像“代码完全没生效”。

2. XSS 漏洞与转义安全

在使用模板字符串拼接 todoList.innerHTML 时,若直接将用户输入的 todo.title 拼入 HTML,恶意用户若输入 <img src=x onerror=alert(1)> 就会导致脚本执行。通过本文提供的 escapeHTML() 预处理函数,能将尖括号与双引号转义为安全实体,彻底规避跨站脚本风险。

3. localStorage 的同步时机与类型转换

localStorage 只能存储纯文本字符串,因此在保存时必须使用 JSON.stringify() 序列化;在读取时必须使用 JSON.parse() 反序列化,并一定要加入 try...catch 容错,防止因用户手动修改了浏览器缓存而引发全局崩溃。

七、知识全景总结与能力拓扑

通过这篇实战教程,我们完成了从静态页面到交互式持久化单页应用的演进,以下是整个项目用到的关键知识地图:

HTML+CSS+JavaScript综合实战:从零制作一个待办事项列表
图4:HTML+CSS+JavaScript 协同构建的核心知识拓扑复盘

通过本项目实战,我们体会到了现代前端框架(如 Vue、React)最底层的运作哲学:“UI = f(State)”。只要打牢这个基础,后续学习组件化开发、虚拟 DOM 与状态管理库就会更加得心应手。

作者最新文章
相关文章
更多