如果你只在微信开发者工具里把几个页面跑起来,小程序开发会给人一种错觉:它好像只是一个受限一点的前端工程。
但只要你真的把一个小程序从 0 推到上线,再跟着用户跑上几轮真实使用,你基本都会收回这个判断。
小程序的问题,从来不只出在页面本身。它的问题会沿着整条链路冒出来:运行时环境、npm 兼容、网络请求、登录态、文件上传、域名白名单、弱网、审核、真机行为、线上监控。很多东西在浏览器里默认成立,到了小程序里全部得自己重新确认一遍。
这篇文章不是“十大注意事项”那种泛泛总结,而是我在交付小程序时最常遇到、也最容易反复出事故的十个点。它们有些是技术问题,有些是工程问题,但共同点都很一致:在本地看起来能跑,并不代表到了真实环境就能活。
1. 第一个误判:把小程序当浏览器
这是很多坑的起点。
前端工程师看到 WXML、WXSS、JS,第一反应很容易是:无非是一个有点特殊的前端容器。这个理解只对了一半。
小程序确实长得像前端项目,但它的运行模型和浏览器差别很大。逻辑层不是一个完整浏览器环境,很多 Web 里默认可用的能力根本不存在,或者语义不一样。你以为一个 npm 包只是“拿来用一下”,实际上它内部可能依赖了:
windowdocumentfetchlocalStorage- 标准
WebSocket FormData- 某些浏览器事件模型
- Node.js polyfill
其中任何一个点不兼容,都足够让一个看起来普通的 SDK 在小程序里直接失效。
这件事带来的后果不是“报一个错然后修掉”,而是你对依赖的判断方式必须改变。
我现在引入小程序依赖时,脑子里会先过这几个问题:
- 它官方到底支不支持小程序,还是只是“理论上可以试试”?
- 它底层依赖的是协议,还是具体运行时对象?
- 如果不兼容,我是要 patch 它,还是干脆自己写一个薄封装?
- 这个依赖值不值得为了它承担后续维护成本?
很多时候,自己写一层 100 行的适配代码,比把一个浏览器世界里的大 SDK 硬拽进来要省心得多。
2. 第二个误判:网络请求只是把 fetch 换成 wx.request
很多教程会把小程序网络层说得很轻巧,好像只是“浏览器里用 fetch,小程序里用 wx.request”。如果只是发一个匿名 GET,这么理解还凑合;一旦进入真实项目,这种说法就会迅速失效。
原因很简单:请求函数从来不只是一个“发包工具”,它通常还承担了登录态注入、超时处理、统一错误结构、幂等控制、日志上报、重试和取消逻辑。
而小程序的请求体系和浏览器差异,会把这些工作全暴露出来。
比如你在 Web 里习惯了 axios 拦截器,到了小程序里很可能要自己重新搭一层:
async function request({ url, method = 'GET', data, headers = {} }) {
const token = wx.getStorageSync('access_token')
return new Promise((resolve, reject) => {
wx.request({
url,
method,
data,
header: {
...headers,
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
success: (res) => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(res.data)
} else {
reject(res)
}
},
fail: reject,
})
})
}
这段封装表面看很普通,但它背后其实是一个原则:请求入口必须统一。
如果项目里每个页面都自己写 wx.request,后面 token 续期、错误提示、弱网上报、埋点、接口切换几乎都会变成体力活。统一请求层的意义,从来不是优雅,而是让你后面还有机会集中治理。
3. 登录不是难在“拿到 token”,而是难在 token 出问题的时候
我见过很多小程序项目,登录流程本身其实写得并不差。真正出事故的地方,几乎都在登录态生命周期上。
最典型的问题是 token 过期时的并发刷新。
假设首页一进来会并行请求用户信息、列表数据、消息未读数和某个推荐区块。只要这几个请求刚好撞上 access token 过期,你就会同时收到一批 401。接下来如果每个请求都各自发起 refresh,就会进入最混乱的阶段:
- 第一个 refresh 成功了
- 第二个 refresh 用了旧 token,失败了
- 某个请求拿旧 token 重试
- 某个请求用新 token 成功
- 最终客户端本地到底写进了哪一份 token,不一定说得清
这类 bug 在开发环境不一定稳定复现,但线上一旦出现,会非常像玄学问题。
所以登录系统真正需要设计的,不是“怎么登录一次”,而是:
- refresh 请求是否做了去重
- 本地 token 写入是否是单点收口
- 请求失败后重试是否依赖最新 token
- 后台唤醒时是否会提前做一次 token 检查
- refresh 失败后是否有统一降级策略
很多人第一次做小程序登录,会把重点全放在 wx.login()、openid、JWT 这些入口名词上。我的经验是,入口反而是比较容易做对的部分,真正难的是后面的会话管理。
4. npm 兼容问题,通常不会在你准备好的时候出现
微信开发者工具支持 npm,这件事经常会给人一种安全感,好像小程序已经和常规前端工程完全打通了。真做起来你会发现,它支持的是“有条件的 npm”。
最常见的几类坑我基本都踩过:
- 依赖里混进了小程序运行时不支持的语法
- 包内部引用 Node.js 内置模块
- 打包后路径和预期不一致
- 某些依赖在本地构建正常,开发者工具里却表现异常
- 工具缓存导致你明明改了东西,界面却像没更新
更麻烦的是,这类问题经常不是出在你自己写的代码里,而是出在依赖树深处。你引了一个看起来无害的库,结果它的子依赖某个版本里带了不兼容实现,最后报错信息还不一定指向真正源头。
这也是为什么我做小程序项目时,会比普通 Web 项目更克制地引依赖。
我的默认心态是:只有当一个库明显比自己写更省总成本时,才值得带进来。
这里的“总成本”不只是开发时间,还包括:
- 小程序环境兼容性
- 包体积
- 后续 patch 成本
- 换版本风险
- 真机行为不一致
如果一个能力底层只是 REST 协议,自己写适配层往往比强行引 SDK 更稳。这也是我后来处理 Supabase 小程序接入时的核心判断之一。
5. 文件上传不是一个“特殊请求”,而是一条单独的链路
很多项目会在一开始把上传看得很轻,觉得无非就是多传个文件。等到真正接业务时,上传很快会从“一个接口”变成“一个子系统”。
因为小程序里的上传和普通请求本来就不是一类东西。它通常要走 wx.uploadFile,而不是统一请求函数。这意味着你原本为请求层做的很多约定,到这里都要重新考虑:
- 鉴权头怎么加
- 错误结构怎么统一
- 返回值是不是还需要手动
JSON.parse - 上传进度怎么暴露给 UI
- 失败后是否可以安全重试
更重要的是,上传真正难的地方往往不在“传”本身,而在文件生命周期。
例如你要不要把文件路径和业务数据同事务保存?如果业务记录删除了,文件是否需要异步清理?私有文件是走公开 URL 还是签名 URL?同一个文件是否支持替换?对象路径怎么设计,才能避免以后迁移困难?
这些都不是表面问题。
我后面再看小程序上传模块时,已经不会只看“图片能不能上去”,而是会问这几件事:
- 上传前有没有做体积和类型校验
- 上传过程中用户是否知道当前状态
- 上传失败后能否恢复
- 存的是短期链接,还是稳定路径
- 文件权限边界是否明确
如果这些没想清楚,上传功能就只是“今天能演示”,不是“明天能维护”。
6. 域名白名单这件事,往往是最后一刻才暴露你准备得并不完整
微信小程序的域名白名单很烦,但我后来觉得它烦得有道理。因为它逼着你把自己到底依赖了哪些外部能力这件事想清楚。
项目里只要一旦接了后端、文件、实时、图片、地图、支付、短信,域名就不再是一个配置项,而是一张完整的依赖清单。更麻烦的是,小程序把它拆成了多个维度:
- request 合法域名
- socket 合法域名
- uploadFile 合法域名
- downloadFile 合法域名
这意味着你不能简单地说“我已经把接口域名配了”。如果你用了 Supabase,至少还要继续追问:
- REST API 走的是哪个域名
- Auth 请求是否复用同域
- Realtime 是不是另一个
wss - Storage 上传和文件下载是不是也都落在允许名单里
这类问题在本地开发工具里很容易被掩盖,因为调试环境和正式真机环境的约束不完全一样。最后表现出来的症状也经常很绕,比如图片上传失败、socket 连接不上、某个资源只在特定机型打不开。
我现在做上线前检查,一定会单独整理一张外部域名表。不是为了形式,而是因为不这样做,域名问题几乎总会在最后一天来找你。
7. 弱网不是异常场景,它才是很多用户的常态
开发时坐在稳定网络下,人很容易高估应用的韧性。
真实用户可不会给你这么理想的环境。地铁里、电梯里、商场里、切后台回来之后、网络从 Wi-Fi 切到蜂窝之后,小程序表现出来的不是“偶尔慢一点”,而是很多设计时没想过的边界同时暴露。
一个功能在弱网里通常会暴露出两类问题。
第一类是交互状态不清晰:
- 用户不知道请求是不是还在进行
- 失败之后没有恢复入口
- 提交按钮没禁用,导致重复提交
- 页面返回后旧请求还在落地,覆盖了新状态
第二类是系统语义不清晰:
- 请求到底是没发出去,还是服务端处理失败
- 重试是否安全,会不会造成重复数据
- 上传中断后能否续传,还是只能整包重来
- WebSocket 断开后重连是否会漏消息
这些问题如果只靠“多试几次”来应对,迟早会在生产环境里变成投诉。
所以我现在看一个关键交互,除了 happy path,还会单独看它的失败状态有没有被正式设计出来。比如提交操作,我至少希望它有这些明确语义:
- 正在提交
- 提交失败
- 可重新提交
- 已成功,且重复点击无效
前端上这些状态是体验问题,服务端上这些状态其实就是幂等和一致性问题。两边缺一个,用户体验都会崩。
8. 审核不是最后要点一下的按钮,而是产品边界的一部分
很多技术团队会把审核当成收尾动作:功能都做好了,最后整理资料提审。
这个想法在小程序里非常危险。
因为审核规则会反过来影响你的产品设计。比如:
- 你的内容到底是不是用户生成内容
- 是否需要审核机制或举报入口
- 某些功能是否要求登录后才可见
- 是否必须提供测试账号
- 用户协议和隐私政策是否覆盖了你真实采集的数据
这些问题如果你在开发尾声才看,会直接导致返工。因为它们不是“补一段文案”的问题,而是会影响功能入口、数据流、后台能力甚至角色设计。
我现在做小程序项目时,会在很早期就把审核风险当成约束列出来。尤其是涉及内容发布、用户资料、外部链接、支付、订阅消息这几类功能时,更不能抱侥幸。
审核不是一个行政动作,它本质上也是平台施加给产品的一层规则系统。
9. 真正让人头疼的,通常不是 bug 本身,而是线上出事后你完全不知道发生了什么
我见过不少小程序项目,功能本身没那么差,真正致命的是出问题以后没有任何观察手段。
用户说“刚刚提交失败了”,你只能问“什么时候”。
用户说“页面卡住了”,你只能问“你再试一次呢”。
这不是技术能力问题,是系统没有给你留下任何可诊断的痕迹。
所以小程序哪怕项目不大,我也会尽量至少留三类信息:
- 请求错误:接口、状态码、错误消息、用户上下文
- 前端异常:页面路径、操作链路、错误堆栈
- 关键业务事件:登录、提交、上传、支付、发布
哪怕一开始不接完整监控平台,至少也要在请求层和错误入口预留统一上报点:
function reportError(error, context) {
console.error('[miniapp-error]', context, error)
}
这段代码本身当然很简陋,但它代表的是一种意识:线上问题不应该只存在于用户聊天记录里。
10. 小程序全栈开发最怕的,不是技术不会,而是整条链路没人兜
最后一个坑,严格说已经不是某个点,而是一个总体现象。
小程序项目很容易让团队误以为自己在做一件“小东西”。结果是每个人都只盯自己那一段:前端盯页面,后端盯接口,产品盯流程,测试盯功能点。真正横向串起整条链路的问题,反而没人负责。
但小程序从来都不是一个单点系统。它至少包含这样一条链:
这条链上每个节点都可能单独看起来“没问题”,但只要某一段没人整体兜底,最终交付体验就会很差。
所以我后来会很依赖各种清单。不是因为我喜欢流程,而是因为小程序里低级事故太多了:
- 域名清单
- 环境变量清单
- 登录链路清单
- 上传权限清单
- 审核材料清单
- 上线回归清单
这些东西看起来很土,但它们常常比多写一个抽象层更能避免事故。
最后一句总结
微信小程序全栈开发最麻烦的地方,从来不是某个 API 不顺手,而是它会不断提醒你:你做的不是一个页面集合,而是一整条需要在真实平台、真实网络、真实设备和真实审核规则里运行的交付链路。
如果只把它当“轻量前端项目”,那这些坑基本一个都不会少。