三亩地 三亩地SAN MU DI · CODE DIARY
CASE STUDY · 001

待办清单 Web 应用

用原生 HTML/CSS/JS 写一个能增删改查、能记住你今天要做的事的小清单,是前端入门必做的第一个完整闭环项目。

待办清单 Web 应用界面预览
前端 · 入门

用原生 JS 写一个能记住你今天要做的事的小清单

这是我把「学了 JS 但还没做过完整东西」的状态打通的第一个项目。一个输入框、一个列表、一个完成勾选、一个删除按钮,看似简单,但它把数据从用户输入到内存、再到屏幕、再到本地存储的完整链路都跑了一遍。做完之后,前端的基本心智模型就立起来了。

技术栈 HTML / CSS / JavaScript
预计耗时 4 - 6 小时
难度等级 入门
数据持久化 localStorage
HTML5 CSS3 原生 JavaScript localStorage Flexbox
STEPS

五步把它做出来

不要一开始就想着「全部写完」,把它拆成五个能独立看到结果的小目标。

1

搭骨架与基础样式

先写一个居中的容器,里面放标题、输入框 + 添加按钮、空的列表 ul。CSS 用 Flexbox 把整体居中,给每条 li 一个圆角卡片样式、左侧一个 checkbox、右侧一个删除小图标。这一步不写任何 JS,先把「静止的样子」做到自己满意。

2

用数组渲染列表

声明一个 todos = [] 数组,写一个 render() 函数:先清空 ul,再遍历数组把每条 todo 拼成 li 字符串插进去。这一步数据写死,比如 [{text:'学完这个案例', done:false}],验证渲染逻辑能跑通再继续。

3

新增与删除

给添加按钮绑定点击事件,读输入框的值、trim 后判空,往数组 push 一个新对象,再调 render()。删除用事件委托:在 ul 上监听 click,通过 e.target.closest('.del') 拿到要删的那条,从数组里 splice 掉再渲染。

4

切换完成状态

点击 checkbox 时把对应 todo 的 done 取反。在 render 里给已完成的 li 加一个 completed class,CSS 用它做删除线 + 灰化。这一步让你第一次体会到「数据变 → 视图变」的响应式雏形。

5

持久化与过滤展示

每次 render 之后把数组 JSON.stringify 存进 localStorage,页面加载时先 JSON.parse 恢复。最后加三个过滤按钮「全部 / 未完成 / 已完成」,点击只是改变 render 的过滤条件,不删数据。到此一个完整闭环就跑通了。

CODE

关键代码片段

把渲染、新增、持久化三段最核心的逻辑拎出来,其他都是围绕它们的胶水代码。

// 1. 数据与渲染:单一数据源,渲染永远跟着数据走
let todos = JSON.parse(localStorage.getItem('todos')) || [];
const listEl = document.querySelector('#todo-list');

function render(filter = 'all') {
  listEl.innerHTML = '';
  const view = todos.filter(t => {
    if (filter === 'active') return !t.done;
    if (filter === 'done') return t.done;
    return true;
  });
  view.forEach((t, i) => {
    listEl.insertAdjacentHTML('beforeend', `
      <li class="${t.done ? 'completed' : ''}" data-id="${t.id}">
        <label><input type="checkbox" ${t.done ? 'checked' : ''}> ${t.text}</label>
        <button class="del">✕</button>
      </li>`);
  });
  localStorage.setItem('todos', JSON.stringify(todos));
}

// 2. 新增
document.querySelector('#add-btn').addEventListener('click', () => {
  const input = document.querySelector('#todo-input');
  const text = input.value.trim();
  if (!text) return;
  todos.push({ id: Date.now(), text, done: false });
  input.value = '';
  render();
});

// 3. 事件委托:勾选 + 删除
listEl.addEventListener('click', (e) => {
  const li = e.target.closest('li');
  const id = Number(li.dataset.id);
  if (e.target.matches('.del')) {
    todos = todos.filter(t => t.id !== id);
  } else if (e.target.matches('input[type=checkbox]')) {
    todos = todos.map(t => t.id === id ? { ...t, done: !t.done } : t);
  }
  render();
});

render(); // 首次渲染

做完这个项目最大的收获不是写出了一个能用的 Todo,而是第一次真正理解了「数据驱动视图」:用户操作的本质是改数据,渲染只是一个把数据翻译成 DOM 的函数。后面学 Vue、React 时,会发现它们的响应式不过是把这个 render() 调用自动化了而已。

—— 三亩地 · 案例笔记
NEXT

继续下一个案例

搞定交互逻辑后,下一个案例我们去练静态布局与响应式,把博客首页做出来。

← 返回案例列表 下一篇:个人博客静态站 →
或者去小型实战项目继续练 →