很多人第一次做小程序,会下意识把它看成一个小项目。页面不算多,功能也许只有几块,后台可能也不复杂,于是大家很容易觉得:先做起来再说,边做边定。
我以前也这么干过。后来发现,小程序从 0 到 1 最容易失控的地方,不是开发量,而是决策延迟。
什么叫决策延迟?就是那些本来应该在早期想清楚的问题,被一路往后拖,拖到后面发现它们会反过来掀翻已经写好的页面、接口和数据库。这种事在小程序里尤其要命,因为小程序有两个 Web 开发没有的硬约束:
- 审核周期。Web 一个 bug 可以随时修随时上线,小程序每次发版都要过审,轻则几小时,重则几天。如果你把关键决策拖到上线前才发现问题,代价不是"改一行代码",而是"改完后重新排队审核"。
- 包体积限制。主包 2MB、总包 20MB 的上限,意味着你不能像 Web 一样无节制引入依赖。如果你写了大量代码和组件后才意识到要分包,改造成本会很高。
这两个约束决定了:小程序的决策不能边走边定,得按一个明确的先后关系来。
这个先后关系不是我在文章里随便编号,而是决策之间的实际依赖:上一环的结论是下一环的输入,跳过任何一环都会在后面被迫返工。 把这条链画出来大概是:
登录模型 → 数据归属 → 数据模型 × 权限 → 后端选型 → 前端分层 → 异常兜底 → 审核上线
下面按这条链逐个展开。
登录模型是第一张多米诺骨牌
小程序登录和 Web 完全不同。Web 你可以自由选 JWT、Session、OAuth、OIDC,但小程序只有一条官方路径:wx.login → 拿到临时 code → 传给后端换 openid。
这个流程有三件事很容易踩坑。
第一,code 只能消费一次,且有效时间很短。如果你不小心在前端重复调了 wx.login,或者后端处理慢了,code 就会过期。所以 wx.login 的调用时机必须严格控制——它不是你随时想要就能拿来用的。
第二,openid 是每个用户在小程序里的唯一标识,但它只在小程序内有效。如果你有公众号、网页或其他微信体系内的产品,需要用到 unionid 来打通用户。这要求你的小程序和公众号绑定在同一个开放平台下,一开头没做绑定,后面不同端的用户数据就是孤岛。
第三,也最容易引发返工的问题:用户什么时候算登录?
是打开小程序就静默登录(用户无感),还是必须点一个登录按钮?这个决定会像多米诺骨牌一样影响后面所有东西。静默登录意味着用户一进来你就能拿到 openid,核心功能可以无感衔接,但审核时体验版必须在显著位置提供说明。强制登录意味着你多了一个登录页,涉及登录态过期、重新授权、用户拒绝授权的兜底——这些不是"以后加个按钮",而是一个完整的状态链路。
我曾经做一个预约类小程序,最开始选了强制登录。做到一半发现,预约列表是公开资源,用户只是想浏览,提前弹登录框非常打扰。后来改成静默登录——先用 wx.login 拿到 openid 但不阻塞页面,用户真正提交预约时才检查是否需要补充手机号等信息。这个改动听起来不大,但登录模型一换,数据归属、权限规则和接口设计全都需要调整。
所以第一条决策链是:先想清楚登录模型,因为数据归属关系直接由它决定。 你的数据是挂在 openid 下面,还是挂在用户主动创建的某个实体下?登录态过期后哪些数据仍然可见?这些问题一旦开始写数据表就改不动了。
数据模型和权限是一件事,不该分两步想
很多项目会把数据模型和权限当成两个独立的步骤——先设计表,再加权限。这在小程序里会出问题,因为小程序的权限逻辑天然和数据归属绑在一起。
举个例子,一个协作型小程序可能有这些核心实体:
这张图看起来简单,但它直接决定了权限怎么写。PROJECT 的归属是 USER,那么"谁能看这个项目"的答案就是:项目创建者 + 被邀请的成员。COMMENT 的归属也是 USER,那么"谁能删这条评论"的答案就是:评论作者或项目管理员。
一旦你把数据归属和权限放在一起想,很多问题会自己冒出来:
- 游客能看到什么?如果静默登录,游客已经在后端有了
openid,那他跟登录用户的边界在哪? - 用户能不能修改别人的数据?如果不能,"别人数据"怎么定义——不是自己创建的就算别人,还是不在同一个项目里才算别人?
- 删除是真删还是软删?如果某个用户被删除了,他的项目、任务、评论怎么办?
这些问题的答案,会直接落成你技术方案里的结构。如果你用 Supabase,它们会变成 RLS 策略:
CREATE POLICY "用户可以读取自己项目的任务"
ON tasks FOR SELECT
USING (project_id IN (
SELECT id FROM projects WHERE owner = auth.uid()
));
如果你用自建后端,它们会落在中间件或 service 层。不管你选什么技术,权限模型本身不能后加——因为它决定了你数据层的查询范围,后加意味着你之前写的所有不带权限过滤的查询都是错的。
所以这条链的第二步是:数据模型和权限一起画,因为数据归属是权限的输入,权限反过来又会约束数据的查询方式。
后端选型:本质是选你愿意长期承担哪类成本
数据模型和权限想清楚之后,后端选型就不再是"哪个最好"这种空对空的讨论了。你会带着具体的判断标准去看每个方案:
- 你的权限模型用 RLS 表达得清楚吗?
- 你的数据关系对查询能力有什么要求?
- 你有没有需要深度自定义的业务规则?
小程序常见的后端方案大概就四类:
微信云开发。和微信生态深度耦合,调用 wx.cloud 可以直连数据库和云函数,省掉自建 HTTP 服务的成本。但代价也很具体——云函数有冷启动延迟(首次调用可能等 1-3 秒),数据库集合有 20MB 上限,索引数量有限制,不能执行复杂 SQL 查询。如果你的数据模型简单、查询模式固定、对实时性不敏感,云开发是很舒服的选择。但如果你的业务规则复杂,或者数据量增长预期明确,就要谨慎。
Supabase。本质上是 PostgreSQL,你拿到的是完整的 SQL 能力、成熟的 Auth 体系和 RLS 行级安全。在小程序场景里,最大的挑战是登录映射——你需要把微信的 openid 变成 Supabase 的 auth.users 里的一个标识。这通常需要自建一个 token-exchange 端点,用 wx.login 换到的 code 去生成 Supabase 的 JWT。这个环节不难,但很容易被忽略。
自建 Node.js 服务。控制力最强,但你需要自己处理的事情也最多——数据库连接池、日志、监控、部署、回滚。如果项目业务规则非常重,或者明确要对接企业内部系统、自定义工作流,自建反而更稳,因为你不会受平台能力的边界约束。
BaaS + 自建逻辑层。比如数据库用 Supabase,核心业务逻辑放在 Node.js 服务里。这种混合方案灵活性最高,但架构多了一层,排障会更复杂。
选型没有标准答案,关键不是它"有多强",而是你选的这条路在未来六个月里会不会成为瓶颈。
前端结构不需要炫技,但要同时考虑分层和包体积
小程序前端有一个其他平台没有的约束:包体积上限。 主包 2MB,分包总 20MB,超过就发不出去。这意味着你做前端分层时,不能只考虑代码组织的整洁,还要考虑"哪些代码应该放在主包、哪些可以拆到分包"。
一个实用的做法是用功能边界而不是技术类型来分目录:
miniprogram/
pages/ # 主包页面(首页、tabBar 页面)
packages/ # 分包(按业务域拆)
booking/ # 预约相关分包
profile/ # 个人中心分包
components/ # 公共组件
services/ # 请求层、Auth 层
request.js # 统一封装 wx.request
auth.js # 登录态管理
store/ # 跨页面共享状态
utils/ # 纯函数,不偷塞业务逻辑
config/ # 环境、常量
这个结构背后有几个取舍:
services/request.js是唯一发请求的地方。这意味着当你需要统一加错误处理、埋点、重试逻辑时,只有一个入口要改。auth.js集中管理登录态,不散落在各个页面。wx.login的调用时机、token 存储和刷新逻辑,都应该只在这里发生。store/只放真正跨页面共享的状态,不是所有数据都往里面扔。很多数据只在单个页面内流动就够了,放进全局 store 反而增加心智负担。
这种分层本身没什么技术浪漫,但它在你需要修改登录模型、替换后端地址、加错误处理时,会让你庆幸自己没有把所有事情摊在页面里。
异常兜底不是功能,是交付质量的一部分
小程序还有一个特点:运行环境不可控。用户可能在弱网下打开,可能在 iOS 和 Android 上表现不一致,可能在后台被系统回收后再切回来。这些场景 Web 也有,但小程序的用户通常不会像 Web 用户一样习惯"刷新试试"。
所以我看一个功能时,不只盯着成功路径,会默认检查一组状态:
- loading——请求中的占位,不能是一个空白页面
- empty——没有数据时给一个明确说明,而不是"看起来坏了"
- error——服务端报错时的提示,以及用户接下来能做什么
- offline / timeout——小程序特有的
wx.onNetworkStatusChange可以用,但至少要在请求超时时给用户一个可操作的状态 - 登录态过期——用户切到后台几小时再切回来,
wx.login拿到的 session 可能已经失效
一个典型场景:表单提交。如果只写 happy path,就是"填完 → 提交 → 成功"。但实际上你必须回答:提交中按钮是否禁用(防重复提交)、网络超时后用户看到什么、服务端校验失败后能不能明确提示哪个字段有问题、登录态过期时流程如何恢复。
这些问题提前想清楚并在代码里收口,交付质量会完全不一样。后补也能做,但成本不是加代码,而是你要重新读一遍自己当初的假设。
上线审核不是流程尾巴,而是一个设计约束
小程序上线前有一道 Web 没有的关卡:微信审核。 审核周期从几小时到几天不等,常见拒审原因包括:
- 登录页没有提供非登录状态的可浏览内容
- 涉及用户隐私但没有隐私政策
- 使用了"诱导分享"类的文案或交互
- 页面功能不完整(比如按钮点了没反应、空页面太多)
这些问题如果在开发早期没人关注,到了上线前突然被拒,改代码的成本可能不高,但重新排队审核的时间成本会直接延后发布日期。
所以审核这件事不能到上线前再说。开发阶段就要确认:审核时体验版需要提供什么测试账号,哪些页面在未登录状态下也能正常展示,隐私弹窗是否收集了所有实际使用的权限。
除此之外,上线前还需要做一轮真机回归。微信开发者工具和真机行为之间的差异,小程序的开发者都懂——有些样式在模拟器上完美,真机上跑偏;有些 API 在开发者工具里正常,真机上行为不一致。iOS 和 Android 也要分别验证,因为小程序在不同系统上实际上运行在不同的渲染引擎上(iOS 用 WKWebView,Android 用 XWeb 内核,或者新版的 Skyline 渲染引擎)。
至少这几件事要在真机上过一遍:登录链路、核心提交链路、文件上传、分享功能。
最后
从 0 到 1 交付一个小程序,不是把页面都做完、把接口都接上,而是按一条明确的依赖链把关键决策尽早回答掉。
登录模型 → 数据归属 → 数据模型和权限 → 后端选型 → 前端分层 → 异常兜底 → 审核上线。这条链上每一个环节的输出,都是下一个环节的输入。跳过任何一个,返工成本都会在后面成倍放大。
技术决策的价值,从来不在于它听起来多先进,而在于你回头时发现,自己不需要把做过的事拆掉重来。