拓冰建站拓冰建站
首页 / 资讯中心 / 正文

微信小程序备忘录源码解析:本地存储与setData优化实践

简介这是一份微信小程序备忘录/待办清单项目源码基于常见todolist结构实现适合小程序初学者或前端开发者学习页面布局、数据绑定、事件交互与本地缓存等核心机制。压缩包体量轻巧仅539KB共21个文件涵盖js逻辑脚本、wxml页面结构、wxss样式表、json配置文件以及运行效果截图和markdown说明文档便于对照代码理解整体实现思路。该资源目前已吸引1715人学习下载具有一定参考价值。通过完整源码和配图读者能快速搭建一个可运行的备忘录应用也可作为课程设计、毕业设计或个人练手的起步模板帮助掌握微信小程序从页面编写到数据处理的完整流程。1. 微信小程序备忘录为什么值得把源码完整读一遍把“微信小程序 备忘录截图源码”这个标题拆开它其实指向一个很具体的工程目标用一套本地存储方案把 CRUD、列表渲染和输入状态管理完整串成一个能发布的微信小程序项目实例。刷到的截图往往只展示 UI 好不好看真正决定这个备忘录能不能长期用的是 setData 的更新路径、storage 的读写时机和键盘弹起时的布局处理。下面按“数据层—界面层—体验层—存储升级”的顺序把这个项目从选型写到边界验证。适合刚跑通第一个微信小程序、想用项目验证本地存储能力的开发者有经验的人可以直接跳到第 5 章看同步转异步和防抖的参数取舍。2. 微信小程序备忘录的数据层本地存储选型与读写边界2.1 备忘录不接云开发wx.setStorageSync 就够很多人拿到“备忘录”需求第一反应是接云开发数据库。这个选择不是错是不划算。备忘录的数据天然是单机、私有、量小的一个人每天记几条一年也就几百条单条正文撑死几千字。云开发引入网络请求、权限系统和异步回调把最简单的题做复杂了。常见做法是直接用本地缓存 wx.setStorageSync把整个列表序列化成 JSON塞进一个 key。本地缓存的边界先要说清楚微信小程序本地缓存常见限制是总容量 10MB、单个 key 上限 1MB卸载小程序或用户在设置里手动清理会清空全部缓存。10MB 对纯文本备忘录是奢侈的一条 500 字的记录按 UTF-8 编码约 1.5KB理论上能存几千条。真正的风险不在容量而在于把“读写缓存”散落在每个页面里后面加字段、换 key 时要改十几个地方。源码里值得抄的不是某一行的写法而是那一层 store 封装。这个项目用原生小程序语言写不需要引入 uniapp 或 HBuilderX 那套跨端框架如果团队项目已经跑在 uniapp 里也可以把这层 store 平移到 uni.setStorageSyncAPI 形状几乎一致唯一的差别是返回值和异常行为略有不同。跨端场景下建议把 store 独立成 utils 目录下不带 Page 引用的纯模块这样无论原生还是 uniapp 都能直接复用。2.2 数据结构与读写封装id 用字符串不用时间戳先定数据结构。我的习惯是数组套对象每个备忘录包含 id、content、createdAt、updatedAt 四个字段。id 不要用时间戳同一毫秒内新建两条会撞生成方式取“时间戳 随机数”转字符串保证即使批量导入也不会重复。// utils/memoStore.js const LIST_KEY memo_list_v2 function randomId() { return Date.now().toString(36) Math.random().toString(36).slice(2, 8) } function loadMemos() { // wx.getStorageSync 在 key 不存在时返回空字符串需要统一归一化 const raw wx.getStorageSync(LIST_KEY) if (!raw) return [] let list raw if (typeof raw string) { try { list JSON.parse(raw) } catch (e) { return [] } } return Array.isArray(list) ? list : [] } function saveMemos(list) { // 写入前强制过滤一次非法项避免脏数据被持久化 const clean list.filter((item) item typeof item.content string) wx.setStorageSync(LIST_KEY, JSON.stringify(clean)) } module.exports { loadMemos, saveMemos, randomId }loadMemos 里做了三重防御key 不存在时返回空数组字符串解析失败时返回空数组解析结果不是数组时也返回空数组。saveMemos 在写盘前过滤非法项防止页面误传 null 进列表。参数上值得注意两点一是 wx.setStorageSync 的 data 虽然是 any 类型但建议显式传 JSON 字符串后续排查数据时可以直接在调试器的 Storage 面板里人读二是 key 带版本号_v2做数据结构升级时可以写一个迁移函数读到旧 key 就转换后写入新 key 再删旧 key这个习惯能省掉未来很痛苦的兼容代码。写入时机也要约定。新建和编辑保存时调 saveMemos 没问题但删除、排序、批量修改都往同一个函数里塞就会在多个页面出现重复的“读-改-写-序列化”代码。更稳的做法是把 store 做成单例内部维护一份内存里的 list页面只调 add、update、remove 三个方法每个方法内部负责持久化。缓存是手段内存态才是页面真正读的数据源。页面 onShow 时从 store 内存态取数而不是重新读 storage这样编辑页返回后列表能立即拿到新值也不会产生重复解析。2.3 同步 API 和异步 API 的取舍与容量预警wx.setStorageSync 是同步实现数据量小的时候最简单。它的代价是阻塞 JS 线程如果列表特别大或写入频率高用户会感觉到点按卡顿。微信小程序提供了异步版本 wx.setStorage 和 wx.getStorage回调里拿不到同步返回值适合保存前需要展示 loading、或者批量写入的场景。我一般按这个标准选型场景推荐 API理由进入页面读列表wx.getStorageSync同步拿值首屏渲染不需要 loading每次编辑实时保存wx.getStorageSync单条数据小写入延迟可忽略批量导入超过几百条wx.setStorage避免长事务阻塞交互接云同步、上传前校验wx.setStorage可在 success 回调里串联下一步另外 wx.getStorageInfoSync 可以拿到 currentSize 和 limitSize值得在源码里做一个容量预警saveMemos 内部检查 currentSize 是否接近 limitSize接近时弹 toast 提示用户清理而不是等系统写满后静默失败。单个 key 超 1MB 时同步 API 会直接抛异常这一步必须用 try/catch 包住否则新建一条超长备忘会让整个页面白屏。3. 微信小程序备忘录的界面层textarea 录入、列表渲染与状态联动3.1 用 textarea 做编辑页键盘遮挡是第一个坑备忘录的核心录入控件是 textarea。微信小程序的 textarea 是原生组件层级和滚动行为跟普通 view 不一样最典型的坑是键盘弹起把输入区挡住。解决办法不是调样式而是给 textarea 配 cursor-spacing 和 adjust-position。!-- pages/edit/edit.wxml -- view classeditor-wrap textarea classeditor value{{draft}} maxlength-1 cursor-spacing{{20}} adjust-position{{true}} placeholder记点什么... bindinputonInput show-confirm-bar{{false}} /textarea /view// pages/edit/edit.js const store require(../../utils/memoStore) Page({ data: { draft: , memoId: null }, onInput(e) { // 这里只更新页面态真正的持久化交给防抖保存 this.setData({ draft: e.detail.value }) }, onSave() { const content this.data.draft.trim() if (!content) { wx.showToast({ title: 内容为空, icon: none }) return } const target this.data.memoId ? store.updateMemo(this.data.memoId, content) : store.addMemo(content) if (target) wx.navigateBack() } })参数说明maxlength 默认 140长笔记会被截断做备忘录要显式设成 -1 表示不限制cursor-spacing 是光标与键盘的距离设 20 左右能避免光标被键盘盖住adjust-position 为 true 时页面会自动上推让光标可见show-confirm-bar 控制键盘右上角的“完成”栏默认会占掉输入区下方一块空间不需要换行确认就关掉。如果页面开启了自定义导航栏还要把 textarea 的 fixed 属性设为 true再手动算状态栏高度给输入区留 padding否则部分机型上输入内容会被顶到导航栏后面。编辑页与列表页的通信我习惯用 wx.navigateTo 的 events 参数配合 EventChannel而不是把回传数据塞进 globalData。原因很简单编辑页是独立页面栈节点事件通道能明确表达“这一页把结果交回给前一页”读代码的人不用去全局变量里翻。编辑页 onLoad 时根据 options.id 从 store 里取出原文填进 draftonSave 里判断 id 是否存在来区分新建和更新。3.2 wx:for 渲染列表wx:key 别用 index列表页是备忘录的门面。渲染用 wx:for但 wx:key 的选择直接影响删除和排序后的视图一致性。最省事的写法是用 index但一旦列表中间删掉一项后面的项会整体复用旧组件实例展开状态、滚动位置都可能串。正确做法是渲染时用稳定 id 做 key。!-- pages/index/index.wxml -- view classmemo-list view classmemo-item wx:for{{displayList}} wx:keyid bindtaponTapItem bindlongpressonLongPressItem >// pages/index/index.js const store require(../../utils/memoStore) Page({ data: { memos: [], displayList: [] }, onShow() { // 每次回到列表页都从 store 内存态取数编辑页返回后能立即刷新 const memos store.getList() this.setData({ memos, displayList: memos }) }, onTapItem(e) { const id String(e.currentTarget.dataset.id) wx.navigateTo({ url: /pages/edit/edit?id id }) }, onLongPressItem(e) { const id String(e.currentTarget.dataset.id) wx.showActionSheet({ itemList: [编辑, 删除], success: (res) { if (res.tapIndex 0) { wx.navigateTo({ url: /pages/edit/edit?id id }) } else if (res.tapIndex 1) { this.removeMemo(id) } } }) }, removeMemo(id) { wx.showModal({ title: 确认删除, content: 删除后无法恢复, success: (res) { if (!res.confirm) return store.removeMemo(id) const memos store.getList() this.setData({ memos, displayList: memos }) } }) } })这里有个容易忽略的细节通过>// 只更新第 index 条的 content 字段 this.setData({ [memos[ index ].content]: newContent })用数组下标拼路径是官方支持的能力可以穿透到第二级属性。这里有两个前置条件index 必须在渲染时同步拿到并且列表没有被并发操作改变位置。如果同时支持长按拖拽排序按 index 更新就不可靠了回退到全量 setData 反而更稳。源码里如果 index 和 wx:key 同时出现评审时我会额外盯一眼两边数据是否同源。三种更新方式的取舍可以这样看更新方式适用条件风险点全量 setData({ memos })列表小于 100 条改动频繁数据大时 diff 成本高按下标路径 setData列表长且位置稳定删除或排序后下标错位按 id 局部更新有稳定 id列表位置会变需要额外维护 id 到 index 的映射拖拽排序属于备忘录项目里“评论区需求”级别的高级功能要做的话重点不在手势识别而在排序完成后 store 数组顺序和页面 wx:key 的一致性。排序结果先写回 store再统一 setData 全量数组比逐项移动靠谱。4. 微信小程序备忘录的体验层搜索、日期分组与空状态4.1 关键词搜索过滤时不污染原数组备忘录记多了之后列表页必须能搜。搜索逻辑不复杂但实现上有讲究。常见的错误写法是在原 memos 数组上 filter 之后直接 setData导致清空搜索框后列表回不来因为原数组已经被覆盖了。正确做法是保留 memos 作为数据源每次根据搜索词生成一个新的展示数组。// pages/index/index.js 搜索实现 data: { memos: [], keyword: , displayList: [] }, onSearchInput(e) { const keyword e.detail.value.trim().toLowerCase() const source this.data.memos const displayList keyword ? source.filter((m) m.content.toLowerCase().includes(keyword)) : source this.setData({ keyword, displayList }) }displayList 是只读的展示数组搜索只是一次过滤计算。这里要注意 includes 对大小写敏感中英文混排时统一 toLowerCase 再比。如果还想做命中高亮不要直接在 WXML 里拼字符串把每条内容按命中位置切分成片段数组再用 rich-text 渲染样式可控也不容易出现 XSS 性质的问题因为备忘录内容是自己写入的纯文本。搜索体量再大可以对 store 加一层分词索引但备忘录项目到不了这一步不必为炫技引入额外状态。搜索框的输入事件会高频触发过滤计算放在 setData 里在低端机上能明显感到掉帧处理方案见 5.2 的防抖。4.2 按日期分组把列表变成可扫读的区块备忘录列表的体验上限在分组。纯按时间倒序排长列表很难扫读按“今天/昨天/更早日期”分组后视觉上是区块化的用户可以快速跳过无关区。分组结果是数组套数组每个分组是 { title, items }渲染时用双层 wx:for。// utils/dateGroup.js function formatDay(ts) { const d new Date(ts) const pad (n) (n 10 ? 0 n : n) return d.getFullYear() - pad(d.getMonth() 1) - pad(d.getDate()) } function groupByDate(list) { const groups [] const today formatDay(Date.now()) const yesterday formatDay(Date.now() - 86400000) const map new Map() list.forEach((item) { const day formatDay(item.createdAt) let title day if (day today) title 今天 else if (day yesterday) title 昨天 if (!map.has(title)) map.set(title, []) map.get(title).push(item) }) map.forEach((items, title) { groups.push({ title, items }) }) groups.sort((a, b) { const ta a.items[0].createdAt const tb b.items[0].createdAt return tb - ta }) return groups }分组实现里有三个不能省的点。第一日期字符串用本地时区拼不要用 toISOString否则跨时区用户会看到同一条记录分到错误的组。第二组间排序按组内第一条记录的 createdAt 做降序不能按组名字符串排否则“昨天”排在“今天”前面。第三Map 的遍历顺序就是插入序先插入今天再插入昨天展示顺序自然正确这一步不需要额外 sort。渲染时外层 wx:key 用 title因为分组标题不重复内层仍然用 id。搜索和分组是两条独立的数据流水线页面里用一个 combine 函数把 memos、keyword 一起算出来再 setData比在 onShow、onInput 里各写一遍更不容易漏状态。提示不要用 new Date(2024-01-01) 这种带连字符的字符串直接参与运算iOS 上部分版本解析不一致统一用时间戳或 new Date(2024, 0, 1)。4.3 空状态与进入页面的加载处理空状态是备忘录源码里最容易被忽略的部分。列表为空时如果只渲染一个空白 view用户会以为页面坏了。我一般放一张轻量占位图和一句引导文案同时区分“没有数据”和“搜索无结果”两种空文案分开写场景判断条件展示文案无数据memos.length 0 且 keyword 为空还没有备忘点右下角新建搜索无结果keyword 非空且 displayList 为空没有找到相关备忘加载中首次读取未完成loading 占位骨架首次进入时如果存储里数据较多全同步读会卡。常见做法是 onLoad 先渲染页面骨架或显示 loading再用异步 API 读缓存读完之后 setData 替换骨架。这和“修改刚进入的加载页面”的需求是同一类问题onLaunch 里做初始化、onShow 里刷列表如果两个生命周期里都读存储就有重复初始化开销。我的约定是 onLaunch 只做一次全量读放入 store 内存态页面 onShow 只从内存态取配合 wx.showLoading({ mask: true }) 在数据回来前挡住交互。这个约定还有一个好处onShow 的抖动、下拉刷新都走同一份数据源不会出现列表页比编辑页数据旧的情况。5. 微信小程序备忘录源码进阶存储升级、防抖保存与效果验证5.1 从同步存储平滑迁移到异步 API当备忘录决定加云同步或批量导入时同步存储会变成瓶颈。迁移不是把所有 wx.setStorageSync 机械替换成 wx.setStorage而是要同时处理“写失败重试”和“调用方不知道何时写完”两个问题。// utils/memoStore.js 异步版本 function saveMemosAsync(list) { return new Promise((resolve, reject) { wx.setStorage({ key: LIST_KEY, data: JSON.stringify(list), success: resolve, fail: reject }) }) } async function updateMemoAsync(id, content) { const list loadMemos() const idx list.findIndex((m) String(m.id) String(id)) if (idx -1) { list[idx].content content list[idx].updatedAt Date.now() } try { await saveMemosAsync(list) return { ok: true } } catch (e) { return { ok: false, error: e } } }Promise 封装解决了调用方的时序问题但引出一个新问题多个页面同时触发写时后写的可能覆盖先写的。源码里要加一个简单的写队列或者约定写操作全部收敛到 store 内部串行执行。这里值得记住的是异步化并不是为了更快而是为了不阻塞主线程网络和存储的耗时不会因为异步而消失。迁移后原有调 saveMemos 的页面可以保留同步版本新的批量导入和云同步走异步版本两套 API 在 store 内部共用同一个数据结构互不干扰。5.2 输入防抖把高频 setData 压成一次写入备忘录编辑页的保存策略决定存储写入频率。每敲一个字就写一次 storage键盘输入时 setData 和 JSON 序列化挤在同一帧里低端机明显卡。我的做法是输入时只更新页面 draft停止输入 500 毫秒后再触发持久化。// utils/debounce.js function debounce(fn, wait) { let timer null return function (...args) { if (timer) clearTimeout(timer) timer setTimeout(() { fn.apply(this, args) }, wait || 500) } } // pages/edit/edit.js const saveDebounced debounce(function (content) { store.updateMemo(this.data.memoId, content) }, 500) onInput(e) { this.setData({ draft: e.detail.value }) if (this.data.memoId) { saveDebounced.call(this, e.detail.value) } }注意 debounce 里要用 apply 绑定 this否则回调用到页面的 store 和 data 都会丢失。防抖时间 500 毫秒是输入体验和写入频率的折中点低于 300 毫秒在快速打字时仍然频繁触发高于 800 毫秒会让用户产生“刚才明明保存了怎么重进页面内容是旧的”的错觉。这个参数应该作为可配置项放进 store 的构造参数里而不是写死在函数内。5.3 验证清单清缓存、看 Storage 面板、测边界源码拿到手验证比阅读更花时间。别人贴的截图只能证明 UI 长什么样逻辑对不对要看调试器的 Storage 面板。验证清单从三个方向展开数据完整性、交互边界、时序一致性。数据完整性上清缓存后首次进入确认空状态正常、onLaunch 初始化不报错在 Storage 面板手动改一条数据的 content再触发 onShow确认列表按最新值渲染。交互边界上单条内容超过 1MB 时保存必须失败并有 toast 提示不能白屏列表超过 100 条时滚动和删除操作不掉帧。时序一致性上快速输入后立刻退出编辑页再回到列表页确认防抖触发的写操作没有丢连续删除多条时观察 Storage 面板里的数组长度是否和界面一致。最后再给一个具体的排查技巧在 Storage 面板把某条的 createdAt 改成 2099 年的时间戳然后在列表页触发一次 onShow分组标题会立刻变成对应的新日期。这一下就能验证分组计算、响应式更新和数据源是否走的是同一套链路比反复重启模拟器高效得多。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门