构建模块化 Todo List 应用:Odin 课程 JavaScript 综合实战(工厂函数、ES6 模块与 localStorage 持久化)
【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum
本指南基于 curriculum 仓库中的javascript/organizing_your_javascript_code/project_todo_list.md项目作业,系统讲解如何用当前课程已学的全部前端技术,从零构建一个功能完整的 Todo List 应用。你将掌握:用工厂函数或类动态生成 todo 对象、用项目(Projects)组织任务、将应用逻辑与 DOM 操作拆分为独立模块、通过 npm 引入 date-fns 处理日期,以及借助 Web Storage API(localStorage)实现数据持久化,最终产出一个可刷新不丢数据、可扩展的真实 Web 应用。
项目定位:把前序技术"拧成一股绳"的收官之作
在 JavaScript 组织代码这一阶段,你已经先后接触了对象构造函数、工厂函数与模块模式(IIFE)、ES6 模块(import/export)、npm 包管理、Webpack 打包以及 JSON 数据格式。Todo List 项目的意义在于:它不是引入新知识,而是要求你把上述技术全部应用到同一个真实项目中,通过实践检验对闭包、模块化、依赖管理、数据序列化这些概念的掌握程度。
与上一个 Library 项目(图书管理)相比,Todo List 在架构上更进一步:Library 主要练习"用一个数组 + 构造函数管理一组对象",而 Todo List 要求你引入多级数据组织(项目 → 任务)、应用逻辑与 DOM 彻底分离以及浏览器本地持久化,复杂度上了一个台阶。
因此在动手写代码之前,务必先花几分钟规划项目结构:目录怎么分、模块怎么拆、数据流怎么走。规划得越清晰,后面写代码越顺利。
动态创建 todo 对象:工厂函数、构造函数与类的选型
Todo List 的第一个硬性要求是:todos 是动态创建的对象,必须使用工厂函数、构造函数或 class 来生成,而不是手工写一个个对象字面量。
用工厂函数实现(推荐起点)
在 factory_functions_and_module_pattern.md 中已经详细讲过:工厂函数就是"返回对象的普通函数",它借力闭包实现私有变量,无需new关键字调用。一个 todo 工厂可以这样设计:
function createTodo(title, description, dueDate, priority) { let completed = false; const getCompleted = () => completed; const toggleCompleted = () => { completed = !completed; }; return { title, description, dueDate, priority, getCompleted, toggleCompleted }; }这里completed状态被刻意做成"私有变量":外部无法直接todo.completed = true绕过逻辑,只能通过toggleCompleted()改变它——这正是 private variables 与闭包 一节的实战应用。工厂函数还天然支持组合:如果你想给 todo 增加"清单(checklist)"能力,可以通过Object.assign组合多个来源的对象,这比继承更灵活。
用 class 实现
如果你更习惯 classes.md 中介绍的 ES6 class 语法,也可以这样写:
class Todo { #completed = false; // 私有字段 constructor(title, description, dueDate, priority) { this.title = title; this.description = description; this.dueDate = dueDate; this.priority = priority; } get completed() { return this.#completed; } toggleCompleted() { this.#completed = !this.#completed; } }class 语法在底层仍是基于原型链,但它提供了更显式的constructor、getter/setter与#私有字段语法,适合偏好面向对象风格的开发者。两种方案都能满足"动态创建 todo 对象"的要求,选哪种取决于你更熟悉哪种模式——保持一致即可,不必在同一个项目里混用。
设计 todo 的属性:从最小集到增强集
作业要求 brainstorm(头脑风暴)todo 对象的属性,并给出一个最低标准:
| 属性 | 说明 |
|---|---|
title | 任务标题(必填) |
description | 任务详细描述(必填) |
dueDate | 截止日期(必填) |
priority | 优先级(必填) |
notes | 备注,可选扩展 |
checklist | 子任务清单,可选扩展 |
completed | 完成状态,用于"设为完成"功能(逻辑上必需) |
最低标准之外,可以自由发挥。比如priority建议用固定枚举值(如low/medium/high)而不是自由文本,这样便于后续用颜色区分;checklist可以是子任务对象数组,同样用工厂或类生成。设计属性时始终记住:这些属性最终会通过 JSON 序列化写入 localStorage,因此只放可序列化的数据(字符串、数字、布尔、数组、普通对象),函数与方法不直接存储(详见下文持久化章节)。
用 Projects 组织任务:默认项目 + 多项目切换
Todo List 的核心数据结构是两级嵌套:Projects(项目)→ Todos(任务)。
作业明确要求:
- 应用首次打开时,必须存在一个"默认项目",所有新建 todo 默认放入其中;
- 用户可以创建新项目,并在创建 todo 时选择它要归属的项目。
典型的数据结构如下:
// 项目对象:包含一个 todo 数组 function createProject(name, todos = []) { return { name, todos }; } // 应用状态:一个项目数组,索引 0 作为默认项目 let projects = [createProject("默认项目")]; // 新建 todo 并放入指定项目 function addTodoToProject(project, todo) { project.todos.push(todo); saveToLocalStorage(); // 每次变更后立即持久化 }在设计阶段就应想清楚几个问题:如何定位一个项目(用索引、名称还是唯一 id)?删除 todo 时如何从对应数组移除?切换项目视图时如何重新渲染?借鉴 project_library.md 的经验,为每个 todo 生成一个唯一id(可用crypto.randomUUID())并在 DOM 元素上用data-*属性与之关联,是处理"增删改"时保持数据与界面同步的稳妥方案。
模块化:把应用逻辑与 DOM 操作彻底分开
这是整个项目最重要的一条架构纪律:应用逻辑(创建 todo、标记完成、修改优先级、项目增删、数据持久化)与 DOM 相关代码(渲染列表、绑定事件、打开编辑弹窗)必须放在不同的模块里。
课程在 es6_modules.md 中讲解了 ES6 模块的export/import语法与入口点概念,在 webpack.md 中讲解了如何用 Webpack 把这些模块打包进dist。一个推荐的目录划分:
src/ ├── index.js # 入口点:初始化应用、首次渲染 ├── todo.js # todo 工厂/类:生成 todo 对象 ├── project.js # project 工厂/类:生成项目对象 ├── storage.js # localStorage 读写:序列化与反序列化 ├── dom/ │ ├── render.js # 渲染项目列表与 todo 列表 │ └── events.js # 表单提交、按钮点击等事件绑定 └── template.html # HtmlWebpackPlugin 使用的模板模块之间的依赖关系是单向的:dom/模块只负责"读状态 + 渲染 + 转发用户操作",它调用todo.js/project.js的工厂生成新对象,调用storage.js做持久化;而todo.js/storage.js完全不碰 DOM。这样带来的直接好处是:未来改用别的渲染方式(比如框架)时,核心数据逻辑一行都不用改——这正是 project_library.md 中"把显示逻辑与数据结构视为不同实体"理念的延续与强化。
模块之间通过命名导出互通,例如:
// storage.js export function saveProjects(projects) { /* ... */ } export function loadProjects() { /* ... */ } // index.js(入口点) import { loadProjects } from "./storage.js"; import { renderProjects } from "./dom/render.js";UI 功能清单:四条必做交互
界面外观可以自由设计,但必须满足以下四个功能点:
- 查看所有项目:侧边栏或顶栏列出全部 project,当前选中的项目有明确高亮;
- 查看每个项目内的所有 todo:通常只显示标题与截止日期,不同
priority用不同颜色标识(例如 high 红色、medium 橙色、low 绿色); - 展开单个 todo 查看/编辑详情:点击可展开显示
description、notes、checklist等完整字段,并支持编辑保存; - 删除 todo:每个 todo 上有删除按钮,点击后从对应项目的
todos数组中移除并重新渲染。
可以参考 Todoist、Things、any.do 这三款经典待办应用的界面截图与介绍视频寻找灵感——重点观察它们如何处理"项目侧栏 + 任务主区 + 详情面板"的布局,以及编辑、删除、完成状态切换的交互方式。
用 npm 引入 date-fns 处理日期
由于项目已经基于 Webpack 工作(见 webpack.md 与 npm.md),引入第三方库非常方便。作业建议使用date-fns来格式化与操作日期:
npm install date-fns它按需导出、tree-shaking 友好,例如在渲染截止日期时:
import { format, isAfter } from "date-fns"; // 将存储的日期字符串格式化为人类可读形式 const prettyDueDate = format(new Date(todo.dueDate), "yyyy-MM-dd"); // 判断任务是否已过期 const overdue = isAfter(new Date(), new Date(todo.dueDate));注意:date-fns 这类库的代码会随 Webpack 一起被打包进dist/main.js,因此应作为普通依赖(dependencies)安装,而不是开发依赖(--save-dev)。如果不用 date-fns,也可以手写原生Date方法,但日期格式化与比较的边界情况很多,成熟库能省去大量踩坑时间。
用 localStorage 实现数据持久化
到目前为止你还没有学过任何服务端存储技术,因此刷新页面后 todos 会全部消失。作业要求用Web Storage API 的 localStorage把数据保存在用户浏览器本地,实现持久化。
基本读写流程
localStorage 只能存字符串,而我们的数据结构是嵌套对象,所以必须借助 JSON 序列化。这一点在 json.md 中已有铺垫:JSON.stringify()把对象转为字符串,JSON.parse()把字符串还原为对象。
// storage.js const STORAGE_KEY = "todoApp.projects"; // 每次创建新项目或新 todo 后调用:保存 export function saveProjects(projects) { localStorage.setItem(STORAGE_KEY, JSON.stringify(projects)); } // 应用首次加载时调用:读取 export function loadProjects() { const raw = localStorage.getItem(STORAGE_KEY); return raw ? JSON.parse(raw) : []; }三个必须注意的坑
坑一:读取时数据可能不存在。用户第一次打开应用时 localStorage 里什么都没有,getItem返回null。必须先判空再解析,否则JSON.parse(null)会直接抛错导致整个应用崩溃:
const raw = localStorage.getItem(STORAGE_KEY); const projects = raw ? JSON.parse(raw) : [createProject("默认项目")];坑二:JSON 存不了函数,方法不会自动回来。这是最隐蔽的问题:JSON.stringify会静默丢弃对象上的方法,因此从 localStorage 读回来的 todo 对象只有数据属性,没有任何方法(比如工厂函数返回的toggleCompleted会消失)。你必须自己设计"还原"逻辑——例如读回纯数据后,用工厂函数或类重新包装一遍,把方法挂回去:
function hydrateTodo(rawTodo) { const todo = createTodo(rawTodo.title, rawTodo.description, rawTodo.dueDate, rawTodo.priority); if (rawTodo.completed) todo.toggleCompleted(); return todo; }坑三:只把要存的字段存进去。反过来写的时候也一样,确认存入的是可序列化的纯数据(字符串、数字、布尔、数组、普通对象),不要尝试把闭包里的私有变量或方法"存"进 JSON——它们本来就无法被序列化。
用 DevTools 检查 localStorage
浏览器开发者工具(DevTools)可以实时查看 localStorage 内容:打开Application面板,在Storage分组下点击Local Storage选项卡。应用中每执行一次setItem(新增、更新、删除数据),这里都会即时刷新。这是调试持久化逻辑最直接的手段:可以确认数据是否写入、JSON 结构是否符合预期、删除操作是否真正生效。
建议的开发顺序与自查清单
为避免在项目中途陷入"逻辑改不动、界面一锅粥"的困境,建议按以下顺序推进:
- 先写
todo.js/project.js的工厂或类,用控制台手动创建对象、验证方法; - 写
storage.js的存取函数,验证序列化/反序列化往返一致(尤其验证方法是否丢失); - 写入口点
index.js,初始化默认项目,从 localStorage 恢复数据(不存在则创建默认项目); - 写
dom/render.js,实现"查看所有项目"与"查看项目内所有 todo"两个基础视图; - 补上"新建 todo 表单"(记得
event.preventDefault()阻止表单默认提交行为)、"展开详情编辑"与"删除 todo"三个交互; - 引入 date-fns 优化日期显示与过期判断;
- 全程用 Webpack 打包并在
npx webpack serve开发服务器下验证,刷新页面确认数据不丢失。
完成后再对照以下问题自查:todo 是否由工厂/类动态生成?属性是否覆盖 title、description、dueDate、priority(及你扩展的字段)?是否有默认项目且能创建/切换新项目?应用逻辑与 DOM 是否在不同模块?四个 UI 功能是否齐全?刷新后数据是否还在?如果全部通过,恭喜——你已经完成了一个架构清晰、可持久化、可扩展的完整前端应用,这正是本课程 JavaScript 组织代码阶段所有知识的综合演练场。
【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考