简介:在微信小程序开发中,前后端分离架构已成为中大型项目的标配。其核心思想是将界面交互与业务逻辑解耦:前端负责用户操作与渲染,后端通过API提供数据服务,同时需要处理登录鉴权、数据库设计、文件存储等关键链路。这种架构不仅提升了开发效率和代码可维护性,还能灵活应对业务扩展。对于带图片上传功能的内容类小程序,还需重点考虑直传对象存储、接口防刷与异常兜底。从电商到工具类应用,前后端分离的实践模式已被广泛验证。本文以记录型工具小程序为例,完整复盘其从V1.0.1到V1.0.39的演进过程,涵盖Node.js后端选型、Token鉴权、数据库索引优化、性能调优及Docker部署等实战经验,为同类型项目提供可直接落地的参考。 榆落微时光从 V1.0.1 一路迭代到 V1.0.39,这个版本号放在小程序赛道上不算多惊人,但足够把一个从零开始的“记录型工具”小程序真正打磨到敢给用户长期使用的状态。这个项目表面上只是“记录生活片段”,可实际做起来,牵扯到的却是小程序前端、后端服务、对象存储、数据库、部署运维一整条链路。很多人会问“小程序商城项目有没有参考价值”,商城当然有价值,但电商逻辑太重;榆落微时光这种记录型工具反而更贴近大量非电商类微信小程序的真实需求——登录、动态列表、图片上传、数据统计、内容审核,每一个模块都不浮夸,但每一个模块都绕不开。如果你也想做一款前后端分离的小程序,或者正在困扰“前端写完不知道后端该怎么搭”,这篇文章值得你泡杯茶慢慢看。
我会按这个版本下我们实际沉淀下来的通用方案来复盘:小程序端用什么架构、后端为什么选 Node.js、表结构怎么设计、请求层怎么封装、V1.0.39 之前踩过的坑,以及最后上线的部署和合规细节。这些内容不是某个框架的官方文档,而是把项目拆开揉碎之后,真正在线上跑过、也真正出过问题的经验。
1. 榆落微时光这个项目的定位:一个记录型工具小程序的完整样貌
1.1 产品定位:不做商城,只做“微时光”记录
榆落微时光的核心功能,是让用户用“一张照片 + 一段文字 + 一个心情标签”记录当天的某个瞬间,并把这些记录按时间轴和日历视图沉淀下来。项目名里“榆落”取的是微小日常的意象——时间不会停下来,但我们可以留下一些细小的切片。
这也决定了它和商城类小程序完全不同的技术侧重点。商城业务核心是商品、订单、支付、库存,而榆落微时光的核心业务是内容生产、内容展示、用户长期留存。从代码层面看,它要处理的是更常见的“用户登录—上传内容—列表展示—互动反馈”链路。很多人觉得这类工具简单,但真正上手就会发现:图片上传的稳定性、列表滚动性能、不同机型上的兼容性、后端接口的鉴权与防刷,每一块都能写成一篇踩坑笔记。
V1.0.39 这个版本号也说明了项目的发展节奏。V1.0.x 阶段意味着整体功能已经稳定,后面的迭代不是推倒重来,而是在一个相对成熟的骨架上不断做性能优化、兼容性修复和体验打磨。从 V1.0.1 到 V1.0.39,核心模块没有大幅变更,但分包结构调整了三次,接口鉴权从“前端随便传 userId”改成了完整的 token 机制,图片上传也从“先传后端再传对象存储”改成了“小程序直传对象存储”。这些变化才是版本号背后真正有价值的东西。
1.2 核心功能模块拆解
为了让你对项目有一个整体感知,我先列出榆落微时光的主要功能模块,以及每个模块在技术上对应的关键点:
| 模块 | 用户侧功能 | 技术关键词 |
|---|---|---|
| 时光首页 | 信息流展示历史记录,支持分页加载 | 列表分页、图片懒加载、下拉刷新 |
| 记录发布 | 拍照/相册选图、输入文字、选择心情 | wx.chooseMedia、canvas 压缩、上传直传 |
| 日历回忆 | 按月份查看哪几天有记录 | 日历组件、日期聚合接口 |
| 个人中心 | 昵称头像、我的记录、设置 | 用户隐私接口、本地缓存 |
| 后台管理 | 用户管理、内容审核、数据统计 | 管理端鉴权、数据报表 |
心情标签这个交互用到了微信小程序的 radio-group 单选框能力。初看很简单,但如果你想做成一排圆形的表情选择器,就需要在 radio 外部包一层自定义样式,并且让选中的 value 和后端枚举严格对应。这类小细节在实战中非常容易出问题,我后面会专门展开。
1.3 前后端整体架构:从请求到落库的完整链路
榆落微时光前后端分离,但小程序端和传统 Web 前端有一个很大区别:微信小程序无法直接访问任意域名,所有的请求域名必须在微信公众平台配置为合法域名,并且必须走 HTTPS。所以整个链路是这样的:
- 小程序端发起 wx.request 请求;
- 请求到达 Nginx 接入层;
- Nginx 将 /api 路径转发到 Node.js API 服务;
- API 服务处理业务逻辑,读写 MySQL 和 Redis;
- 图片文件则通过临时凭证直传对象存储,不走 API 服务中转。
后端服务本身拆成两个大块:面向 C 端小程序的 API,以及面向内部运营的管理端接口。管理端不放在小程序里,而是单独一个 Web 页面,通过账号密码登录后获取管理 token,操作内容审核和数据查看。这样 C 端和管理端的 token 体系分开,权限边界更清晰。
2. 前端骨架:原生框架选择、分包设计与请求层封装
2.1 为什么坚持用原生小程序,而不是 uni-app 或 Taro
开发阶段我们讨论过要不要上跨端框架。最后决定用原生微信小程序,核心原因是:榆落微时光只投放微信端,没有多端需求,而原生框架对微信底层能力的支持是最直接的。
举个例子,微信后来的 Skyline 渲染引擎,以及分包异步化、自定义导航栏、隐私保护接口这些新能力,官方原生框架可以第一时间接入,而跨端框架往往要等框架层适配。用原生还有一个好处:微信开发者工具对原生代码的调试体验最完整,wxml 的节点审查、Network 面板的请求查看、Storage 的可视化操作,在原生项目里都是零损耗的。不是 uni-app 不好,是对于“只做微信端”的项目,多引入一层编译框架确实没必要。如果你有快手、抖音小程序等多端需求,uni-app 或 Taro 是完全合理的选择,但你要接受它们在个别平台能力上可能存在滞后。
在渲染引擎的选择上,我们也做了一次取舍。V1.0.39 版本里,大部分页面仍然使用默认的 WebView 渲染,只有信息流列表页开启了 Skyline 渲染。Skyline 在长列表滚动、手势动画上的表现更好,但它和 WebView 的能力边界不完全一样,比如部分 CSS 选择器支持程度不同。所以我建议:不要把整个项目一次性迁到 Skyline,而是挑选最适合的页面逐步切换,出了问题影响面也可控。
2.2 主包与分包:控制包体是第一个性能瓶颈
微信小程序规定主包不能超过 2MB,整个小程序的上限根据不同基础库会放宽到 20MB 左右。但包体越大,冷启动越慢,用户在弱网环境下等待就越久。榆落微时光第一版把所有页面都塞进主包,结果主包到了 1.9MB,每次冷启动都让人紧张。后来我们做了一次彻底的分包调整:
- 主包:tabBar 页面(首页、记录、日历、个人中心),以及公共组件和公共工具库;
- 分包A:记录详情、记录编辑、图片预览;
- 分包B:数据统计、关于、隐私协议等低频页面。
结构调整之后,主包降到 1.3MB,冷启动明显加快。这里有一个关键点:分包不是简单把页面文件夹移出去就完了,页面之间的跳转路径会变,tabBar 页面不能放在分包里,某些组件跨分包使用也要注意。实际上,微信的基础库后来支持了“分包异步化”,也就是说在一个分包里可以通过异步 require 去使用另一个分包里的组件或逻辑,这非常实用。
// 在分包A的页面中,异步获取分包B中的工具模块 let subpackageUtil = null Page({ async onLoad() { // 先加载分包B,再使用其中的模块 const loadTask = wx.loadSubpackage ? wx.loadSubpackage({ name: 'packageB' }) : Promise.resolve() if (loadTask && loadTask.then) { await loadTask.catch(() => wx.showToast({ title: '资源加载失败,请重试' })) } subpackageUtil = require('../../packageB/utils/sharedModule/common.js') } })这种做法可以让页面先快速渲染出来,再异步加载低频功能所需的分包,尤其适合“首页很轻、详情页较重”的产品形态。不过要注意,低版本基础库不支持 loadSubpackage 时,需要有降级处理,否则会把整个包体一次性下载回来。
2.3 请求层封装:统一处理登录态、401 和错误提示
小程序开发中,最容易被忽视也最影响长期维护质量的,就是请求层。如果每个页面都直接写 wx.request,代码会迅速失控。榆落微时光的请求封装很早就定了,核心思路是所有请求走同一个入口,由入口统一携带 token、统一处理 HTTP 状态码、统一在业务 code 非 0 时弹出错误提示。
const BASE_URL = 'https://api.example.com/api' function refreshToken() { return new Promise((resolve, reject) => { wx.request({ url: `${BASE_URL}/auth/refresh`, method: 'POST', header: { Authorization: `Bearer ${wx.getStorageSync('refreshToken')}` }, success: (res) => { if (res.data && res.data.code === 0) { wx.setStorageSync('token', res.data.data.token) wx.setStorageSync('refreshToken', res.data.data.refreshToken) resolve(res.data.data.token) } else { reject(res) } }, fail: reject }) }) } function request(url, options = {}) { const token = wx.getStorageSync('token') const header = { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, ...(options.header || {}) } return new Promise((resolve, reject) => { wx.request({ url: `${BASE_URL}${url}`, method: options.method || 'GET', data: options.data || {}, header, success: (res) => { // 登录态失效 if (res.statusCode === 401) { refreshToken() .then(() => request(url, options)) .then(resolve) .catch(() => { wx.reLaunch({ url: '/pages/index/index' }) reject(res) }) return } const body = res.data if (body && body.code === 0) { resolve(body.data) } else { if (options.showError !== false) { wx.showToast({ title: (body && body.message) || '请求失败', icon: 'none' }) } reject(body) } }, fail: (err) => { wx.showToast({ title: '网络异常,请检查网络', icon: 'none' }) reject(err) } }) }) }一个容易出现的问题是:多个请求同时返回 401,就会触发多次 refreshToken。解决办法是给刷新操作加一个 Promise 单例,在刷新过程中后续的 401 请求都等待同一个刷新任务完成,而不是各自重新刷新。这个小细节不处理,线上会出现“登录态刷新风暴”,用户会莫名其妙被踢下线。
2.4 组件与工具沉淀:导航栏高度、单选框与地图选型
榆落微时光里有很多看起来不起眼、实际容易翻车的组件细节。第一个是自定义顶部导航栏。很多页面为了沉浸式体验,会设置 navigationStyle: custom,然后用自定义组件填充顶部。但微信小程序的胶囊按钮位置在不同机型上不一样,不能写死一个高度。正确做法是通过 wx.getMenuButtonBoundingClientRect 获取胶囊按钮的位置信息,再用 window 信息计算出导航栏高度和状态栏高度,动态应用到页面上。这个高度在 iOS 和安卓上是不同的,测试时一定要找一台安卓低端机看效果。
第二个是单选框。微信小程序的 radio-group 组件本身能力没问题,但默认样式非常朴素。榆落微时光的心情标签需要做成多个圆形表情图,点击后高亮。我们的做法是隐藏原生 radio,用 view 模拟选中状态,再用一个隐藏的 radio-group 保存当前选中值。这里有个经验:隐藏 radio 时不要用 display: none,否则部分安卓机型在表单提交时拿不到值,最好用绝对定位移出可视区域但不销毁组件。
第三个是地图选型。很多人问过“微信小程序可以使用天地图地图组件吗”。结论比较明确:原生 map 组件底层由微信统一封装,并不支持直接切换成天地图数据源。如果只是做普通位置展示,直接用原生 map 组件就够了;如果业务必须用到天地图的影像或特定瓦片资源,那就只能通过 web-view 嵌入天地图网页版,但体验、交互和用户身份打通都要自己处理。榆落微时光目前没有强地图需求,所以不引入地图 SDK,也少了一堆授权弹窗的兼容问题。
3. 后端设计:选型、表结构、鉴权与图片上传
3.1 后端技术栈:为什么是 Node.js + Express + TypeScript
后端选型时我们做了三组对比。很多人提到 ruoyi 这类 Java 框架,它确实在企业级权限系统里很强,菜单权限、用户体系、代码生成都是现成的;但榆落微时光这种轻量记录型小程序,后端只需要几十个接口,用 Spring Boot 跑起来相当于“杀鸡用牛刀”,而且部署时对服务器内存的压力更大。NestJS 是一个更工程化的 Node.js 框架,适合中大型项目;但对于我们这种规模,Express 加 TypeScript 就足够,核心接口保持清晰,方便后续演进。
| 技术栈 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| Express + TypeScript | 轻量、灵活、前端可维护 | 工程规范需要自己约束 | 中小型项目 |
| NestJS | 模块化、依赖注入、规范强 | 学习成本和样板代码较多 | 中型以上项目 |
| Spring Boot(ruoyi 等) | 企业级权限体系成熟 | 启动重、服务器成本高 | 复杂企业后台 |
最终落地的后端结构大致如下:
server/ src/ modules/ user/ # 用户模块:登录、资料 moment/ # 记录模块:发布、列表、日历 upload/ # 上传模块:生成临时凭证 admin/ # 管理端:审核、统计 common/ middlewares/ # 鉴权、日志、错误处理 utils/ # 加解密、分页工具 app.ts # Express 实例 config.ts # 环境配置 Dockerfile docker-compose.yml这里有一个非常实用的经验:从第一天就引入 TypeScript,不要等到项目中期再迁移。JS 写路由和简单业务确实是爽,但是等接口变多、数据结构变复杂后,你再回想一个对象里到底有哪些字段,真的会崩溃。TypeScript 的 interface 在这个阶段给你的帮助是几何级数的。
3.2 数据表设计:用户表与记录表是核心
榆落微时光的表结构不算复杂,但设计时还是踩过索引的坑。核心表是 user 和 moment。
CREATE TABLE `user` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `openid` VARCHAR(64) NOT NULL, `unionid` VARCHAR(64) DEFAULT '', `nickname` VARCHAR(64) DEFAULT '', `avatar_url` VARCHAR(255) DEFAULT '', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1正常 0禁用', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_openid` (`openid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `moment` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `user_id` INT UNSIGNED NOT NULL, `content` VARCHAR(500) NOT NULL DEFAULT '', `images` JSON NOT NULL COMMENT '图片URL数组', `mood` VARCHAR(16) DEFAULT '' COMMENT '心情标签', `record_date` DATE NOT NULL COMMENT '记录日期', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1正常 0隐藏', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_date` (`user_id`, `record_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;我在初版没给 moment 表加联合索引,结果用户记录多了之后,首页时间轴按 userId 倒序查询非常慢。后来加了 (user_id, record_date) 联合索引,情况立刻好转。这里也提醒你:JSON 字段在 MySQL 里操作并不方便,如果后续需要对图片数量做统计,尽量把图片数量单独拆成列,不要每次都去 JSON 里解析。
3.3 鉴权与登录态:从 wx.login 到可主动失效的 token
小程序的登录流程和普通 Web 不一样。前端调用 wx.login 拿到的是临时 code,后端用这个 code 向微信接口换取 openid 和 session_key。注意,openid 是用户在当前小程序下的唯一标识,不要拿它当密码,也不要直接暴露给前端。后端换到 openid 后,会查询或创建用户,然后签发两个 token:
- accessToken:短期有效,比如 2 小时,小程序请求时放在 Authorization 头里;
- refreshToken:长期有效,比如 14 天,专门用来刷新 accessToken。
这些 token 我们选择存 Redis,key 为 userId,value 为当前有效的 token。这么做的好处是:服务端可以主动让某个用户的 token 失效——当用户被封禁、换设备登录、或者账号状态异常时,直接删除 Redis 里的对应关系,比纯 JWT 方案更容易控制。纯 JWT 在“服务端主动踢人”这个场景上很被动,因为只要它没过期就永远有效。
另外有一点必须提到:现在微信对用户头像昵称的获取方式已经改了,wx.getUserProfile 不再返回真实的头像和昵称,替代方案是让用户在小程序内主动填写昵称、选择头像。这既是平台规则,也是用户隐私保护的实际需要。后端接口设计上,不要把“昵称头像”当成登录的必填参数,登录只需要 code,用户资料的完善可以在个人中心单独引导。
3.4 图片上传:临时凭证直传对象存储
榆落微时光发布记录时,最怕的就是用户选了几张高清原图,上传卡在半路。如果把图片先传到 Node 后端,再由后端转发到对象存储,接口会长时间占用,服务器带宽也会被打满。所以我们改成了“小程序直传对象存储”的方案:
- 小程序端先调后端接口获取临时上传凭证,凭证里包含对象存储的 bucket、地域、临时密钥、过期时间;
- 小程序端直接用临时凭证把图片上传到对象存储;
- 上传成功后,把图片 URL 随记录内容一起提交。
这个方案的另一个好处是安全性。临时凭证过期时间很短,即使被泄露,影响窗口也有限。上传时还要注意:小程序端在真实场景中应先用 canvas 压缩图片再上传。微信拿到的原图可能 5MB 甚至 10MB,一张照片压缩到宽 1920、质量 80,体积能控制在 500KB 左右,用户上传体验和对象存储成本都能大幅改善。
4. 迭代到 V1.0.39:性能优化与真机兼容踩坑记录
4.1 setData 优化的三个层次,直接决定列表页卡不卡
小程序前端的性能瓶颈,90% 出在 setData 上。逻辑层和渲染层之间是通过数据通信的,setData 的数据量越大、调用越频繁,页面就越卡。榆落微时光在列表页踩过一个大坑:一页加载 20 条记录,每条记录包含好几张图片 URL、文案、时间,当时直接 setData 整个数组,在安卓中端机上滑动列表掉帧严重。
第一个层次是减少单次 setData 的数据量。分页加载时不要拼接整个数组再一次性 setData,而是用 concat 之后也只传新增的那部分,配合页面 data 里的数组变量,保持增量更新。
第二个层次是使用局部路径更新。比如只需要修改某一条记录的点赞状态,不要重新 setData 整个列表,而是:
this.setData({ [`momentList[${index}].liked`]: true })第三层次是拆分组件。把列表中的每一项抽成独立组件,并且给组件传入最小必要数据,这样某一项内部状态变化时,只更新那一个组件,而不是整页。做到这三个层次之后,榆落微时光的列表滚动流畅度提升非常明显。
4.2 首屏加载提速的几个实际动作
针对记录型工具,用户体验关键在于冷启动和首屏。我们做了四件事,每件事都不复杂,合起来效果很突出。
第一,页面 onLoad 里用 Promise.all 并行请求首页所需的数据,而不是一个接口一个接口串行等待。第二,列表接口改成游标分页,不传 page 页码,而是传 lastId(上一页最后一条记录的 ID),这样新增数据时不会导致分页数据错位。第三,图片 URL 统一走对象存储的图片处理参数,比如缩略图模版,列表页只加载宽度 400px 的小图,详情页再加载原图。第四,对某些低频分包做预下载,在用户进入首页并稳定之后,调用 wx.preloadSubpackage 提前下载统计分包,这样用户真正点进统计页时基本不会有加载白屏。
4.3 真机兼容问题:iOS 键盘遮挡与安卓蓝牙适配
V1.0.39 之前,我们在真机兼容上花的时间比开发新功能还多。一个高频问题是 iOS 上键盘弹起时遮挡输入框。微信官方配置里有一个 adjust-position 属性,默认在键盘弹起时上推页面,但实测在某些 iOS 版本上会失效。我们的兜底方案是:手动监听键盘高度变化,把输入框滚动到可视区域。
wx.onKeyboardHeightChange((res) => { if (res.height > 0) { // 输入框需要上移的高度,根据页面元素位置动态计算 this.setData({ keyboardHeight: res.height }) } else { this.setData({ keyboardHeight: 0 }) } })另一个很多人问的,是安卓 14 上小程序蓝牙模块的连接问题。榆落微时光早期版本做过一个“蓝牙打卡”的辅助功能,踩过的坑主要是:安卓 12 之后的权限模型更严格,蓝牙扫描必须先动态申请定位权限和附近设备权限;BLE 设备的扫描回调在某些国产 ROM 上有延迟,不能只用 onBluetoothDeviceFound 无限等待,要加上超时判断。如果你没有蓝牙需求,这段可以跳过,但如果你准备做智能硬件类小程序,一定要在权限申请时机和超时处理上多留几个心眼。
4.4 异常监控与告警:从“用户说打不开”到主动发现
线上问题最怕的不是 bug,而是用户碰到问题之后你没有一点感知。榆落微时光在 V1.0.39 期间接入了轻量级错误上报:前端在小程序 app.js 里监听 onError 和 onUnhandledRejection,把错误信息、页面路径、机型、系统版本通过一个独立接口上报到后端;后端记录错误日志,同时在接口耗时异常时打点告警。
还有一类问题是前端请求到了后端,但后端处理超时。我们给所有外部依赖都设置了超时时间,并在请求失败时做了指数退避重试。不要小看这个设计,线上偶发请求失败如果下一秒所有用户同时重试,会把后端打挂。指数退避配合 jitter(随机抖动),能把重试风暴的概率降得很低。
5. 前后端分离的小程序从开发到发布:部署、合规与备份
5.1 本地开发环境的几个关键点
本地跑这个项目时,最容易犯的错误是“在小程序开发者工具里勾选不校验合法域名”然后开发完就忘了。这个选项只适合本地调试,上线前一定要到微信公众平台把 API 域名和 uploadFile 合法域名都配上,并且域名必须是 HTTPS。
本地联调时,我们一般不用“本地代理”那套复杂配置,而是让后端服务监听局域网 IP,小程序开发者工具直接请求 http://192.168.x.x:3000,同时勾选本地不校验域名。手机预览时,手机和电脑连同一个局域网,把 BASE_URL 改成电脑 IP 即可。测试环境、生产环境的配置一定要拆开,用环境变量控制,避免上线时忘了切换地址,所有请求打到测试库还把数据写乱了。
5.2 用 Docker Compose 编排整套后端环境
榆落微时光的后端部署用 Docker Compose 管理,这是目前中小型前后端分离项目最省心的一套方案。相关服务包括 Nginx、API 服务、MySQL、Redis,一次 docker compose up -d 就能全部起来。
version: '3.8' services: nginx: image: nginx:1.24-alpine ports: - "80:80" - "443:443" volumes: - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro - /etc/letsencrypt:/etc/letsencrypt:ro depends_on: - api restart: always api: build: ./server environment: - NODE_ENV=production - MYSQL_HOST=mysql - MYSQL_PORT=3306 - MYSQL_USER=yulu_app - MYSQL_PASSWORD=please_use_env_file - REDIS_HOST=redis ports: - "3000:3000" restart: always depends_on: - mysql - redis mysql: image: mysql:8.0 environment: - MYSQL_DATABASE=yulu_microtime - MYSQL_USER=yulu_app - MYSQL_PASSWORD=please_use_env_file - MYSQL_ROOT_PASSWORD=please_use_env_file volumes: - ./data/mysql:/var/lib/mysql restart: always redis: image: redis:7-alpine volumes: - ./data/redis:/data restart: always这里要特别提醒:生产环境不要把这些密码直接写死在 docker-compose.yml 里,应该用 .env 文件管理,并将 .env 加入 .gitignore。还有一个很容易忽略的问题:MySQL 容器升级镜像时,如果数据卷配置不对,可能造成数据丢失。所有数据库文件都要挂载到宿主机目录,并且定期备份。
5.3 Nginx 层要做的事:HTTPS、路径转发与静态资源缓存
Nginx 在这里承担了整套环境的入口。小程序要求所有请求域名必须是 HTTPS,所以 Nginx 层必须配置好证书,并把 443 端口收到的请求转发到 API 容器。
server { listen 443 ssl http2; server_name api.example.com; ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem; location /api/ { proxy_pass http://api:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }注意,proxy_pass 后面的地址使用了 api,这是 Docker Compose 服务名,在同一个网络内可以直接访问。Nginx 还会帮我们做一层访问控制,比如限制上传接口的单 IP 请求频率,防止有人用脚本恶意刷图。这个在公网环境下还是挺重要的。
5.4 发布前的合规与运营细节
小程序上线前有一堆和代码无关但必须处理的事情,漏掉任何一个都会被审核打回。榆落微时光是记录类工具,类目选择上属于工具类别,不涉及在线播放、观看服务,所以不需要选择文娱视频类目。如果你的小程序涉及视频播放或直播,那就要提前补充“文娱-其他视频类目”以及对应的资质材料,这点审核卡得非常严格。
另外几个容易被忽略的运营细节:
- ICP 备案和域名备案:小程序 API 域名必须完成备案,而且小程序本身也需要完成平台侧的备案流程;
- 用户隐私保护指引:微信后台要求填清楚收集哪些用户信息,小程序代码里调用 wx.getPrivacySetting 判断用户的隐私授权状态,不能用“一刀切”方式强制获取;
- 订阅消息:如果要做“明天提醒”之类的服务,提前在后台申请订阅消息模板,测试号也有次数限制;
- 灰度发布:微信公众平台支持分阶段发布,实践下来最稳妥的方式是先 5%,观察线上错误率和用户反馈,再逐步放量到全量。
5.5 数据备份与回滚:把事故变成小麻烦
任何项目做到后期,都必须把“万一挂了怎么办”当成一个正式需求来对待。榆落微时光的数据备份策略很朴素但很有效:每天凌晨通过 cron 使用 mysqldump 备份整个数据库,保留最近 7 天;对象存储本身有服务商提供的高可用和版本管理,不额外处理;后端镜像在构建时打上版本 tag,比如 api:v1.0.39,发布新版本时如果发现严重问题,可以直接 docker compose 指定旧镜像回滚。
前端的回滚更简单,微信公众平台支持把线上版本回退到任意一个历史审核通过版本。但前提是后端接口要做向前兼容,所以一个原则是:后端先发布新接口,保留旧接口一段时间,等小程序新版本覆盖率达到预期后再下线旧接口。这个习惯能避免很多“前端没更新完,后端已经删了字段”的线上事故。
把一个小程序从原型做到 V1.0.39,最大的收获不是这个版本号本身,而是把一个又一个“看着没问题”的隐患变成了“确定没问题”的方案。记录型小程序的技术难度不在于某个刁钻的算法,而在于每一个环节的数据流是否清晰、每一类异常情况是否有兜底、每一次发布是否有回退路径。我个人比较深的体会是:这类项目一定要把请求层封装做干净、把后端鉴权想透、把部署流程固化下来,否则每次改需求都会像在补一个不断扩大的窟窿。如果你也正在维护一个前后端分离的小程序,建议先把这几个基础问题解决好,再谈加功能和换框架。项目稳定跑上一年之后,你会发现当初在这些“枯燥”环节上花的时间,才是回报率最高的投入。
本文还有配套的精品资源,点击获取