微信小程序多功能记事本开发指南:存储、分页与避坑解析
简介:多功能记事本毕业设计项目资料包,面向需要完成Java课程设计或毕业设计的在校生,及希望学习记事本类小程序开发逻辑的初学者。资料覆盖论文文档、前后台Java项目源码、数据库文件与项目截图,从需求分析、功能设计到编码实现全流程,可作为完整方案直接参考。资源包约49.19MB,文件类型以源码、论文文档、数据库文件及项目截图为主,目录结构清晰,按文档、代码、数据库分组存放,便于快速定位。已有240人浏览学习,适合课程设计、毕业设计及答辩展示场景使用。附带的前后台设计能让读者同时了解服务端与客户端的交互方式,在Eclipse、IDEA等环境中导入源码即可运行体验。数据库文件可快速初始化系统数据,截图则辅助核对功能界面。读者可在此基础上扩展标签、搜索、富文本编辑等功能,提升项目完整性与答辩竞争力。
1. 记事本小程序不是“做个输入框”:多功能记事本系统到底在解决什么
很多人拿到“记事本小程序设计多功能记事本系统.zip”这类题目时,下意识会觉得不就是个textarea加保存按钮嘛。真要在微信小程序里把一个能记录文字、待办、图片、标签、搜索、提醒的记事本系统跑起来,难点根本不在输入框,而在数据模型怎么设计、页面状态怎么同步、本地缓存怎么封装。这篇不假装手里有源码,而是按我自己做这类工具型小程序的完整路径拆开讲:从选型、工程骨架、数据模型,到列表加载更多、标签筛选、提醒检查、图片附件,最后落到打包超限和键盘遮挡这些真实翻车点。适合拿来做毕设、课程设计,或者想认真练一个完整小程序项目的开发者。全篇的第一原则是:先封装好存储层,再写页面,否则后面每一项功能都会返工。
2. 记事本小程序的选型与工程骨架:原生还是 uni-app
2.1 工具型记事本,为什么第一版不做后端
记事本这类工具型小程序有个鲜明特点:数据是个人化的,交互链路上基本是“打开 → 查看 → 编辑 → 保存 → 离开”,没有多人协同诉求。因此第一版完全可以用微信小程序本地缓存顶住。很多人一上来就想着云开发、后端数据库,结果连“笔记列表加载更多”都没跑通,先被登录态和接口联调耗掉大半时间。
本地缓存方案里最关键的约束是wx.setStorageSync单条 key 上限 10MB。纯文本场景下,一条笔记算 2KB,10MB 能存几千条,个人记事完全够用;图片不直接存 base64,而是保存本地文件路径,体积压力更小。等到真的需要多端同步、账号体系时再引入后端,迭代顺序才合理。我一般建议的架构演进路线是:本地缓存 → 导出备份 → 云存储 → 云同步,每个阶段都能独立上线,不会一上来就被后端绑死。
2.2 原生小程序和 uni-app 的边界在哪
选型这个问题没有绝对答案,但有明确的适用边界。原生小程序(微信开发者工具直接写的 WXML/WXSS/JS)和 uni-app 是这条路上最常见的两个选择,差异集中在调试定位、多端复用和包体控制上。
| 维度 | 原生微信小程序 | uni-app |
|---|---|---|
| 学习门槛 | 低,语法就是 WXML + JS | 中,需要先接受 Vue 语法 |
| 调试体验 | 微信开发者工具直接定位到源码 | 运行时要经过编译层,报错栈容易绕 |
| 多端复用 | 仅微信 | 一套代码可编译到 H5、App、支付宝小程序等 |
| 包体控制 | 开发者手动控制,直观 | 编译产物容易膨胀,常见打包 2MB 超限 |
| 适合场景 | 微信内只用、功能独立的工具型小程序 | 明确要做多端分发或已有 Vue 技术栈 |
记事本这种功能不复杂、不依赖大量跨端能力的项目,选原生反而顺畅。我自己见过不少新手冲着“uni-app 微信小程序打包”的教程去用 uniapp,结果遇到source size 2612kb exceed max limit 2mb这类编译产物膨胀问题,光砍包体就耗掉两三天,这种踩坑很不划算。反过来,如果你确实要同时做 H5 版本,那 uni-app 是合理选择,但要想清楚代价是调试链路变长。
2.3 最小可运行的工程骨架:app.json 和页面划分
无论选原生还是 uni-app,工程骨架的思路一致:首页负责列表展示,编辑页负责新建和修改,详情页负责只读查看。下面这份app.json是原生小程序里最朴素的配置,注释里标了每个字段的作用。
{ "pages": [ "pages/index/index", "pages/edit/edit", "pages/detail/detail" ], "window": { "navigationBarBackgroundColor": "#ffffff", "navigationBarTitleText": "我的记事本", "navigationBarTextStyle": "black" }, "style": "v2", "sitemapLocation": "sitemap.json" }pages数组的第一项就是小程序启动后的首页,所以列表页要放在最前面。window里的navigationBarTitleText是全局默认标题,后面在编辑页里还会通过wx.setNavigationBarTitle动态覆盖它,这是“小程序动态设置标题”的常见做法。style: "v2"启用新版组件样式,textarea、switch 等组件在视觉上会更协调,不用自己再写一大套 reset 样式。
工程目录上我会额外建一个utils文件夹放存储封装,一个components文件夹放标签选择弹层这类复用组件。页面文件用原生小程序的四件套:.js、.wxml、.wxss、.json,每个页面独立目录,方便后续按分包迁移。
3. 数据模型、列表页和编辑页:把“多功能”拆成可维护的结构
3.1 一个笔记对象应该有哪些字段
多功能记事本最忌讳的是“功能多了,数据字段乱了”。待办要截止时间,随手拍要图片路径,普通记事要正文,如果把三类数据分别建三套存储结构,首页聚合展示时就会写一堆分支判断。我常用的做法是用一个type字段区分类型,再统一成一个笔记结构:
// 一条笔记的完整数据结构 const note = { id: '1720000000000_4821', // 时间戳 + 随机数,保证唯一且可排序 type: 'note', // note 普通记事 / todo 待办 / photo 图文 title: '', // 标题,列表页展示用 content: '', // 正文,textarea 绑定 tags: ['工作', '项目'], // 标签数组,直接冗余在文档里 remindAt: 0, // 提醒时间戳,0 表示不提醒 images: [], // 图片本地文件路径数组 createdAt: Date.now(), // 创建时间 updatedAt: Date.now() // 更新时间,列表排序依据 }type字段的设计很关键。不要在页面层面把“记事”和“待办”做成两个独立表单,那会让列表页的渲染逻辑重复且难维护。用type控制编辑页的差异项:todo类型多显示一个截止时间选择器,photo类型多显示图片上传按钮,其余字段完全复用。id用Date.now() + 随机数拼接,不依赖自增序号,本地删除和导出导入都不会产生主键冲突。updatedAt是列表排序的第一依据,每次保存必须刷新它。
3.2 存储封装:不要在每个页面里直接 setStorageSync
这是整篇文章里我最想强调的一点。很多初学项目里,wx.setStorageSync散落在各个页面的保存函数里,key 名五花八门,今天写noteList,明天写notes,后天又写allNotes,一旦缓存里出现两套数据,整个系统就乱了。正确的做法是建一个独立的存储模块,所有页面只通过它读写。
// utils/noteStore.js const STORAGE_KEY = 'notebook_entries_v1' function getAll() { try { const data = wx.getStorageSync(STORAGE_KEY) return data && Array.isArray(data) ? data : [] } catch (e) { console.error('读取本地笔记失败', e) return [] } } function save(note) { const list = getAll() const index = list.findIndex(item => item.id === note.id) if (index > -1) { list[index] = note } else { list.unshift(note) } try { wx.setStorageSync(STORAGE_KEY, list) return true } catch (e) { console.error('写入本地笔记失败', e) return false } } function remove(id) { const list = getAll().filter(item => item.id !== id) wx.setStorageSync(STORAGE_KEY, list) } module.exports = { getAll, save, remove }getAll做了两件事:一是try-catch包裹,防止小程序在隐私授权异常或存储空间不可写时直接抛错导致页面白屏;二是做了Array.isArray校验,防止缓存里被写入脏数据后整个页面崩溃。save的逻辑是“存在就更新,不存在就插到最前面”,这样首页列表天然按创建时间倒序,不需要额外排序。所有方法统一用STORAGE_KEY,后续如果数据结构升级,只需要把这个常量改成_v2并写一个迁移函数,旧数据不会丢。
3.3 首页列表:分页加载和空态缺一不可
记事本列表页最容易被忽略的是“数据多起来之后的渲染性能”。一次性把几百条笔记全部wx:for渲染出来,在低端手机上会有明显卡顿,这也是热搜里“微信小程序页面列表加载更多”反复被问的原因。标准解法是维护一个分页游标。
// pages/index/index.js 的列表分页核心逻辑 const PAGE_SIZE = 20 let allEntries = [] let currentPage = 0 function loadPage() { const start = currentPage * PAGE_SIZE const end = start + PAGE_SIZE const pageData = allEntries.slice(start, end) this.setData({ displayedEntries: this.data.displayedEntries.concat(pageData), hasMore: end < allEntries.length }) currentPage++ } // 页面触底时触发 onReachBottom() { if (this.data.hasMore) { this.loadPage() } } // 每次从存储重新拉全量数据 function refresh() { allEntries = noteStore.getAll() currentPage = 0 this.setData({ displayedEntries: [], hasMore: true }) this.loadPage() }分页的关键参数是PAGE_SIZE和currentPage。PAGE_SIZE取 20 是权衡了首屏渲染速度和滚动连续性之后比较稳定的值,网络图片多时可以降到 10。注意allEntries存在页面实例上而不是data里,因为全部数据不需要参与视图层渲染,只作为分页的原始数据源,这样能明显减少setData的内容量。空态也不要省:displayedEntries.length === 0时显示“点击右下角新建第一条笔记”的引导页,否则用户打开小程序看到空白页会直接关掉。
3.4 编辑页:保存、草稿和动态标题
编辑页是状态管理最容易翻车的地方,难点集中在三处:编辑和新建的区分、切后台时的草稿保存、导航标题的联动。下面这段逻辑覆盖了这三个点。
// pages/edit/edit.js Page({ data: { noteId: '', type: 'note', title: '', content: '', remindAt: 0 }, onLoad(options) { // 编辑已有笔记时,options 里会带 id if (options.id) { const list = noteStore.getAll() const note = list.find(item => item.id === options.id) if (note) { this.setData({ noteId: note.id, type: note.type, title: note.title, content: note.content, remindAt: note.remindAt }) // 动态设置标题,替代写死的导航栏文案 wx.setNavigationBarTitle({ title: '编辑笔记' }) } } else { // 新建时只透传类型 this.setData({ type: options.type || 'note' }) wx.setNavigationBarTitle({ title: '新建笔记' }) } }, onHide() { // 切后台前自动保存草稿,防止用户输入完忘记点保存 this.saveDraft() }, saveDraft() { const note = { id: this.data.noteId || this.generateId(), type: this.data.type, title: this.data.title.trim(), content: this.data.content, remindAt: this.data.remindAt, updatedAt: Date.now() } noteStore.save(note) }, generateId() { return `${Date.now()}_${Math.floor(Math.random() * 10000)}` } })onLoad里的options.id是列表页跳转时通过wx.navigateTo的url参数传过来的,编辑和新建共用同一个页面,靠这个字段区分。onHide在用户切入后台、被电话打断、或跳转到图片选择时都会触发,此时保存草稿最合适,既不会频繁写缓存,又不会丢输入内容。注意saveDraft里对title做了trim(),但content没做,因为正文允许保留首尾换行。保存成功后如果是新建的笔记,需要把生成的id写回noteId,保证下次保存时走的是更新分支而不是重复插入。
4. 标签、提醒、图片附件和搜索:四个常见“多功能”怎么落地
4.1 标签:用数组字段而不是建关联表
标签功能在小程序里不需要建标签表,直接在笔记对象里冗余一个tags数组就是最务实的方案。个人记事场景下标签数量有限、不需要跨笔记统计,数组查询的损耗可以忽略。筛选时用Array.filter就能完成,没必要提前引入数据库级别的关联设计。
function filterByTag(entries, tag) { if (!tag) return entries return entries.filter(note => { return note.tags && note.tags.includes(tag) }) }标签选择弹层一般放在编辑页底部,用wx:for渲染常见标签,再带一个输入框支持自定义标签。这里有个细节:标签项用放大镜图标加文字,比纯文字列表直观;选中的标签用高亮背景标记,避免用户以为标签是删除操作。存储端唯一要做的是在保存笔记前对tags数组做去重,防止同一个标签被重复添加。
4.2 提醒:小程序没有后台定时,打开时检查最省事
这是很多第一次做提醒功能的开发者会踩的大坑。微信小程序在普通工具类目下没有后台常驻定时能力,wx.setTimeout只在页面存活时有效,用户一离开小程序,定时器就失效了。所以“记事到时间提醒我”这个功能,在小程序里的合理实现是“打开时检查 + 置顶展示”,而不是系统级推送。
// 在首页 onShow 里调用 function checkDueReminders(entries) { const now = Date.now() const dueList = entries.filter(note => { return note.remindAt && note.remindAt <= now }) if (dueList.length > 0) { wx.showToast({ title: `${dueList.length} 条待办到期`, icon: 'none' }) } return dueList }onShow每次从后台切回前台都会触发,天然契合“监听用户离开小程序”的场景:用户在别的地方处理完事情,切回小程序时立刻看到到期提醒。remindAt用时间戳而不是日期字符串,比较大小方便,也可以直接传给new Date()用于展示。如果你确实想要微信通知栏级别的提醒,那需要订阅消息模板,并且要配合自己的服务器在到期时下发,这条路涉及后端,建议放到二期。本地版做到“打开时检查置顶”已经能覆盖记事本场景里九成的提醒诉求。
4.3 图片附件:选图、压缩和转存
记事发图片路径是低成本方案,但这里有个高频翻车点:wx.chooseMedia返回的是临时文件路径,临时文件在用户重启小程序或长时间使用后可能会被系统清理。正确做法是拿到临时路径后立即转存到本地用户目录。
wx.chooseMedia({ count: 9, mediaType: ['image'], sourceType: ['album', 'camera'], success(res) { const tempFilePath = res.tempFiles[0].tempFilePath wx.getFileSystemManager().saveFile({ tempFilePath: tempFilePath, success(fileRes) { const savedPath = fileRes.savedFilePath // 把 savedPath 存入 note.images,而不是 tempFilePath console.log('图片已转存', savedPath) }, fail(err) { console.error('图片转存失败', err) } }) } })count: 9对应微信一次最多选 9 张的限制,这是为了配合朋友圈式的九宫格展示。sourceType: ['album', 'camera']同时允许相册和拍照。saveFile成功后拿到的savedFilePath长这样:wxfile://store_xxx,这个路径在用户换设备后失效,但本地工具型场景下足够了。这里容易被忽略的另一个参数是压缩:chooseMedia默认返回原图,一张照片动辄四五兆,存savedFilePath后缓存体积压力很大。我一般会设置sizeType: ['compressed'],让微信先压缩再返回路径,画质损失在手机端看图几乎感知不到。
4.4 搜索:别等数据量大了再补
搜索功能放到较后期做没问题,但做的时候接口要一次设计对。记事本搜索的输入是用户在搜索框里敲的关键词,需要匹配标题、正文和标签三个字段。直接监听input事件实时过滤会有性能隐患,因为每次按键都触发一次全量遍历和setData。标准做法是加一个 300 毫秒的防抖。
let searchTimer = null function onSearchInput(e) { clearTimeout(searchTimer) const keyword = e.detail.value.trim().toLowerCase() searchTimer = setTimeout(() => { const results = noteStore.getAll().filter(note => { const title = note.title.toLowerCase() const content = note.content.toLowerCase() const tags = (note.tags || []).join(',').toLowerCase() return title.includes(keyword) || content.includes(keyword) || tags.includes(keyword) }) this.setData({ searchResults: results }) }, 300) }防抖的核心逻辑是clearTimeout加setTimeout:用户连续输入时只保留最后一次触发,停顿 300 毫秒后才真正执行过滤。搜索词统一转小写,是因为用户可能输入大写或混写,而笔记内容可能是小写开头,不转换会出现“明明有这个词却搜不到”的玄学问题。搜索结果用独立data字段维护,不要直接改displayedEntries,否则退出搜索后列表状态就乱了,还得重新分页。
5. 记事本小程序避坑清单:打包超限、键盘遮挡和缓存丢数据
踩坑记录统一按“现象 → 原因 → 解决”来写,都是我在类似项目里真实经历过的,或是在社区里高频见到的问题。
5.1 编译报错 source size 2612kb exceed max limit 2mb
现象:微信开发者工具真机预览时直接报错,提示主包大小超过 2MB,常见于用 uni-app 打包的项目,原生小程序里也偶尔出现。
原因:编译产物把大图片、第三方 UI 库、或者core-js之类的 polyfill 全部收进主包。uni-app 项目里这个问题尤其普遍,因为 Vue 运行时本身就有体积,再加上开发阶段不自觉地引入的组件库,超限几乎必然。
解决:先删未使用的组件和插件,这是零成本的第一刀;再把项目按subPackages分包,列表页、编辑页放主包,图片选择、标签弹层等独立页面放进分包,小程序会在进入分包页面时才加载对应代码。最后检查app.js里是否全局引入了不会用到的 API。记住一个原则:2MB 里只放首屏必须的东西。
5.2 textarea 被键盘弹起后遮挡,保存按钮点不到
现象:在小程序里输入多行正文时,键盘弹起来把输入框和底部保存按钮一起顶出屏幕,用户输入完没法保存,只能先收起键盘再盲点。
原因:微信小程序的textarea是原生组件,层级和普通键盘处理逻辑跟 H5 不同。页面底部如果有固定定位的操作栏,键盘弹起时会出现遮挡。
解决:textarea上设置cursor-spacing属性,例如cursor-spacing="120",让输入光标和键盘之间保持 120px 间距;同时给输入框绑定adjust-position属性,默认值为true,允许页面自动上推。底部操作栏不要固定定位,改成页面流式布局的一部分,这样键盘弹起时它自然被推到上方,这是最稳妥的做法。另外,iOS 上input和textarea的字体大小不能小于 17px,否则聚焦时会被自动放大,这也是个容易忽略的体验细节,把字号统一设在 17px 以上能避免这个现象。
5.3 用户切后台再回来,草稿全部丢失
现象:用户编辑到一半切到微信聊天,回来看记事本,输入的内容只剩一小半,甚至完全空白。
原因:小程序页面在后台被销毁,或者onHide时没有做任何保存动作,编辑页的data随页面实例一起被回收了。
解决:在编辑页onHide和onUnload里都调用保存草稿方法。onHide覆盖的是切后台、被打断的场景,onUnload覆盖的是用户直接返回列表页的场景。保存的草稿里必须带上noteId,这样用户再次点击同一条笔记时,靠id能找回之前未完成的编辑。注意保存动作要做幂等校验,草稿内容和当前已有内容相同时跳过写入,避免反复覆盖。
5.4 wx.setStorageSync 写入失败,整页白屏
现象:笔记内容存到一定量后,某个页面突然抛setStorageSync:fail,代码里没有try-catch,导致整页渲染中断。
原因:两个常见因素叠加:一是本地存储超过 10MB 上限,二是开发者只在写入时做了成功的判断,没有处理异常分支。10MB 看着很大,但图片路径和较长的正文积累起来是能触顶的。
解决:所有wx.setStorageSync调用必须包try-catch,失败时至少console.error并主动提示用户导出备份。存储模块里加一个体积检查函数,估算当前存储数据的 JSON 字符串长度,超过 8MB 时在首页显示“存储空间即将用满”的提示。这个 8MB 阈值是自己留的余量,避免真到 10MB 才触发失败。另外,定期清理已删除笔记的图片文件,用wx.getSavedFileList和笔记中的images字段比对,把孤儿文件删掉,是延长存储寿命最直接的手段。
5.5 自定义导航栏高度不同机型错位
现象:为了好看,在页面 json 里设置"navigationStyle": "custom"自定义导航栏,结果标题在 iPhone 上居中,在安卓上偏下,刘海屏和普通屏都各差一截。
原因:不同机型的状态栏高度不一样,小程序提供wx.getWindowInfo()可以拿到状态栏高度,但很多人没有用它,直接写死了 top 值。
解决:最省事的建议是:记事本这种工具型小程序直接用默认导航栏,不要自定义,省掉一套适配工作。确实要自定义时,用wx.getWindowInfo().statusBarHeight加上胶囊按钮高度计算标题的 top 值,这个组合是目前比较稳定的通用做法。还有一种简化办法:把标题文字去掉,导航栏只放一个返回按钮,这样即使高度有偏差,视觉上也不那么违和。
5.6 页面栈混乱导致返回跳错页
现象:用户在编辑页保存后返回,却回到了另一个编辑页,或者按返回直接退出了小程序。
原因:跳转方式用错了。wx.redirectTo会关闭当前页面,wx.navigateTo会保留当前页面并压栈。如果列表页用redirectTo跳编辑页,编辑页返回时栈里没有列表页,就会直接退出小程序。
解决:明确一套跳转约定:列表 → 详情 → 编辑,全部用wx.navigateTo;列表页之间的 tab 切换用wx.switchTab;任何地方都别用wx.redirectTo做页面跳转,它是给“登录页 → 首页”这种一次性替换场景用的。保存成功后的返回用wx.navigateBack({ delta: 1 }),不要用redirectTo再跳一遍列表页,否则列表页会重新创建,滚动位置丢失。
6. 验证你的记事本系统:导出备份和恢复是最值得先做的进阶功能
本地缓存方案最大的风险是“手机清理缓存后一切归零”,所以多功能记事本系统第一个要做的进阶功能不是云同步,而是导出和恢复。这个功能既能当后悔药,又能验证存储层的数据结构是否能完整序列化和反序列化。
导出用文件系统接口把整个笔记数组写到用户文件目录:
function exportBackup() { const data = noteStore.getAll() const json = JSON.stringify(data, null, 2) const filePath = `${wx.env.USER_DATA_PATH}/notebook_backup_${Date.now()}.json` wx.getFileSystemManager().writeFile({ filePath, data: json, encoding: 'utf8', success() { wx.showModal({ title: '备份已生成', content: `文件路径:${filePath}。建议通过开发者工具的缓存目录取出。`, showCancel: false }) }, fail(err) { console.error('导出失败', err) } }) }JSON.stringify(data, null, 2)里第二个参数是替换函数,传入null表示不处理字段;第三个参数 2 表示缩进两个空格,让备份文件可读。恢复流程就是readFile读回 JSON,JSON.parse后先校验Array.isArray,再调用存储封装整体覆盖。恢复前一定要用时间戳备份当前数据,防止导入数据格式不兼容导致旧数据也丢了。
功能都做完之后,按下面这个表跑一遍冒烟测试,能过就算这个记事本系统基本立住了:
| 用例 | 操作步骤 | 预期结果 |
|---|---|---|
| 新建笔记 | 首页点新建,输入标题正文,保存 | 首页第一条出现新笔记,标题正文完整 |
| 重启持久化 | 杀掉小程序重新打开 | 笔记仍存在,顺序按更新时间排列 |
| 编辑更新 | 修改标题后保存再进详情 | 标题更新,创建时间不变,更新时间变化 |
| 标签筛选 | 给两条笔记打相同标签,点击该标签 | 列表只显示这两条笔记 |
| 搜索 | 输入标题中的任意连续两字 | 秒出结果,大小写不敏感 |
| 备份恢复 | 导出后清空缓存,再导入文件 | 全部笔记原样恢复 |
这套验证流程我每做一个新版本都会完整跑一遍。早年间做第一版记事本时没有导出功能,一次清理缓存让我一个月的随手记全没了,从那之后导出备份就成了我所有本地存储类小程序的默认起手式。先把数据兜底做好,再谈多功能,这个顺序希望帮到你。
本文还有配套的精品资源,点击获取