JavaScript实战项目实例:从零构建待办事项清单应用

作者:窗边听雨

时间:2026-10-30

浏览:301

本教程详细讲解如何使用纯原生JavaScript、HTML5与CSS3从零构建功能完备的待办事项清单应用,涵盖状态驱动视图设计、事件委托机制、数据持久化存储与防御式编程实践。

文章目录
项目类型:原生Web前端实战 技术栈:HTML5 / CSS3 / ES6+ JavaScript 无任何第三方依赖
待办事项清单(Todo List)是前端工程领域中检验语言基础、DOM操作能力与架构思维的标准实战项目。本文不依赖任何第三方前端框架,直接使用原生HTML5、CSS3与现代JavaScript,从零构建一个具备任务增删、状态标记、多维度分类筛选以及本地持久化(LocalStorage)存储的完整应用。通过本项目,你将掌握状态驱动视图的核心理念与事件委托机制的工程化实践。

一、项目设计与数据流转架构

在编写代码前,明确软件系统的架构与数据流动模式是避免代码混乱的关键。初学者编写原生JavaScript项目时,常见的误区是将状态直接存储在DOM节点上(例如通过读取类名或属性判断任务是否完成),这会导致DOM操作与业务逻辑高度耦合。

本项目采用“状态驱动视图(State-Driven UI)”的单向数据流设计。我们维护一个独立的纯内存JavaScript数组作为真实数据源(Single Source of Truth),所有用户交互(输入、勾选、删除)仅负责修改该状态数据;状态改变后触发专门的渲染函数将新状态映射到DOM中,并同步序列化至本地缓存。

JavaScript实战项目实例:从零构建待办事项清单应用
图1:待办事项应用架构与单向数据流模型(数据驱动视图)

二、构建流程与步骤概览

将大任务拆解为条理清晰的步骤是工程化实现的必备能力。从零构建一个健壮的待办事项应用,通常分为四个标准阶段:

  1. 语义化DOM骨架搭建:使用规范的HTML5标签构建清晰的文档树结构,包括任务输入表单、过滤器控制栏及任务列表容器。
  2. 核心状态与数据契约设计:确定每个任务对象的数据结构,规范其唯一标识符(ID)、内容与完成标记,并建立本地存储读写工具函数。
  3. 渲染函数与事件委托绑定:编写根据状态生成HTML的纯函数,并在外层父级容器上利用事件委托集中捕获用户的各项交互行为。
  4. 业务逻辑闭环与边界防御:实现待办添加、删除、切换状态、清空已完成功能,同时处理空输入校验、HTML转义防XSS注入与异常恢复。
JavaScript实战项目实例:从零构建待办事项清单应用
图2:从零构建待办清单应用的四个标准开发步骤

三、第一步:HTML结构设计与基础样式

我们首先创建清晰的HTML语义化结构。为了保证无障碍访问(Accessibility)并支持键盘回车直接提交,整个输入区域必须被包含在<form>元素中。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>原生JS待办事项清单</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="todo-app">
    <header class="app-header">
      <h1>待办事项清单</h1>
      <p class="subtitle">专注高效,记录每一项关键行动</p>
    </header>

    <!-- 任务创建表单 -->
    <form id="todo-form" class="todo-form">
      <input 
        type="text" 
        id="todo-input" 
        placeholder="添加新的待办任务(回车确认)..." 
        autocomplete="off"
        required
      >
      <button type="submit" id="add-btn">添加任务</button>
    </form>

    <!-- 过滤与控制工具条 -->
    <div class="toolbar">
      <div class="filters" id="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>
      <span class="count-display" id="todo-count">剩余 0 项未完成</span>
    </div>

    <!-- 动态列表渲染容器 -->
    <ul id="todo-list" class="todo-list">
      <!-- 此处将由 JavaScript 根据 state 动态注入 li 节点 -->
    </ul>

    <!-- 底部清理操作 -->
    <footer class="app-footer">
      <button type="button" id="clear-completed-btn" class="clear-btn">清除已完成事项</button>
    </footer>
  </main>

  <script src="app.js"></script>
</body>
</html>

关键布局样式采用现代CSS Flexbox布局,为已完成状态赋予淡化和删除线效果:

/* 基础样式片段 */
.todo-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 16px;
  background: #FFFFFF;
  border-bottom: 1px solid #ECE7DE;
  transition: background 0.2s ease;
}
.todo-item:hover {
  background: #FAF8F5;
}
.todo-item.completed .todo-text {
  text-decoration: line-through;
  color: #9AA5B1;
}
.todo-item .delete-btn {
  background: transparent;
  color: #C53030;
  border: none;
  cursor: pointer;
  padding: 4px 8px;
  font-size: 14px;
}

