news 2026/9/15 20:06:43

构建模块化 Todo List 应用:Odin 课程 JavaScript 综合实战(工厂函数、ES6 模块与 localStorage 持久化)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
构建模块化 Todo List 应用:Odin 课程 JavaScript 综合实战(工厂函数、ES6 模块与 localStorage 持久化)

构建模块化 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 语法在底层仍是基于原型链,但它提供了更显式的constructorgetter/setter#私有字段语法,适合偏好面向对象风格的开发者。两种方案都能满足"动态创建 todo 对象"的要求,选哪种取决于你更熟悉哪种模式——保持一致即可,不必在同一个项目里混用

设计 todo 的属性:从最小集到增强集

作业要求 brainstorm(头脑风暴)todo 对象的属性,并给出一个最低标准

属性说明
title任务标题(必填)
description任务详细描述(必填)
dueDate截止日期(必填)
priority优先级(必填)
notes备注,可选扩展
checklist子任务清单,可选扩展
completed完成状态,用于"设为完成"功能(逻辑上必需)

最低标准之外,可以自由发挥。比如priority建议用固定枚举值(如low/medium/high)而不是自由文本,这样便于后续用颜色区分;checklist可以是子任务对象数组,同样用工厂或类生成。设计属性时始终记住:这些属性最终会通过 JSON 序列化写入 localStorage,因此只放可序列化的数据(字符串、数字、布尔、数组、普通对象),函数与方法不直接存储(详见下文持久化章节)。

用 Projects 组织任务:默认项目 + 多项目切换

Todo List 的核心数据结构是两级嵌套:Projects(项目)→ Todos(任务)

作业明确要求:

  1. 应用首次打开时,必须存在一个"默认项目",所有新建 todo 默认放入其中;
  2. 用户可以创建新项目,并在创建 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 功能清单:四条必做交互

界面外观可以自由设计,但必须满足以下四个功能点:

  1. 查看所有项目:侧边栏或顶栏列出全部 project,当前选中的项目有明确高亮;
  2. 查看每个项目内的所有 todo:通常只显示标题与截止日期,不同priority用不同颜色标识(例如 high 红色、medium 橙色、low 绿色);
  3. 展开单个 todo 查看/编辑详情:点击可展开显示descriptionnoteschecklist等完整字段,并支持编辑保存;
  4. 删除 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 结构是否符合预期、删除操作是否真正生效。

建议的开发顺序与自查清单

为避免在项目中途陷入"逻辑改不动、界面一锅粥"的困境,建议按以下顺序推进:

  1. 先写todo.js/project.js的工厂或类,用控制台手动创建对象、验证方法;
  2. storage.js的存取函数,验证序列化/反序列化往返一致(尤其验证方法是否丢失);
  3. 写入口点index.js,初始化默认项目,从 localStorage 恢复数据(不存在则创建默认项目);
  4. dom/render.js,实现"查看所有项目"与"查看项目内所有 todo"两个基础视图;
  5. 补上"新建 todo 表单"(记得event.preventDefault()阻止表单默认提交行为)、"展开详情编辑"与"删除 todo"三个交互;
  6. 引入 date-fns 优化日期显示与过期判断;
  7. 全程用 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 20:01:32

FastAPI实战:构建带JWT登录与断点续学的在线课程学习系统

简介:基于FastAPI框架的在线课程学习系统Python源码及项目说明,是一份面向具备一定Python基础的开发者的完整参考实现。系统以在线学习平台为场景,围绕用户、课程、学习、互动、管理五大模块构建,覆盖用户注册登录与信息维护、课程…

作者头像 李华
网站建设 2026/9/15 20:00:03

MATLAB上采样与下采样实战:BPSK链路采样率转换深度解析

简介:资源聚焦数字通信中的BPSK调制与采样率变换,面向信号处理、通信原理方向的学生及MATLAB开发者,提供可直接运行的上采样与下采样实现。BPSK通过载波相位传递0/1信息,上采样在序列中插零提高采样率,下采样则按整数因…

作者头像 李华
网站建设 2026/9/15 19:59:14

Maven构建复杂项目

最近研究开源监控平台Hertzbeat,于是github上fork了这个项目(master版本日期20251120),把代码拉到本地跑起来。Hertzbeat属于父子模块项目,通过前后端进行部署。Maven构建时软件版本要求Maven3,JDK17及以上…

作者头像 李华