做这个日常活动记录系统的起因还挺朴素的——我发现自己每天的日程、灵感、随手记下来的碎片全部散落在备忘录、微信收藏、记事本和相册里真正想找的时候什么都找不到。干脆用自己顺手的技术栈做一个小程序把日常活动统一管理起来。最终落地了一套基于nodejsuniappvue的微信小程序方案后端接口用Node.js提供前端界面用UniApp搭配Vue语法开发一份代码就能编译到微信小程序、H5和App多端运行。整个项目从初始化到跑通上线花了一个多星期的时间踩了不少坑也沉淀出很多经验。这篇就把整个系统的设计思路、技术选型、核心功能实现、微信小程序适配还有上线流程完整复盘一遍给正在做类似小程序项目的朋友一个可以直接参考的路线。无论你是刚接触uni-app的初学者还是想给自己的业务快速搭一个跨端应用的开发者这篇的内容都值得你花几分钟看完。1. 系统总体设计与技术选型思路1.1 为什么选nodejsuniappvue这套组合先说结论这套组合最大的优势就是技术栈统一、开发效率高、跨端成本低。在日常活动记录这个场景里核心需求其实很简单要有地方存数据、要有界面录数据、要有入口看数据。但真正做起来你会发现纯原生开发微信小程序非常难受。逻辑层和视图层分离写个复杂点的列表页都要拆成好几个文件更别提以后如果还想上H5版、App版代码基本要重写一遍。而uni-app基于Vue语法完全可以用Vue的组件化思维来组织页面然后一键编译到微信小程序以后想加H5端或者App端也只是勾选一个编译目标的事。后端选Node.js原因也很直接前后端都用JavaScript不用在脑子里维护两套语言模型。而且Node.js生态里Express这个框架非常成熟写RESTful接口是十分钟就能上手的事部署也简单一台便宜的云服务器就能跑起来甚至开发阶段本地起服务就能调试。1.2 日常活动记录系统的功能模块拆解我把这个系统拆成了四个核心模块每个模块对应一组用户操作场景活动记录这是最核心的功能。用户新增一条活动记录包含标题、内容、时间、地点、分类这些字段支持上传图片。后续支持查看详情、编辑、删除。分类管理把活动归类到工作、学习、生活、运动等类型下。分类用固定枚举加自定义的组合既能保证统计口径统一又给用户留了灵活空间。时间线查看所有记录按日期倒序排列支持按月份切换筛选。这个模块解决的核心痛点是“回顾”——用户最常问的问题是上个月我干了什么。数据概览展示本月活动总数、连续记录天数、分类占比。这个模块的价值是给用户正向反馈让记录这件事本身变得有成就感也能提升用户持续使用的意愿。从开发角度看这四个模块各有侧重点记录模块考验表单交互和图片上传分类管理涉及数据字典设计时间线要考虑列表性能和数据聚合的SQL怎么写数据概览则要处理统计查询和图表展示。分工清楚之后整个系统的开发路径就很明确了。1.3 活动记录的数据模型设计数据库我选了SQLite零配置、单文件特别适合这种工具型个人应用。生产环境如果数据量上来了无缝切到MySQL也容易因为查询语句基本通用。活动记录表的设计我斟酌了很久最终定下的字段结构是这样的字段名类型说明idINTEGER 主键自增记录唯一标识titleTEXT活动标题必填contentTEXT活动详细内容categoryTEXT分类默认值lifeactivity_dateTEXT活动日期格式YYYY-MM-DDlocationTEXT活动地点imagesTEXT图片路径多个用逗号分隔created_atTEXT创建时间updated_atTEXT更新时间这个表的几个关键设计点我解释一下日期单独用一个字段存字符串而不是存时间戳是为了方便按天分组查询——一条GROUP BY activity_date就能统计出每天的活动数量不用先把时间戳转成日期再做聚合。images字段用逗号分隔的字符串而不是单独建一张图片表是因为这个场景下图片永远跟随记录存在不需要被多个记录复用冗余一点换来了查询时的简单直接是划算的。2. 开发环境搭建与项目初始化2.1 Node.js安装与环境变量配置这一步看着基础但在我实际接触的项目里一大半的启动报错都出在环境配置上。Node.js安装本身没什么难度去官网下载LTS版本长期支持版的安装包双击一路Next就行。值得留意的是安装完成后的环境变量验证。在命令行中输入node -v和npm -v能看到版本号才算安装成功。如果提示“node不是内部或外部命令”那就要手动配置PATH环境变量把Node.js的安装目录默认是C:\Program Files\nodejs添加进去Windows系统下打开“系统属性-环境变量-系统变量-双击Path-新建-填入目录”即可。提示安装Node.js时默认会同时安装npm版本对应关系在下载页有说明尽量选版本号与当前最新稳定版对齐的版本避免后面npm install某些依赖时出现engines冲突。2.2 UniApp项目初始化与目录结构我用的是HBuilderX这个IDE来管理uni-app项目原因是它对uni-app的支持最完善新建、编译、发行一条龙都集成了不需要额外配置命令。项目创建流程打开HBuilderX文件-新建-项目-选择uni-app模板输入项目名称可以就叫activity-record选择默认模板点击创建。创建完成后默认的项目结构长这样activity-record/ ├── pages/ # 页面文件每个页面一个文件夹 ├── static/ # 静态资源图片、图标等 ├── App.vue # 应用入口文件全局生命周期和样式 ├── main.js # 入口JS创建Vue实例 ├── manifest.json # 全局配置AppID、各平台设置 ├── pages.json # 页面路由和导航栏配置 └── uni.scss # 全局样式变量pages.json是uni-app框架里最重要的一个配置文件它决定了小程序的页面路由、导航栏标题、窗口背景这些全局表现。新建页面之后一定要在pages.json的pages数组里注册路由这一步很多新手会漏掉结果编译成功但页面打开是空白的。2.3 manifest.json配置与微信小程序AppIDmanifest.json是另一个需要重点关注的配置文件。在HBuilderX里manifest.json有可视化编辑界面切换到“微信小程序配置”一栏填入你在微信公众平台注册小程序时拿到的AppID。如果只是本地开发调试这里可以先填测试号。但要注意的是测试号不能用于真机预览时调用很多真实接口比如登录、支付这些能力。正式开发建议直接注册一个小程序账号个人主体就行注册流程在微信公众平台官网走一遍大概半天就能审核通过AppID是即拿即用。填好AppID之后在HBuilderX菜单栏选择“运行-运行到小程序模拟器-微信开发者工具”如果环境正常微信开发者工具会自动打开并把编译产物加载进去看到默认的index页面出现在模拟器里项目就算跑起来了。3. 核心功能实现活动记录全流程3.1 后端API接口设计与代码实现后端我基于Express框架搭了一套RESTful接口核心是围绕活动记录的增删改查。Express的初始化很简单几行代码npm init -y npm install express cors body-parsercors中间件用来解决跨域请求body-parser用来解析请求体。在本地开发阶段小程序开发者工具会限制请求域名但可以在工具栏勾选“不校验合法域名、web-view业务域名、TLS版本以及HTTPS证书”来跳过限制所以这里我先不需要处理生产环境的域名配置问题。接口设计上我遵循了资源命名规范方法路径功能GET/api/activities获取活动列表支持年月筛选GET/api/activities/:id获取单条活动详情POST/api/activities新增一条活动PUT/api/activities/:id修改活动信息DELETE/api/activities/:id删除一条活动GET/api/statistics获取统计数据以新增和列表查询两个核心接口为例看看代码是怎么写的。新增接口的核心逻辑其实就三步接参数、校验必填项、插库返回// 新增活动记录 app.post(/api/activities, (req, res) { const { title, content, category, activity_date, location, images } req.body; if (!title || !activity_date) { return res.status(400).json({ code: 1, msg: 标题和日期不能为空 }); } const created_at new Date().toISOString(); const sql INSERT INTO activities (title, content, category, activity_date, location, images, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?); const params [title, content || , category || life, activity_date, location || , images || , created_at, created_at]; db.run(sql, params, function(err) { if (err) { return res.status(500).json({ code: 1, msg: err.message }); } res.json({ code: 0, data: { id: this.lastID }, msg: success }); }); });列表查询接口要考虑分页和日期筛选。我采用的方案是前端传year和month后端拼接SQL条件再按活动日期倒序排列app.get(/api/activities, (req, res) { const { year, month } req.query; let sql SELECT * FROM activities; const conditions []; const params []; if (year) { conditions.push(strftime(\%Y\, activity_date) ?); params.push(year); } if (month) { conditions.push(strftime(\%m\, activity_date) ?); params.push(month); } if (conditions.length 0) { sql WHERE conditions.join( AND ); } sql ORDER BY activity_date DESC, created_at DESC; db.all(sql, params, (err, rows) { if (err) { return res.status(500).json({ code: 1, msg: err.message }); } res.json({ code: 0, data: rows, msg: success }); }); });用SQLite内置的strftime函数做日期匹配比在JavaScript里手动处理字符串更可靠也避免了时区导致的日期偏移问题。日期在存储时统一用YYYY-MM-DD格式字符串比较就是日期前后比较排序也自然正确。3.2 前端列表页与记录表单页开发前端核心页面有两个活动列表页和新增编辑页。列表页的核心交互是三块顶部月份切换、中部分类筛选、主体列表展示。月份切换我用uni-app的picker组件mode设为date的“年月”模式选择结果传入后端接口拉数据。分类筛选用一个横向滚动的scroll-view里面渲染分类标签点击时选中样式切换同时重新请求列表。列表项的展示采用卡片式布局每条记录显示分类标签、活动标题、日期、地点缩略信息。为了渲染性能我用了uni-app的列表渲染方式并且在onPullDownRefresh下拉刷新和onReachBottom触底加载时重新拉取或追加数据template view classlist-page view classmonth-picker picker modedate fieldsmonth :valuecurrentMonth changeonMonthChange view classmonth-display{{ currentMonth }}/view /picker /view scroll-view scroll-x classcategory-tabs view v-forcat in categories :keycat.key classcat-item :class{ active: currentCat cat.key } clickchangeCategory(cat.key) {{ cat.name }} /view /scroll-view view classactivity-list view v-foritem in list :keyitem.id classactivity-card clickgoDetail(item.id) view classcard-header text classcategory-tag{{ getCatName(item.category) }}/text text classdate{{ item.activity_date }}/text /view view classcard-title{{ item.title }}/view view classcard-location v-ifitem.location{{ item.location }}/view /view /view /view /template新增编辑页则是一个标准的表单页面用uni-app的form组件配合v-model双向绑定。需要注意的地方有两个。第一个是日期选择直接用picker的date模式绑定activity_date字段就行默认值设置为今天。第二个是图片上传用uni.chooseImage选择图片再用uni.uploadFile上传到后端的/upload接口返回的文件路径拼接成完整URL存入图片字段uni.chooseImage({ count: 6, sourceType: [camera, album], success: (res) { const tempFilePaths res.tempFilePaths; const uploadTasks tempFilePaths.map((path, index) { return new Promise((resolve, reject) { uni.uploadFile({ url: baseUrl /upload, filePath: path, name: file, success: (uploadRes) { const data JSON.parse(uploadRes.data); resolve(data.url); }, fail: reject }); }); }); Promise.all(uploadTasks).then((urls) { this.form.images urls; }); } });用Promise.all并行上传多张图片比串行一张一张传效率高很多实测六张图同时传基本两秒左右都能完成。上传接口返回的是文件名前端展示时拼上服务器的静态资源前缀就能访问。3.3 分类统计与数据概览实现数据概览页我做了三个维度的统计活动总数、月度分布、分类占比。后端接口通过SQL聚合直接算出结果前端不需要做二次处理app.get(/api/statistics, (req, res) { const totalSql SELECT COUNT(*) AS total FROM activities; const monthSql SELECT strftime(%Y-%m, activity_date) AS month, COUNT(*) AS count FROM activities WHERE activity_date date(now, -6 months) GROUP BY month ORDER BY month DESC; const categorySql SELECT category, COUNT(*) AS count FROM activities GROUP BY category; db.get(totalSql, (err, totalRow) { if (err) return res.status(500).json({ code: 1, msg: err.message }); db.all(monthSql, (err, monthRows) { if (err) return res.status(500).json({ code: 1, msg: err.message }); db.all(categorySql, (err, categoryRows) { if (err) return res.status(500).json({ code: 1, msg: err.message }); res.json({ code: 0, data: { total: totalRow.total, months: monthRows, categories: categoryRows } }); }); }); }); });分类占比展示我用了uni-ui库里的比例进度条组件uni-data-progress每个分类一条横条宽度百分比就是当前分类占总数的比例。月度分布则用简单的柱状图方式每个月份一个矩形块高度对应条数虽然没用ECharts这种重量级图表库但胜在轻量、加载快完全够用。3.4 小程序端图片预览与视频播放扩展图片预览功能用uni.previewImage就能实现传入当前图片URL和所有图片URL数组小程序原生弹层就带缩放和滑动切换能力不需要自己写轮子。这个系统天然只处理了图片但如果你做的活动记录需要存视频也有现成方案。uni-app里视频组件用video标签src直接指向后端返回的视频地址就能播放。需要注意的是小程序真机上播放视频要求域名必须配置为业务域名且支持HTTPS本地调试时可以先在开发者工具里关闭域名校验。我最初想在这个系统里加一个“记录瞬间”的视频入口后来因为部分低版本基础库兼容问题暂时搁置了等基础库版本升级后再放出来思路是一样的。4. 微信小程序适配与发布上线4.1 顶部导航栏高度与安全区适配做了这么多页面后你会发现小程序的环境和普通H5网页差异最大的地方其实不在API而在屏幕尺寸和安全区。顶部导航栏的高度不是固定的不同机型胶囊按钮的位置、刘海屏的占用都不一样。我采用的做法是在pages.json里把navigationStyle设置为custom也就是自定义导航栏然后用uni.getSystemInfoSync获取状态栏高度和胶囊按钮位置动态计算出导航栏高度const systemInfo uni.getSystemInfoSync(); const menuButtonInfo uni.getMenuButtonBoundingClientRect(); const navBarHeight (menuButtonInfo.top - systemInfo.statusBarHeight) * 2 menuButtonInfo.height;这个公式算出来的是标准的自定义导航栏高度适配了大部分机型。底部则用safe-area-inset-bottom这个CSS环境变量来做安全区留白iPhone X及以后机型底部有Home指示条加了之后页面内容不会被遮挡。4.2 生命周期管理与跨页面传参uni-app的页面生命周期和Vue组件的生命周期有对应关系但也有一些小程序特有的钩子。我在这个项目里用的最频繁的三个是onLoad页面首次加载时执行适合做初始化数据请求接收上一个页面通过url参数传过来的数据。onShow页面每次显示时执行从列表页切到新增页再返回列表时需要onShow里重新拉数据这样新加的记录才能立即显示。onPullDownRefresh下拉刷新触发配合uni.startPullDownRefresh开启下拉动画数据加载完成后调用uni.stopPullDownRefresh结束动画。跨页面传参我走的是两种方式简单参数直接用url拼接?id123对象或者数组这类复杂数据存到globalData或者uni.setStorageSync里取的时候再读出来。url拼接传参会自动做URL编码中文和特殊字符不会乱码但要注意参数长度有限制太长的内容建议走缓存方案。4.3 HBuilderX发行微信小程序的完整流程这里把从HBuilderX到微信开发者工具的完整打包发布流程写一遍照着操作就能走通。第一步在HBuilderX里点击菜单栏“发行-小程序-微信小程序”。这一步会在项目目录下生成unpackage/dist/dev/mp-weixin目录里面是编译好的小程序原生代码。第二步打开微信开发者工具选择“导入项目”目录选中上面生成的mp-weixin文件夹AppID填你注册好的小程序AppID。导入之后开发者工具会做一次编译没有报错就能在模拟器里看到页面了。第三步真机预览。在微信开发者工具工具栏点击“预览”会生成一个二维码手机微信扫码就可以在真机上打开小程序进行测试。这一步建议多做几轮重点检查不同机型下自定义导航栏的布局和底部安全区的表现。第四步上传版本。测试没问题后在微信开发者工具点击“上传”填入版本号和备注代码就提交到了微信公众平台后台。登录微信公众平台在“版本管理-开发版本”里找到刚提交的版本提交审核审核通过后点击“发布”即可上线。注意小程序上线前必须在微信公众平台后台配置服务器域名。request、uploadFile都要求服务器域名是HTTPS开头且已经ICP备案。如果后端没有备案域名开发阶段可以用本地调试但正式发布就绕不开这一步域名问题要提前准备。5. 常见问题与排查技巧实录5.1 npm.ps1无法加载文件的问题这个问题在我配新电脑环境时几乎每次都遇到。在Windows PowerShell里执行npm install报错信息是“加载文件...npm.ps1因为在此系统上禁止运行脚本”。原因是PowerShell默认的执行策略是Restricted不允许运行本地脚本文件。解决方案有两种。临时方案当前窗口生效Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass永久方案以管理员身份打开PowerShell执行Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSignedRemoteSigned表示本地脚本可以运行从网上下载的脚本需要有签名才运行安全性有保障。如果不想动执行策略也可以直接用CMD命令提示符来跑npm命令CMD不会执行这个脚本所以没有这个限制很多老手就是这么干的。5.2 小程序请求接口时的跨域与域名问题开发阶段最常见的一个现象代码逻辑没错接口地址也能在浏览器里访问但小程序里请求就是不返回数据。原因大概率在小程序对请求域名的限制上。解决方法是打开微信开发者工具右上角的“详情-本地设置”勾选“不校验合法域名、web-view业务域名、TLS版本以及HTTPS证书”开发模式下请求就能正常发出了。另外还有一个本地调试的细节如果后端跑在你电脑上小程序真机调试时不能填localhost要填你电脑在局域网里的IP地址比如http://192.168.1.100:3000并且手机和电脑要在同一个Wi-Fi下。这个坑花了我不少时间排查。5.3 真机预览白屏或样式错乱的排查思路真机预览白屏排除代码问题后最常见的原因是基础库版本太低。在微信开发者工具里可以切换调试基础库版本遇到报错就看控制台输出的版本兼容提示选择合适的基础库版本即可。样式错乱则更多是因为小程序端和H5端的CSS渲染差异。比如position: fixed在小程序里表现正常但H5上有些场景会出现偏移flex布局里某些属性在低版本基础库不支持。我的经验是开发时先以小程序端为基准调试其他端适配优先级往后放因为日常活动记录这种工具型应用的核心场景还是小程序为主。5.4 开发中遇到的其他小坑速查问题原因解决方案new Date()在iOS上显示NaNiOS不支持2019-01-01这种带横杠的日期格式用replace(/-/g, /)把横杠换成斜杠再new Date上传图片后路径404图片存在但静态资源目录没配置Express中配置app.use(/uploads, express.static(uploads))页面白屏console无报错pages.json里路由未注册检查pages数组是否包含当前页面路径下拉刷新不触发页面没开启enablePullDownRefreshpages.json里对应页面配置style.enablePullDownRefresh为true模拟器正常真机请求失败域名未备案或接口地址是localhost真机用局域网IP域名提前备案并配置这里面的第一条我印象最深。模拟器里用Android内核跑得一点问题没有真机iPhone上一运行日期全部变成NaN排查了半天才意识到是iOS的JavaScript引擎对日期字符串格式的要求比V8严格得多把横杠替换成斜杠就都正常了。这类平台差异在跨端开发里非常典型多端并行测试是唯一的解法。5.5 项目结构与后续扩展建议这个系统目前的功能已经完整覆盖了日常活动记录的闭环场景。从架构上看前后端分离接口按资源模块化拆分数据模型简洁清晰后续扩展点也不少。如果想给这个系统加一些高级功能我的建议优先级是这样的首先是数据导出能力做一个按月导出Markdown或PDF的小功能记录数据就能形成月度总结其次是活动提醒在记录里带上提醒时间配合小程序的订阅消息做推送再次是地点打卡能力用uni-app的定位接口获取当前经纬度反解成地址自动填入。我自己的体会是这种工具型小程序的护城河不在功能多少而在使用习惯的养成。把记录这个动作做得越顺畅、越轻量用户才越愿意用。表单字段能留默认值就留默认值日期默认今天、地点可以自动定位、分类可以自动记忆这些细节对体验的提升远比多做几个统计图表明显。这次开发过程也让我对uni-app的跨端能力有了更全面的认识写一套Vue组件同时输出到小程序和H5这件事省下的开发时间是实打实的。