四、第二步:状态模型与本地存储抽象

任何一个健壮的数据驱动应用,都必须明确规定数据项的形状(Schema)。在待办清单中,每个任务项均定义为包含以下属性的JavaScript对象:

字段名 类型 说明
id string | number 全局唯一标识符,本教程采用时间戳字符串保证唯一性
text string 待办事项文本内容,经过清洗转义后的纯文本
completed boolean 任务是否已完成(true 或 false)
createdAt number 任务创建的毫秒级时间戳

接下来,在 app.js 的起始位置定义持久化存储与核心全局状态:

// 存储键名常量
const STORAGE_KEY = 'javascript_todolist_data_v1';

// 本地存储读写工具封装(具备容错兜底机制)
function loadStoredTodos() {
  try {
    const rawData = localStorage.getItem(STORAGE_KEY);
    return rawData ? JSON.parse(rawData) : [];
  } catch (error) {
    console.error('读取本地存储失败,初始化为空数组:', error);
    return [];
  }
}

function persistTodos(todos) {
  try {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
  } catch (error) {
    console.error('写入本地存储失败:', error);
  }
}

// 核心应用状态
let appState = {
  todos: loadStoredTodos(),
  currentFilter: 'all' // 可选值:'all' | 'active' | 'completed'
};

五、第三步:事件委托机制与精准动作分发

在待办清单应用中,用户会不断添加新的待办项。如果我们对每一个新生成的 <li> 或按钮单独使用 addEventListener,不仅会导致内存开销随着待办数量线性增加,而且在节点被销毁替换时容易造成内存泄漏。

事件委托(Event Delegation)是前端性能优化的黄金准则。我们将单一的 click 监听器挂载到静态存在的父容器 <ul id="todo-list"> 上,利用事件冒泡机制捕获所有子孙元素的点击;通过检查触发目标 event.target 上的自定义属性 data-action 和 data-id,精确判断用户的操作意图并调用对应的数据修改函数。

JavaScript实战项目实例:从零构建待办事项清单应用
图3:原生事件委托机制与精准动作分发架构

六、第四步:完整的JavaScript核心实现

下面是完整的 app.js 生产代码。代码严格遵照模块化职责分离规范,包含了安全转义工具、状态更新函数、过滤计算派生以及渲染器。

/**
 * 待办事项应用完整业务实现
 */
