怎么构建第一个JavaScript项目:整合语法完成待办事项应用

作者:小圆点

时间:2026-10-30

浏览:498

针对JavaScript初学者的实战项目教程,系统讲解如何将变量、数组、函数、DOM操作、事件监听与本地存储等语法融会贯通,从零构建一个完整的待办事项(To-Do List)应用。

文章目录
构建第一个 JavaScript 项目的关键,在于打破孤立语法知识与完整交互之间的壁垒。本文通过实现一个具备增删、状态标记与本地持久化的待办事项(To-Do List)应用,演示如何以“数据状态(State)”为中心,通过事件监听(Event Listener)响应用户操作,进而更新数据并自动同步到界面(DOM),帮助你建立清晰的工程逻辑体系。

一、项目设计:理解“数据驱动”的核心运行闭环

初学者在刚开始写 JavaScript 页面时,最容易走入的弯路是“直接把数据写死在 HTML 元素里”。例如每当用户输入新待办时,立刻拼接一段 HTML 字符串插入到列表中;当需要删除时,又通过复杂的父子节点查找(如 parentNode.parentNode.remove())直接抹除节点。这种做法在代码行数增加后,极易导致界面显示与底层数据脱节。

构建任何前端项目,推荐优先确立“数据驱动视图”的设计思想:

  • 状态(State):在内存中声明一个数组 todos,所有待办事项的内容、是否完成、唯一标识都存储在该数组中,它作为整个应用的“唯一可信数据源”。
  • 渲染(Render):编写一个纯粹的渲染函数,其职责仅仅是读取 todos 数组,并根据数据生成对应的 DOM 节点并放入列表。
  • 交互更新(Update):用户的点击或提交操作不直接改动 HTML,而是直接修改 todos 数组,随后触发渲染函数刷新界面,并同步保存到 localStorage。
怎么构建第一个JavaScript项目:整合语法完成待办事项应用
图1:待办事项应用的数据驱动与单向环流架构图

二、基础准备:搭建干净的目录与页面骨架

我们不需要任何打包构建工具或外部依赖库,只需在本地新建一个文件夹,包含三个标准文件:

  • 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 核心逻辑

编写逻辑时,严格按照“步骤分解”依次递进,每一个步骤都只做一件明确的事情:

怎么构建第一个JavaScript项目:整合语法完成待办事项应用
图2:从零构建待办事项应用的五个关键执行步骤

步骤 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 = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
    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 与数据驱动的区别

常见疑问:为什么不直接在表单提交时往 ul 里面 appendChild,而非要多绕一层 todos 数组?

初学者常常觉得“直接操作 DOM”最直观:点一下添加就追加一行,点一下删除就删掉对应的 HTML 节点。这种写法在功能极简时确实能工作,但随着需求增加(例如加上“过滤:全部/未完成/已完成”、“一键清空完成项”、“批量排序”等),直接操作 DOM 会让逻辑迅速失控,状态四处分散。

怎么构建第一个JavaScript项目:整合语法完成待办事项应用
图3:直接操作 DOM 与状态驱动集中渲染的深度对比

五、实战注意事项与常见问题排查

在实际编写和测试项目时,有几个新手极易忽视的边界条件与细节:

  1. 输入空值与空白字符过滤:必须使用 text.trim() 进行截断检查。如果用户只按了几次空格键就提交,不应产生空白待办项。
  2. XSS 跨站脚本防御:如果直接使用模板字符串 <span>${todo.title}</span> 且未转义,用户若输入类似 <img src=x onerror=alert(1)> 的文本,浏览器就会执行潜在脚本。使用 escapeHTML 或直接赋值 span.textContent = todo.title 能从根源避免该问题。
  3. 数据集(dataset)类型的自动转换:通过 element.dataset.id 取出来的值永远是字符串类型。而在我们生成 id: Date.now() 时它是数字类型。因此在 find 或 filter 比较前,务必使用 Number(parentLi.dataset.id) 进行显式类型转换,否则 === 比较会永远返回 false。
  4. 缓存解析的防御性捕获:localStorage 中的数据可能被用户从开发者工具手动篡改或残留脏数据。在执行 JSON.parse() 时包裹 try...catch,能避免整个页面脚本因一条损坏的本地缓存而彻底崩溃。

六、全景复盘:知识点在项目中的整合

通过这个简单的待办事项项目,你实际上已经把 JavaScript 核心知识体系中的几大模块全部串联在了一起:

怎么构建第一个JavaScript项目:整合语法完成待办事项应用
图4:JavaScript 核心语法在待办事项应用中的整合复盘

完成了这套逻辑后,你的第一个项目就已经具备了完整的产品级闭环。如果想要进一步提升,可以在现有架构之上尝试增加:按“全部/未完成/已完成”过滤列表、双击待办标题进行原位编辑,或者通过 HTML5 Drag and Drop API 实现拖拽排序。无论功能如何扩展,只要坚持“修改数据状态 -> 重新渲染视图”的主线,你的代码就会始终保持清晰与健壮。

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章