本文还有配套的精品资源点击获取简介直接可用的微信小程序待办事项管理项目支持任务添加、修改、删除、完成状态切换等核心操作所有功能基于微信原生API实现不强制依赖Redux或第三方框架。项目结构清晰src目录存放主逻辑代码libs目录封装常用工具方法README.md提供详细接入指引。内置多张真实界面截图直观展示列表页、新增页、编辑页等交互效果。数据采用wx.setStorageSync和wx.getStorageSync进行本地持久化适合理解小程序数据绑定机制、页面生命周期onLoad/onShow/onReady及简易状态管理实践。附带.gitignore和MIT协议LICENSE文件符合标准开源规范可一键导入微信开发者工具运行调试也方便在此基础上扩展分类、优先级、日期提醒等功能。remote-redux-devtools.gif说明曾支持远程调试集成但主体功能完全独立轻量易读新手友好。1. 项目概述为什么这个Todo清单值得你花10分钟打开它我做过不下20个微信小程序教学项目从计算器到天气预报再到电商首页仿写但真正能让我在带新人时第一节课就拿出来跑通、讲透、改出新功能的只有两类一个是“点击计数器”另一个就是“Todo清单”。前者太单薄后者刚好卡在能力平衡点上——它不复杂到让人望而却步又足够完整能把小程序开发里最核心的几根“筋”全牵出来页面跳转与传参、数据双向绑定、生命周期钩子的实际调度、本地持久化策略选择、状态驱动UI更新逻辑、目录结构组织逻辑。而眼前这个源码包不是网上那种删了注释、缺了样式、README里写着“自行补全app.js”的半成品它是我在2024年6月用最新版微信开发者工具v1.07.2405300实测通过、截图真实、目录干净、连.gitignore都按小程序最佳实践配好的“可交付级”教学样本。关键词里写的“微信小程序、Todo清单、本地存储、源码包、任务管理”每一个都不是虚词。它不叫“Todo Demo”而叫“Todo清单”意味着它默认就支持中文语境下的交互习惯比如长按删除、左滑标记完成、输入框失焦自动保存、空列表友好提示它强调“本地存储”不是简单调用wx.setStorageSync就完事而是把“什么时候存、存什么、存之前要不要去重、存失败怎么兜底”这些新手根本不会想但上线必踩的细节全揉进了src/utils/storage.js里它标榜“源码包”就真给你一个解压即开、导入即跑的文件夹没有npm install、没有构建脚本、没有环境变量配置——因为原生小程序压根不需要。你甚至不用懂ES6模块语法require(../../libs/dateUtils)这种写法和十年前写jQuery插件一样直白。它适合三类人刚学完官方文档前五章的小白想快速验证某个API行为的中级开发者以及需要给实习生布置“加个截止日期筛选”作业的团队负责人。它不炫技但每行代码都在回答一个问题“微信小程序里这件事到底该怎么干才对”2. 整体设计思路拆解轻量不是偷懒是克制后的精准2.1 为什么放弃Redux坚持原生setData看到remote-redux-devtools.gif这个文件名很多人第一反应是“哦用了Redux”。但翻开源码你会发现整个src/目录下根本没有store/文件夹app.js里也没有createStore调用pages/index/index.js里所有数据更新都是this.setData({ todos: newTodos })。那个GIF只是作者某次技术验证时留下的快照并非当前主干功能依赖项。这背后是明确的设计取舍Todo清单的本质是单页状态管理而非跨页面复杂状态流。用户99%的操作发生在index列表页和add/edit表单页之间数据流向清晰——新增/编辑后回传删除直接操作本地数组完成状态切换只改一个布尔值。引入Redux会带来至少三层冗余Action Creator函数、Reducer纯函数、Store实例维护。而原生setData配合页面data对象加上onLoad时从本地读取、onHide时强制保存的策略完全能覆盖所有场景且调试时console.log一眼就能看到当前状态快照。我试过把这套逻辑强行套进Redux结果是代码行数增加3倍mapStateToProps写错两次导致列表不刷新最后还得靠console.log(store.getState())来定位问题——而原生方案里console.log(this.data.todos)就能解决90%的状态疑问。2.2 目录结构为何这样分src/、libs/、static/的边界在哪很多新手拿到源码第一件事是乱改路径结果require报错。这个包的目录划分其实是微信小程序工程规范的微缩样板src/是绝对核心放所有与业务强耦合的代码。pages/下每个子目录对应一个路由页面index/,add/,edit/components/里放可复用的自定义组件比如todo-itemapp.js和app.json是全局入口和配置。这里不放工具函数因为工具函数不该知道页面逻辑。libs/是纯粹的“能力提供者”。比如dateUtils.js只做一件事把new Date()转成“今天 14:30”或“6月12日 周三”这样的字符串它不关心这个时间是创建时间还是截止时间validator.js只校验字符串是否为空、长度是否超限不决定“标题为空时该弹Toast还是禁用按钮”。它们被设计成无状态、无副作用的纯函数集合可以被任何页面require也可以轻松抽离成npm包。static/虽然源码里没显式建这个文件夹但README.md提到的截图和index.html暗示了它的存在专放静态资源。界面截图放在static/screenshots/下用相对路径引用index.html是为方便本地预览生成的简易HTML壳它不参与小程序编译只是开发者自查UI效果的辅助工具。这种分法的好处是当你需要扩展“按优先级排序”功能时只需在src/pages/index/index.js里加一个sortTodosByPriority方法再在WXML里绑定bindtapsortTodosByPriority完全不用动libs/里的代码而如果要升级日期显示格式改libs/dateUtils.js一个函数即可所有用到它的页面自动生效。边界清晰修改成本可控。2.3 本地存储策略为什么选wx.setStorageSync而不是云开发项目摘要里强调“基于微信原生API实现”其中wx.setStorageSync和wx.getStorageSync是关键。有人会问现在不是都推云开发吗为什么不用云数据库存Todo答案很实在学习成本与目标匹配度。云开发需要开通环境、配置安全规则、理解wx.cloud.callFunction异步流程、处理网络异常而Todo清单的核心教学目标是理解“数据如何从内存落到磁盘再读回来”。wx.setStorageSync是同步阻塞调用try...catch就能捕获存储失败比如用户关闭了小程序权限wx.getStorageSync返回的就是一个JS数组直接赋值给this.data.todos就能触发视图更新。我对比过两种方案的调试体验用云开发时console.log(res.result)要等网络请求完成中间有几百毫秒空白期新手容易误以为代码卡死而wx.getStorageSync(todos)执行完立刻打印出数组节奏感极强。更重要的是wx.setStorageSync有10MB容量上限对于个人待办事项存十年都够用——它不是技术妥协而是对学习场景的精准适配。3. 核心细节解析与实操要点那些README里没写的“为什么”3.1 数据结构设计一个Todo对象字段不多但个个有讲究打开src/pages/index/index.js你会看到初始化data时定义的todos数组示例todos: [ { id: todo-1, title: 买牛奶, completed: false, createdAt: 2024-06-10 14:22:35, updatedAt: 2024-06-10 14:22:35 } ]这个结构看似简单但每个字段都有明确意图id必须是字符串而非数字。微信小程序的wx:key要求key值唯一且稳定数字ID在数组排序后可能重复比如两个Todo都叫1而todo- Date.now()生成的字符串天然唯一。我试过用Math.random().toString(36).substr(2, 9)但发现当用户快速连点添加按钮时毫秒级时间戳比随机数更可靠。title不做trim处理。很多教程一上来就title.trim()但实际场景中“ 空格开头的任务 ”也是合法输入应该由业务逻辑决定是否过滤。这个包选择保留原始输入把清理逻辑交给后续的校验层libs/validator.js里有isEmptyOrWhitespace方法。completed布尔值但WXML里绑定checked{{item.completed}}时必须确保它永远是true或false不能是undefined。所以新增Todo时代码里强制completed: false避免首次渲染出现checkbox未定义的警告。createdAt和updatedAt都用new Date().toLocaleString()生成格式统一为“YYYY-MM-DD HH:mm:ss”。这里有个坑toLocaleString()在不同手机系统上可能返回不同格式iOS带逗号安卓不带所以libs/dateUtils.js里封装了formatDate(new Date(), YYYY-MM-DD HH:mm:ss)内部用正则标准化输出确保排序时字符串比较能正确反映时间先后。提示如果你要扩展“截止日期”功能不要直接往Todo里加dueDate: 2024-12-31。更好的做法是加一个dueDate: new Date(2024-12-31)然后在setData前用dateUtils.formatDate(dueDate, YYYY-MM-DD)转成字符串存这样既能保证存储格式统一又能在JS层直接用Date对象做计算比如判断是否逾期。3.2 页面生命周期钩子onLoad、onShow、onReady谁该干啥新手常混淆这三个钩子。在这个Todo清单里它们的分工非常明确onLoad只做一次性初始化。比如从本地存储读取初始数据const savedTodos wx.getStorageSync(todos) || []然后this.setData({ todos: savedTodos })。它只在页面首次加载时触发比如从首页点击进入Todo页。注意这里不处理“从编辑页返回后刷新列表”因为onLoad不会再次触发。onShow负责每次页面可见时的状态同步。比如用户在add页添加了一个新Todo点击“完成”后返回index页此时onShow会触发。代码里会再次调用wx.getStorageSync(todos)确保列表显示的是最新数据。这是防止页面状态滞后最关键的钩子。onReady只做DOM就绪后的UI操作。比如需要获取某个元素的宽高wx.createSelectorQuery()或者启动一个定时器如倒计时组件。在这个Todo清单里onReady是空的因为不需要操作原生节点。我踩过的坑是曾把wx.getStorageSync放到onReady里结果页面第一次打开时列表空白——因为onReady在onLoad之后触发但setData还没执行this.data.todos还是空数组。后来改成onLoad里读取setDataonShow里再读取一次并对比如果数据有变则setData彻底解决了状态不同步问题。3.3 WXML数据绑定与事件传递bindtap和catchtap的区别在哪index.wxml里删除按钮的写法是view classtodo-item__actions button bindtaphandleDelete>// src/pages/index/index.js Page({ data: { todos: [] }, // 新增按创建时间倒序排列 sortTodosByTime() { const sorted this.data.todos.sort((a, b) { // 将字符串时间转为时间戳比较 const timeA new Date(a.createdAt).getTime(); const timeB new Date(b.createdAt).getTime(); return timeB - timeA; // 降序 }); this.setData({ todos: sorted }); }, // 在onLoad里调用一次确保初始加载就排序 onLoad() { const savedTodos wx.getStorageSync(todos) || []; this.setData({ todos: savedTodos }); this.sortTodosByTime(); // 加这一行 } });第二步在WXML里加触发按钮!-- src/pages/index/index.wxml -- view classheader text classtitle我的待办清单/text button bindtapsortTodosByTime sizemini typedefault按时间排序/button /view第三步优化用户体验——避免重复排序上面的sortTodosByTime每次点击都重新排序但数组本身没变。更好的做法是加个状态标记data: { todos: [], isSortedByTime: false }, sortTodosByTime() { let sorted [...this.data.todos]; // 浅拷贝避免直接修改原数组 if (this.data.isSortedByTime) { // 如果已排序就恢复原始顺序按ID升序 sorted.sort((a, b) a.id.localeCompare(b.id)); this.setData({ todos: sorted, isSortedByTime: false }); } else { sorted.sort((a, b) new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); this.setData({ todos: sorted, isSortedByTime: true }); } }这样点击一次升序再点一次降序体验更自然。这个小功能就把setData的不可变性、数组排序、状态管理全串起来了。4.3 本地存储增强失败时的优雅降级方案wx.setStorageSync不是万能的。当用户手动关闭小程序存储权限或磁盘空间不足时它会静默失败。源码里只是简单调用没做兜底。我们可以加一层防护// src/utils/storage.js export function saveTodos(todos) { try { wx.setStorageSync(todos, todos); console.log([Storage] Todos saved successfully); } catch (e) { console.error([Storage] Save failed:, e); // 降级方案存到内存下次onShow时尝试重存 getApp().globalData.tempTodos todos; // 弹Toast提示用户 wx.showToast({ title: 保存失败请检查存储权限, icon: none, duration: 2000 }); } } export function loadTodos() { try { const saved wx.getStorageSync(todos); if (saved Array.isArray(saved)) { console.log([Storage] Todos loaded from storage); return saved; } } catch (e) { console.error([Storage] Load failed:, e); } // 降级返回内存缓存或空数组 return getApp().globalData.tempTodos || []; }然后在index.js里替换原来的存储调用// 替换原来的 wx.setStorageSync(todos, newTodos) import { saveTodos } from ../../utils/storage; // ... saveTodos(newTodos);这个改动很小但让应用健壮性提升一个档次。它教会你的不是API用法而是如何在不确定的环境中构建确定的用户体验。5. 常见问题与排查技巧实录那些只有亲手调试才会遇到的坑5.1 “添加后列表不更新”问题排查树这是新手提问最高频的问题原因往往不在逻辑而在细节。我整理了一个速查表现象最可能原因快速验证方法解决方案点击“完成”后页面没变化但控制台没报错setData调用位置错误比如写在wx.showModal的success回调外在handleAdd方法开头加console.log(start add)结尾加console.log(end add)看是否执行到结尾确保this.setData在异步操作如wx.showToast的success回调内或使用Promise链式调用列表显示旧数据重启小程序才更新onShow里没重新读取存储或读取后没setData在onShow里加console.log(onShow triggered, todos length:, this.data.todos.length)在onShow里调用loadTodos()并setData或监听onShow事件后主动刷新新增Todo后列表多出一个空项input组件的bindinput事件里this.setData({ inputValue: e.detail.value })写成了this.setData({ inputValue: e.detail })console.log(e.detail)看输出是不是一个对象e.detail.value才是输入内容e.detail是整个事件对象删除后列表项还在但数据已消失handleDelete里filter方法写错比如todos.filter(todo todo.id ! id)写成todos.filter(todo todo.id id)console.log(filtered todos:, filtered)看数组长度filter返回新数组必须赋值给变量再setData实操心得我曾经为一个“列表不更新”问题调试了两小时最后发现是app.json里window配置里navigationBarTitleText写成了中文引号“”而不是英文引号”“导致整个JSON解析失败app.js都没执行。所以遇到诡异问题先看控制台顶部有没有红色JSON parse error。5.2 真机预览“白屏/空白页”终极排查指南扫码后手机上一片空白是开发者工具最让人抓狂的场景。按以下顺序逐项检查检查app.js导出打开wechat-todo-demo/app.js确认最后一行是App({})的闭合括号})且前面没有多余的逗号或分号。检查app.json页面路径确认pages数组里的路径比如pages/index/index对应的文件夹wechat-todo-demo/pages/index/下必须有index.js、index.wxml、index.wxss、index.json四个文件。缺任何一个都会白屏。检查WXML语法错误打开index.wxml看有没有未闭合的标签比如view没写/view或者block wx:for{{todos}}后面忘了/block。微信小程序对WXML语法错误极其敏感一个写成全角符号都会导致白屏。检查JS中的this指向在index.js的onLoad里如果写了setTimeout(function() { this.setData(...) }, 100)这里的this会丢失变成undefined。必须用箭头函数setTimeout(() { this.setData(...) }, 100)或提前const that this。清空开发者工具缓存微信开发者工具右上角【详情】→ 【本地缓存】→ 【清空缓存】→ 重启工具。有时候旧的编译缓存会导致奇怪问题。5.3 “截图不显示”问题静态资源引用的三个致命陷阱README.md里说“内置界面截图”但你在WXML里写image src/static/screenshots/list.png/image却看不到图原因通常是路径错误/static/是绝对路径表示项目根目录下的static文件夹。但源码包里截图可能放在wechat-todo-demo/根目录而不是wechat-todo-demo/static/。正确路径应该是/screenshots/list.png假设截图和app.js同级。文件名大小写Windows系统不区分大小写但iOS真机会区分。list.PNG和list.png是两个文件。确保文件名和WXML里写的完全一致。图片格式不支持微信小程序只支持.png、.jpg、.jpeg、.gif。如果你用Sketch导出的list.svg是无法显示的。用Photoshop或在线转换工具转成PNG。我建议的做法是把所有截图统一放在wechat-todo-demo/images/文件夹下WXML里用/images/list.png引用。这样路径清晰不易出错。6. 后续扩展建议从Todo清单到个人知识库的跃迁路径这个Todo清单的价值不仅在于它能跑通更在于它是一块“可生长的土壤”。我根据实际带团队的经验列出了三条平滑的扩展路径每一步都保持原有代码结构不变6.1 加入分类管理用“标签”代替“文件夹”很多用户想要“工作”、“生活”、“学习”分类。最简单的实现不是新建页面而是给每个Todo加一个category字段// 新增Todo时 const newTodo { id: todo-${Date.now()}, title: this.data.inputValue, completed: false, category: 工作, // 默认值 createdAt: new Date().toLocaleString() };然后在index.wxml里加一个分类筛选器view classfilter-bar button bindtapfilterByCategory style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />简介直接可用的微信小程序待办事项管理项目支持任务添加、修改、删除、完成状态切换等核心操作所有功能基于微信原生API实现不强制依赖Redux或第三方框架。项目结构清晰src目录存放主逻辑代码libs目录封装常用工具方法README.md提供详细接入指引。内置多张真实界面截图直观展示列表页、新增页、编辑页等交互效果。数据采用wx.setStorageSync和wx.getStorageSync进行本地持久化适合理解小程序数据绑定机制、页面生命周期onLoad/onShow/onReady及简易状态管理实践。附带.gitignore和MIT协议LICENSE文件符合标准开源规范可一键导入微信开发者工具运行调试也方便在此基础上扩展分类、优先级、日期提醒等功能。remote-redux-devtools.gif说明曾支持远程调试集成但主体功能完全独立轻量易读新手友好。本文还有配套的精品资源点击获取