(function() {
  'use strict';

  // 1. 存储键名与安全工具
  const STORAGE_KEY = 'javascript_todolist_data_v1';

  // 预防 XSS 注入的纯字符转义函数
  function escapeHTML(str) {
    if (!str) return '';
    return str
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#039;');
  }

  // 2. 本地持久化封装
  function loadStoredTodos() {
    try {
      const raw = localStorage.getItem(STORAGE_KEY);
      return raw ? JSON.parse(raw) : [];
    } catch (e) {
      console.error('加载本地存储失败,重置为空:', e);
      return [];
    }
  }

  function persistTodos(todos) {
    try {
      localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
    } catch (e) {
      console.error('保存至本地存储失败:', e);
    }
  }

  // 3. 应用单例状态
  const state = {
    todos: loadStoredTodos(),
    filter: 'all' // 'all' | 'active' | 'completed'
  };

  // 4. DOM 元素缓存
  const dom = {
    form: document.getElementById('todo-form'),
    input: document.getElementById('todo-input'),
    list: document.getElementById('todo-list'),
    count: document.getElementById('todo-count'),
    filterGroup: document.getElementById('filter-group'),
    clearBtn: document.getElementById('clear-completed-btn')
  };

  // 5. 状态操作纯逻辑函数
  function addTodo(text) {
    const trimmed = text.trim();
    if (!trimmed) return;

    const newTodo = {
      id: 'todo_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5),
      text: trimmed,
      completed: false,
      createdAt: Date.now()
    };

    state.todos.unshift(newTodo);
    saveAndRender();
  }

  function toggleTodo(id) {
    const target = state.todos.find(item => item.id === id);
    if (target) {
      target.completed = !target.completed;
      saveAndRender();
    }
  }

  function deleteTodo(id) {
    state.todos = state.todos.filter(item => item.id !== id);
    saveAndRender();
  }

  function clearCompleted() {
    state.todos = state.todos.filter(item => !item.completed);
    saveAndRender();
  }

  function setFilter(filterType) {
    state.filter = filterType;
    render();
  }

  function saveAndRender() {
    persistTodos(state.todos);
    render();
  }

  // 6. 视图渲染引擎
  function render() {
    // 6.1 依据当前 filter 计算派生列表
    let visibleTodos = state.todos;
    if (state.filter === 'active') {
      visibleTodos = state.todos.filter(t => !t.completed);
    } else if (state.filter === 'completed') {
      visibleTodos = state.todos.filter(t => t.completed);
    }

    // 6.2 渲染待办列表项(若为空则输出空状态占位)
    if (visibleTodos.length === 0) {
      dom.list.innerHTML = `
        <li class="empty-placeholder" style="text-align:center;padding:32px;color:#9AA5B1;">
          当前分类下没有任何待办任务
        </li>
      `;
    } else {
      dom.list.innerHTML = visibleTodos.map(todo => `
        <li class="todo-item ${todo.completed ? 'completed' : ''}" data-id="${todo.id}">
          <label class="todo-label" style="display:flex;align-items:center;gap:12px;flex:1;cursor:pointer;">
            <input 
              type="checkbox" 
              data-action="toggle" 
              ${todo.completed ? 'checked' : ''}
            >
            <span class="todo-text">${escapeHTML(todo.text)}</span>
          </label>
          <button 
            type="button" 
            class="delete-btn" 
            data-action="delete" 
            aria-label="删除任务"
          >
            删除
          </button>
        </li>
      `).join('');
    }

    // 6.3 更新未完成统计
    const uncompletedCount = state.todos.filter(t => !t.completed).length;
    dom.count.textContent = `剩余 ${uncompletedCount} 项未完成`;

    // 6.4 更新过滤按钮激活样式
    const buttons = dom.filterGroup.querySelectorAll('.filter-btn');
    buttons.forEach(btn => {
      if (btn.dataset.filter === state.filter) {
        btn.classList.add('active');
      } else {
        btn.classList.remove('active');
      }
    });
  }

  // 7. 事件绑定与初始化
  function initEvents() {
    // 7.1 表单提交事件
    dom.form.addEventListener('submit', function(e) {
      e.preventDefault();
      addTodo(dom.input.value);
      dom.input.value = '';
      dom.input.focus();
    });

    // 7.2 列表容器上的事件委托
    dom.list.addEventListener('click', function(e) {
      const actionTrigger = e.target.closest('[data-action]');
      if (!actionTrigger) return;

      const itemRow = actionTrigger.closest('[data-id]');
      if (!itemRow) return;

      const id = itemRow.dataset.id;
      const action = actionTrigger.dataset.action;

      if (action === 'toggle') {
        toggleTodo(id);
      } else if (action === 'delete') {
        deleteTodo(id);
      }
    });

    // 7.3 过滤按钮组事件委托
    dom.filterGroup.addEventListener('click', function(e) {
      const btn = e.target.closest('.filter-btn');
      if (btn && btn.dataset.filter) {
        setFilter(btn.dataset.filter);
      }
    });

    // 7.4 清理已完成按钮
    dom.clearBtn.addEventListener('click', function() {
      clearCompleted();
    });
  }

  // 8. 启动应用
  initEvents();
  render();
})();

七、关键工程细节与避坑指南

防范 XSS(跨站脚本攻击)注入

在利用模板字符串动态拼接 innerHTML 时,若未对用户输入的文本进行过滤,恶意用户可能输入形如 <img src="x" onerror="alert(1)"> 的字符引发脚本执行。本实战中专门封装了 escapeHTML() 函数,对 &, <, >, ", ' 等敏感字符进行转义,筑牢客户端安全底线。

LocalStorage 的配额与容错处理

浏览器对 LocalStorage 施加了约 5MB 的存储上限,且在用户开启“无痕浏览模式”或本地存储被浏览器策略禁用时,调用 setItem 会直接抛出 QuotaExceededError 运行时异常。务必像本教程所示,使用 try...catch 代码块包裹持久化操作,保证应用在存储失败时内存逻辑依然可用。

八、技术总结与拓展进阶体系

通过从零构建这个待办清单应用,我们实践了前端开发中最核心的几个设计哲学:从被动的手动DOM微操转向状态驱动视图;从松散零碎的事件监听转向父级事件委托;从内存易失转向LocalStorage持久化同步。这套模式不仅适用于原生开发,更是现代前端框架(如 React、Vue)底层运作逻辑的缩影。

JavaScript实战项目实例:从零构建待办事项清单应用
图4:待办事项应用关键要点与进阶拓展路线

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章