微信小程序开发从入门到精通:环境搭建、WXML/WXSS、组件库、架构、平台能力与发布全指南
- 约 44 分钟读完 - 全文 17573 字微信小程序开发从入门到精通:环境搭建、WXML/WXSS、组件库、架构、平台能力与发布全指南
目标读者:想进入小程序开发的前端同学,或者写过 Vue/React、想搞清楚「小程序和网页开发到底差在哪」的同学。
知识骨架与编排方式参考姊妹篇《Android 开发从入门到精通》——同样是「由浅入深、踩坑驱动」。写 UI 的心法还可以看《前端 UI 开发从入门到精通(Vue 技术栈)》,小程序的组件化思想和 Vue 一脉相承。
全文共八篇,顺序就是学习的顺序——每一篇都依赖前一篇打下的基础:
flowchart LR
A["一 环境搭建<br/>注册 AppID 跑起 Hello World"] --> B["二 语法基础<br/>WXML/WXSS/JS/JSON 四件套"]
B --> C["三 UI 组件库<br/>写出像样的页面"]
C --> D["四 架构与生命周期<br/>从写页面到写应用"]
D --> E["五 网络与存储<br/>数据从哪来 存到哪去"]
E --> F["六 平台能力<br/>登录 订阅消息 分享 支付"]
F --> G["七 质量与发布<br/>性能 体验版 审核上线"]
G --> H["八 AI 时代<br/>让 AI 写 80% 的代码"]
style H fill:#e8f4e8
为什么是这个顺序?前三篇解决「能不能写出来」;第四篇解决「页面多了以后会不会散架」;第五、六篇解决「能不能和后端、和微信这个平台打交道」;第七篇解决「敢不敢提交审核发给真实用户」;第八篇解决「如何持续快下去」。
第一篇 环境搭建:比 Android 友好,但坑藏在「账号」里
先说好消息:小程序的环境搭建是整个客户端开发里最友好的——不用装 JDK,不用配 SDK,不用 Gradle 转圈。坏消息是:门槛从「装软件」转移到了「注册账号」,而账号相关的坑往往要在提交审核那天才爆出来。
1.1 你需要准备的三样东西
flowchart TD
A["小程序账号 (AppID)<br/>mp.weixin.qq.com 注册"] --> D["微信开发者工具"]
B["微信开发者工具<br/>官方 IDE 写代码 + 模拟器 + 调试"] --> E["能跑起 Hello World<br/>= 入门完成一半"]
C["微信扫码登录<br/>一个微信号即可"] --> D
D --> E
动手清单:
- 注册小程序账号:到 mp.weixin.qq.com 选择「小程序」注册,拿到 AppID(在「开发管理 → 开发设置」里)。注意一个邮箱只能绑定一个微信公众产品账号。
- 下载微信开发者工具:认准官方页面,选稳定版(Stable Build)。它是「编辑器 + 模拟器 + 调试器 + 发布工具」四合一,写小程序不需要再装别的。
- 新建项目:填入 AppID,模板选「不使用云服务」的最小模板,后端服务选「不使用模板」。模拟器里看到 Hello World 即通关。
如果只是学习不打算发布,也可以在新建项目时选「测试号」,跳过注册——但测试号很多能力(支付、订阅消息、发布)用不了,建议还是注册一个正式的个人账号。
1.2 踩坑故事一:类目选错,审核排队两周白排
现象:朋友的小程序功能全部做完,信心满满提交审核,第二天被拒:「所选类目与小程序实际内容不符」。改类目需要提供《增值电信业务经营许可证》——他没有。推倒重来,换了个人主体账号重新注册,前面所有配置全部作废。
原因:注册时选择的服务类目和主体类型(个人 / 企业)在注册后很难改,而且直接决定你的小程序能做什么:
flowchart TD
Q{"小程序要做什么?"}
Q -->|"内容展示 / 工具<br/>不涉及交易"| A["个人主体即可<br/>注册免费 门槛低"]
Q -->|"电商 / 需要微信支付"| B["必须企业主体<br/>+ 对应经营类目"]
Q -->|"医疗 / 金融 / 社交"| C["企业主体 + 行业资质<br/>提前查资质清单再动手"]
style C fill:#ffe4e1
教训:先查类目,再写代码。微信官方有《小程序开放的服务类目》文档,写清楚每个类目需要什么资质。个人主体不能碰电商交易、不能做陌生人社交——无数个人开发者做到一半才发现「个人号不支持」,这是小程序生态最贵的一个坑,因为它爆在最后一步。
1.3 踩坑故事二:「不校验合法域名」的虚假安全感
现象:开发期接口调得好好的,一到真机预览就白屏,控制台报 request:fail url not in domain list。
原因:小程序的 wx.request 只能请求在 mp 后台预先配置过的 HTTPS 域名(request 合法域名)。开发者工具里有个「详情 → 本地设置 → 不校验合法域名」的开关,很多人开发期一直勾着它,勾到忘记它的存在——而真机上这个开关不存在(除非打开调试模式)。
解法:
- 去 mp 后台「开发管理 → 开发设置 → 服务器域名」配置 request 合法域名,必须 HTTPS,且域名要 ICP 备案。
- 开发期就把「不校验合法域名」关掉,让问题暴露在写代码的时候,而不是发给产品经理体验的时候。
教训:小程序的「域名校验」和 Android 的「明文流量拦截」是同一类东西——平台用白名单逼你上 HTTPS。这类安全约束的特点是「先放行以后再收」基本等于永远不收,第一天就把域名配好。
第二篇 语法基础:四个文件就是一个页面
环境通了,接下来是语言。小程序的语法对前端来说几乎没有学习成本——它就是「受限版的 Vue」:模板 + 数据绑定 + 指令 + 组件化。这一篇的重点不是语法本身,而是它和自由 Web 的差异,坑全在差异里。
2.1 一个页面 = 四个文件
flowchart TD
A["index.wxml<br/>模板 ≈ HTML<br/>写结构"] --- P["一个页面"]
B["index.wxss<br/>样式 ≈ CSS<br/>多了 rpx 单位"] --- P
C["index.js<br/>逻辑:Page({...})<br/>data + 生命周期 + 事件处理"] --- P
D["index.json<br/>配置:导航栏标题<br/>引入哪些组件"] --- P
P --> E["在 app.json 的 pages 数组里<br/>注册路径才算存在"]
全局还有一组 app.js / app.json / app.wxss:App 的生命周期、全局配置(页面注册、tabBar、窗口样式)、全局样式。
一个最小页面长这样:
<!-- pages/index/index.wxml -->
<view class="container">
<text class="title">{{title}}</text>
<view wx:for="{{todos}}" wx:key="id" class="todo">
<text class="{{item.done ? 'done' : ''}}">{{item.text}}</text>
</view>
<button bindtap="onAdd">添加</button>
</view>// pages/index/index.js
Page({
data: {
title: '我的待办',
todos: [
{ id: 1, text: '学会 WXML', done: true },
{ id: 2, text: '学会 setData', done: false },
],
},
onAdd() {
const todos = this.data.todos.concat({
id: Date.now(),
text: '新任务',
done: false,
});
// ⚠️ 改数据必须走 setData,直接 this.data.todos = todos 界面不会刷新
this.setData({ todos });
},
});2.2 setData:小程序开发的第一性原理
小程序的视图和逻辑跑在两个不同的线程:视图层是 WebView,逻辑层是 JSCore。你在 JS 里改 data,要通过 setData 把数据序列化后跨线程发给视图层,界面才会更新。
sequenceDiagram
participant L as 逻辑层 (JSCore)
participant B as 原生桥 (Native)
participant V as 视图层 (WebView)
L->>B: this.setData({ todos })
Note over B: 数据序列化传输<br/>大对象 / 高频调用都在这里变慢
B->>V: 更新 data
V->>V: 重新渲染 WXML
这一个架构事实解释了小程序里 80% 的性能问题和 80% 的新手困惑:
- 直接赋值
this.data.x = 1数据变了但界面不刷新——因为没有经过那座桥。 setData传几十 KB 的大对象会卡——桥上的运输成本。- 在
onPageScroll里每次滚动都setData会卡成 PPT——桥上堵车。
现在只需要记住结论:setData 是有成本的,数据要小、频率要低。 第七篇会专门讲怎么优化。
2.3 十分钟速览:和 Vue 的核心差异
Page({
// 1. 数据:必须先声明在 data 里,模板才能用
data: { count: 0, list: [] },
// 2. 改数据:setData,支持路径写法
onInc() {
this.setData({ count: this.data.count + 1 });
// 深层路径:this.setData({ 'list[0].done': true })
},
// 3. 没有计算属性,官方方案是 WXS 或 setData 时算好
// (新语法支持 computed 需引第三方库,见第四篇)
// 4. 生命周期:onLoad / onShow / onReady / onHide / onUnload
onLoad(options) {
// options 里是页面跳转带来的参数:/pages/detail?id=1 → options.id === '1'
},
onShow() {
// 每次页面显示都会来(包括从下一页返回),刷新数据的常见位置
},
});WXML 指令对照表(会 Vue 的看一眼就够):
| Vue | 小程序 WXML |
|---|---|
v-if |
wx:if |
v-else |
wx:else |
v-for |
wx:for,配合 wx:key |
@click |
bindtap |
:src="url" |
src="{{url}}" |
v-model |
没有,手写 value="{{x}}" bindinput="onInput" |
感悟:小程序没有 v-model 是刻意的——双向绑定在「两线程架构」下是个性能陷阱,官方逼着你显式处理每一次输入。理解这一点,你就理解了小程序 API 设计的整体气质:宁可啰嗦,不可模糊。
2.4 rpx:一个单位解决屏幕适配
小程序做了件 Android 和 Web 都很羡慕的事:定义了 rpx(responsive pixel),屏幕宽度恒为 750rpx。设计稿按 750 宽出图,iPhone 6/7/8 上 1rpx = 0.5px,在更宽的机型上等比放大。样式里写 width: 375rpx 就是「半屏宽」,不用算任何比例。
踩坑故事三:rpx 用在不该用的地方
现象:海报生成页用了 Canvas 手绘,按设计稿把字号写成 28rpx,导出图片在小屏手机上文字糊成一团。
原因:Canvas 2D 接口、以及 wx.createSelectorQuery 拿到的尺寸单位都是物理 px,不是 rpx。rpx 只活在 WXSS 里。
解法:涉及 Canvas / JS 计算尺寸的地方,先换算:
const { pixelRatio, windowWidth } = wx.getWindowInfo();
const rpxToPx = (rpx) => (rpx * windowWidth) / 750;教训:记住 rpx 的管辖范围只有 WXSS。凡是在 JS 里处理尺寸,第一件事就是确认单位。
2.5 踩坑故事四:回调里的 this 指向丢了
现象:wx.request 的 success 回调里 this.setData(...) 报错 Cannot read property 'setData' of undefined。
// ❌ 经典错误:普通函数的 this 不是页面实例
wx.request({
url: 'https://api.example.com/todos',
success: function (res) {
this.setData({ todos: res.data }); // this 是 undefined
},
});
// ✅ 解法一:箭头函数(词法 this),今天的推荐写法
wx.request({
url: 'https://api.example.com/todos',
success: (res) => {
this.setData({ todos: res.data });
},
});教训:这是 JavaScript 的老问题,但它是每个小程序新手的「成人礼」。统一用箭头函数写回调,这个问题就绝迹了。顺手把 var that = this 这种上古写法从代码里清除出去。
第三篇 UI 组件库:别从零画按钮
语法有了,马上写界面。小程序自带一套基础组件(view、text、button、image、scroll-view、swiper……),但它们是「零件」不是「成品」——没有现成的日历、下拉刷新、商品卡片。这一篇解决两个问题:选什么组件库,以及怎么用它写出一个像样的页面。
3.1 主流组件库怎么选
flowchart TD
Q{"做什么样的小程序?"}
Q -->|"电商 / 通用业务<br/>组件要全"| A["vant-weapp (有赞)<br/>生态最大 文档最细"]
Q -->|"要腾讯系设计风格<br/>长期维护"| B["TDesign (腾讯官方)<br/>设计规范完整"]
Q -->|"京东系 / 多端"| C["NutUI (京东)"]
Q -->|"一套代码同时出<br/>小程序 + H5 + App"| D["跨端框架<br/>Taro / uni-app<br/>⚠️ 见 3.4 节的权衡"]
- vant-weapp:有赞出品,事实上的生态老大。组件全(60+),电商场景(商品、优惠券、地址编辑)开箱即用,社区问题几乎都能搜到答案。不知道选什么就选它。
- TDesign:腾讯官方,设计体系完整,维护稳定。和 vant-weapp 二选一主要看设计稿更接近哪个风格。
- NutUI:京东出品,风格轻量,多端版本(Taro)做得好。
- 基础组件之外值得一提的:
wxml-to-canvas(海报生成)、echarts-for-weixin(图表)、computed/mobx-miniprogram(下一篇的主角)。
引入方式(以 vant-weapp 为例):小程序从 2019 年起支持 npm,但和 Web 的 npm 用法不一样,多一步「构建」:
npm i @vant/weapp -S然后在开发者工具里点「工具 → 构建 npm」,再在页面的 json 里声明:
{
"usingComponents": {
"van-button": "@vant/weapp/button/index",
"van-field": "@vant/weapp/field/index"
}
}3.2 踩坑故事五:npm 装了,组件死活找不到
现象:npm install 成功,usingComponents 也写了,模拟器报 Component is not found in path "@vant/weapp/button/index"。
原因:小程序构建工具不直接读 node_modules,需要「工具 → 构建 npm」把它编译成 miniprogram_npm 目录。装完包没构建、或者 app.json 同级目录结构不对(project.config.json 里 miniprogramRoot 配错),都会撞上这个报错。
解法:构建 npm 后重启编译;团队项目把 miniprogram_npm 加入 git 忽略,但要在 README 里写明「拉代码后先构建 npm」——新人入职第一天栽在这上面的比例高得离谱。
3.3 Demo:一个五脏俱全的登录页
把数据绑定、事件、vant-weapp 组件串起来——每个小程序都有的页面:
<!-- pages/login/login.wxml -->
<view class="page">
<van-field
value="{{form.phone}}"
label="手机号"
placeholder="请输入手机号"
error-message="{{errors.phone}}"
bind:change="onPhoneChange"
/>
<van-field
value="{{form.code}}"
label="验证码"
placeholder="请输入验证码"
bind:change="onCodeChange"
>
<van-button slot="button" size="small" type="primary"
disabled="{{countdown > 0}}"
bindtap="onSendCode">
{{countdown > 0 ? countdown + 's 后重发' : '发送验证码'}}
</van-button>
</van-field>
<van-button type="primary" block round
loading="{{loading}}"
bindtap="onLogin">
登录
</van-button>
</view>// pages/login/login.js
Page({
data: {
form: { phone: '', code: '' },
errors: { phone: '' },
countdown: 0,
loading: false,
},
onPhoneChange(e) {
// van-field 的值在 e.detail 里
this.setData({
'form.phone': e.detail,
'errors.phone': '',
});
},
onCodeChange(e) {
this.setData({ 'form.code': e.detail });
},
onSendCode() {
if (!/^1\d{10}$/.test(this.data.form.phone)) {
this.setData({ 'errors.phone': '手机号格式不对' });
return;
}
this.setData({ countdown: 60 });
this.timer = setInterval(() => {
const next = this.data.countdown - 1;
this.setData({ countdown: next });
if (next <= 0) clearInterval(this.timer);
}, 1000);
// api.sendSmsCode(this.data.form.phone)
},
onLogin() {
const { phone, code } = this.data.form;
if (!/^1\d{10}$/.test(phone)) {
this.setData({ 'errors.phone': '手机号格式不对' });
return;
}
this.setData({ loading: true });
// 第五篇会把它换成真正的请求封装
setTimeout(() => {
this.setData({ loading: false });
wx.switchTab({ url: '/pages/home/home' });
}, 800);
},
onUnload() {
clearInterval(this.timer); // ⚠️ 页面销毁要清定时器
},
});设计要点(这些比语法值钱):
- 表单状态装进一个
form对象,更新用 setData 的路径语法('form.phone': value),只传变化的那一小条数据过线程桥——2.2 节的第一性原理在这里第一次兑现。 - 按钮的「正常 / 加载中 / 禁用」形态由状态驱动,UI 本质是状态机,这在 Vue、Android、小程序里是同一句话。
- 定时器在
onUnload里清掉——页面销毁了定时器还在跑,轻则无效 setData 报错刷屏,重则内存泄漏。这是第四篇生命周期的预演。
3.4 原生还是跨端框架?一个绕不开的决策
只要做小程序,早晚有人问你:为什么不用 Taro / uni-app,一套代码出五端?我的建议是按团队和业务的真实情况选,而不是按技术情怀:
flowchart TD
Q{"业务分布在哪?"}
Q -->|"只发微信小程序<br/>且追求极致体验"| A["原生开发<br/>能力最全 坑最少<br/>新 API 第一天就能用"]
Q -->|"微信 + 支付宝/抖音多平台<br/>或同时要 H5"| B["uni-app / Taro<br/>用平台差异最小的子集"]
Q -->|"团队全是 React 技术栈"| C["Taro"]
Q -->|"团队全是 Vue 技术栈"| D["uni-app"]
踩坑故事六:跨端框架里用了「全平台都支持」的组件,抖音端直接白屏
现象:uni-app 项目用了一个官方组件库的级联选择器,微信端、H5 端正常,发布到抖音小程序直接白屏,排查三天发现是该组件内部用了抖音端不支持的一个 API。
感悟:跨端框架解决的是「语法转换」,解决不了「平台能力差异」。微信小程序的能力更新最快(微信自家的新能力当然先给自己),跨端框架追平永远有时间差。选跨端,就要接受「用所有平台能力的最小公约数」这个约束。小团队单平台起步,先原生,等真的要多端了再迁移,往往比第一天就上跨端框架更划算。
第四篇 架构与生命周期:从「写页面」到「写应用」
前三篇你能写出一个页面了。但真实小程序是几十个页面、被用户以你想象不到的顺序跳转、随时被微信切到后台。这一篇建立组织代码的地基。
4.1 生命周期:App、Page、Component 三层
stateDiagram-v2
[*] --> App_onLaunch: 冷启动(整个小程序只此一次)
App_onLaunch --> Page_onLoad: 打开第一个页面
Page_onLoad --> Page_onShow: 页面显示
Page_onShow --> Page_onReady: 首次渲染完成
Page_onReady --> Page_onHide: 跳转下一页 / 切后台
Page_onHide --> Page_onShow: 返回本页 / 回到前台
Page_onShow --> Page_onUnload: 页面被销毁(redirect/navigateBack)
Page_onUnload --> [*]
真正值钱的是这三条规则:
onLoad一生只来一次,onShow每次显示都来:onLoad只在页面创建时执行一次,拿着跳转参数;onShow每次显示都来。「返回时要不要刷新列表」的答案就是把刷新逻辑放onShow,而不是假设onLoad会再来。onReady之前别碰节点:wx.createSelectorQuery()查 DOM 要等首次渲染完成,放onLoad里查高度会拿到 0。- 小程序没有可靠的「退出回调」:用户按胶囊按钮关掉小程序,
onUnload之后 App 可能还活着,也可能随时被系统回收。重要数据不能指望「退出时再保存」,和 Android 的onDestroy不保证调用是同一个道理。
踩坑故事七:编辑页保存返回,列表还是旧数据
现象:从列表页进编辑页,改完保存返回,列表纹丝不动。
原因:返回上一页走的是 navigateBack,上一页的实例没有被销毁,onLoad 不会再执行。
解法,两种主流姿势:
// 解法一:列表页把刷新放 onShow
onShow() {
this.loadList(); // 返回时自然刷新,缺点是首次会重复加载,加 flag 控制
}
// 解法二:编辑页保存后主动通知上一页(事件总线 / getCurrentPages)
const pages = getCurrentPages();
const prevPage = pages[pages.length - 2];
prevPage.onNeedRefresh?.(); // 直接调上一页的方法
wx.navigateBack();4.2 页面栈:小程序导航的「宪法」
小程序的页面不是平铺的,是一个栈:
flowchart LR
subgraph stack["页面栈(最多 10 层)"]
A["首页<br/>栈底"] --> B["列表页"] --> C["详情页"] --> D["编辑页<br/>栈顶"]
end
E["navigateTo<br/>压栈 保持当前页"] -.->|"+1 层"| stack
F["navigateBack<br/>出栈返回"] -.->|"-1 层"| stack
G["redirectTo<br/>替换栈顶"] -.->|"层数不变"| stack
H["reLaunch<br/>清空重开"] -.->|"栈重置"| stack
I["switchTab<br/>跳 tabBar 页"] -.->|"非 tabBar 页全部出栈"| stack
踩坑故事八:跳转到第十一层,按钮点了没反应
现象:一个「猜你喜欢 → 详情 → 相关推荐 → 详情 → …」的商品浏览链路,用户连点十次之后,再点卡片毫无反应,既不跳转也不报错(老基础库静默失败)。
原因:navigateTo 的页面栈上限是 10 层,到了第 11 次压栈直接失败。
解法:自循环的详情链路用 redirectTo(替换栈顶而不是压栈),或者监测栈深度,满了就 reLaunch 回首页。这也是「返回键要按十下才能退出」这种体验事故的根源——栈越深,用户的返回路径越长。
踩坑故事九:navigateTo 跳 tabBar 页,永远不生效
现象:wx.navigateTo({ url: '/pages/home/home' }) 调用了,纹丝不动。
原因:tabBar 页面不在普通页面栈体系里,必须用 wx.switchTab;反过来,非 tabBar 页不能用 switchTab。两类页面的 URL 在 app.json 里就决定了(tabBar.list 里的那几个),API 选错是唯一可能的原因。
教训:这条错不报错、静默失败,是新人调试时间最长的坑之一。记住口诀:tabBar 用 switchTab,栈满用 redirectTo,重启用 reLaunch,其他 navigateTo。
4.3 组件化与通信:从页面到 Component
页面大了要拆组件。自定义组件也是四文件结构,js 里用 Component({...}) 代替 Page({...}):
// components/price-card/index.js
Component({
options: {
styleIsolation: 'isolated', // ⚠️ 见踩坑故事十
},
properties: {
price: { type: Number, value: 0 }, // 父传子
},
data: { /* 组件内部状态 */ },
methods: {
onBuy() {
this.triggerEvent('buy', { price: this.data.price }); // 子传父:派事件
},
},
});<!-- 父页面使用 -->
<price-card price="{{item.price}}" bind:buy="onBuy" />通信方式的全景:
flowchart TD
A["父 → 子<br/>properties"] --- S["组件通信"]
B["子 → 父<br/>triggerEvent 派事件"] --- S
C["任意组件 ↔ 全局<br/>mobx-miniprogram 绑定"] --- S
D["页面间<br/>EventChannel(navigateTo 时建立)<br/>或事件总线"] --- S
踩坑故事十:改组件库样式改不动,自己页面的样式却互相污染
现象:给 van-button 写了个覆盖样式死活不生效;反过来,A 页面给 .title 写的红色,B 页面的 .title 也变红了。
原因:小程序的样式隔离规则和直觉相反:
- 页面之间:样式是全局的!A 页面 WXSS 里的类名会污染 B 页面(同名类)。
- 组件内部:默认有隔离(
isolated),外面的样式进不去,里面的样式出不来——所以改不动组件库。
解法:
- 页面样式一律加页面级命名空间:
.page-order .title { ... },或者干脆多用组件(组件天然隔离)。 - 组件需要接收外部样式时,在
options里调styleIsolation: 'apply-shared'(外能进内不出)或shared(双向),改组件库主题时经常要用到。
4.4 全局状态:mobx-miniprogram 就够了
页面多了之后,「用户信息、购物车数量」这类全局状态在几十个页面间传来传去会迅速失控。小程序生态没有 Vuex/Pinia 那样的官方答案,事实标准是 mobx-miniprogram + mobx-miniprogram-bindings:
// stores/user.js
import { observable, action } from 'mobx-miniprogram';
export const userStore = observable({
userInfo: null,
get isLoggedIn() {
return !!this.userInfo;
},
setUserInfo: action(function (info) {
this.userInfo = info;
}),
});// 页面绑定:store 变了,页面 data 自动同步
import { createStoreBindings } from 'mobx-miniprogram-bindings';
import { userStore } from '../../stores/user';
Page({
onLoad() {
this.storeBindings = createStoreBindings(this, {
store: userStore,
fields: ['userInfo', 'isLoggedIn'],
actions: ['setUserInfo'],
});
},
onUnload() {
this.storeBindings.destroyStoreBindings(); // ⚠️ 记得销毁
},
});思考:这套写法的心智模型和 Vue 的 Pinia + computed 完全一致——「store 是唯一数据源,页面只是绑定」。同时别忘了给 Component 配上官方扩展库 computed 和 watch(mobx-miniprogram 同一作者出品),小程序原生组件没有计算属性,手写派生状态是代码腐烂的起点。
第五篇 网络与存储:数据从哪来,存到哪去
架构定了,现在填数据的肉。这一篇学完,你就能写出「登录 + 联网 CRUD + 本地缓存」的完整小程序了。
5.1 wx.request:先把回调包成 Promise
小程序的联网 API 是回调风格的 wx.request,直接用会很快陷入回调地狱(2.5 节那种嵌套套嵌套)。项目第一天就该做的封装:
// utils/request.js
const BASE_URL = 'https://api.example.com';
function request({ url, method = 'GET', data, needAuth = true }) {
return new Promise((resolve, reject) => {
wx.request({
url: BASE_URL + url,
method,
data,
timeout: 10000,
header: {
// 统一带 token:小程序没有 Cookie 自动管理,登录态自己维护
Authorization: needAuth ? `Bearer ${wx.getStorageSync('token')}` : '',
},
success(res) {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(res.data);
} else if (res.statusCode === 401) {
// token 过期:清登录态,跳登录页
wx.removeStorageSync('token');
wx.reLaunch({ url: '/pages/login/login' });
reject(new Error('登录已过期'));
} else {
reject(new Error(res.data?.message || `请求失败 ${res.statusCode}`));
}
},
fail(err) {
// 断网、超时、域名不在白名单都走这里
reject(new Error('网络异常,请稍后重试'));
},
});
});
}
// 使用:async/await,和前端写 axios 一个手感
async onLoad() {
try {
const todos = await request({ url: '/todos' });
this.setData({ todos });
} catch (e) {
wx.showToast({ title: e.message, icon: 'none' });
}
}设计要点:拦截逻辑(带 token、401 处理、错误归一化)写在这一层,业务代码就只剩一行 await——这和 Android 的 OkHttp 拦截器、前端的 axios 拦截器是同一种分层思想。
踩坑故事十一:401 了,但页面还停在原地
现象:token 过期后接口返回 401,封装的 request 里做了 reLaunch 跳登录页,但用户反馈「偶尔跳不过去」。
原因:wx.reLaunch 跳转登录页时,如果当前页面栈里有 tabBar 页,reLaunch 的目标 URL 带了 query 参数在部分基础库上被丢弃;更隐蔽的是——401 时多个并发请求会触发多次 reLaunch,第二次 reLaunch 打断了第一次。
解法:401 处理加防抖锁:
let isRedirectingToLogin = false;
function handleUnauthorized() {
if (isRedirectingToLogin) return;
isRedirectingToLogin = true;
wx.removeStorageSync('token');
wx.reLaunch({
url: '/pages/login/login',
complete: () => { isRedirectingToLogin = false; },
});
}移动端特有的提醒(小程序同理):微信用户会进电梯、切 Wi-Fi 和流量,弱网和断线是常态而不是异常。timeout 必须配、失败要给重试入口、wx.showToast 报错别弹全屏错误页——静默重试一次,体验天差地别。
5.2 本地存储:wx.setStorage 的分寸
小程序的存储比 Android 简单得多,没有 Room 没有 DataStore,就一个 wx.setStorage / wx.getStorage(异步)和 wx.setStorageSync / wx.getStorageSync(同步):
flowchart TD
Q{"要存什么?"}
Q -->|"token / 用户设置<br/>小键值对"| A["wx.setStorageSync<br/>同步 简单"]
Q -->|"列表缓存 / 草稿<br/>几 KB 以上的数据"| B["wx.setStorage (异步版)<br/>⚠️ 同步版大数据会卡 UI"]
Q -->|"要跟随用户换设备<br/>或免后端"| C["微信云开发<br/>云数据库 + 云函数"]
Q -->|"敏感数据(密码之类)"| D["❌ 别存本地<br/>存 token 不存密码"]
两个数字要记住:单个 key 上限 1MB,总上限 10MB。超出直接写入失败。
踩坑故事十二:storage 同步读写把首页卡死了
现象:首页加载越来越慢,Profiling 发现 getStorageSync 一个调用占了 200ms。
原因:运营活动的弹窗配置、AB 实验分组、用户信息……十几个模块都往 storage 里塞,有个模块把整份商品列表(几百 KB)也存了进去,首页启动时同步读,主线程原地罚站。
解法:
- 大对象读写一律用异步版(
wx.getStorage)。 - 启动时只读「渲染首屏必需」的字段,其余懒加载。
- token 这种小且高频的用同步版没问题——按数据大小和频率选 API,不是按顺手程度。
5.3 云开发:没有后端时的另一条路
如果你是个人开发者、没有自己的服务器,微信云开发(CloudBase)值得单独了解:云数据库(类 MongoDB)、云函数(Node.js)、云存储、云调用(直接调微信 API 免鉴权),在开发者工具里一站式完成,不用买服务器、不用配域名、不用过 ICP 备案——对,云开发的请求不受 1.3 节域名白名单限制,这对个人开发者是巨大优势:
// 免鉴权、免域名配置,一行调用
const db = wx.cloud.database();
const res = await db.collection('todos').where({ done: false }).get();代价是:数据模型简单、复杂业务(多表事务、复杂查询)不如自建后端灵活,迁移到自有后端的成本不低。工具类、内容类、Demo 项目用云开发;交易核心、复杂业务自建后端。
第六篇 平台能力:登录、订阅消息、分享、支付与分包
到这里,你的小程序已经「自成一体」了。这一篇让它和微信这个平台打交道。平台能力的坑有一个共同特征:文档看着简单,但 API 的生命周期变迁极快,照着两年前的教程写必踩坑。
6.1 登录:code 换 openid 的三段握手
小程序登录是平台能力里设计最优雅的一个,也是面试必考题:
sequenceDiagram
participant MP as 小程序前端
participant WX as 微信服务器
participant S as 你的后端
MP->>WX: wx.login() 拿 code(5分钟有效)
MP->>S: 把 code 发给自己的后端
S->>WX: code + appid + secret 换<br/>openid + session_key
WX-->>S: openid(用户唯一标识) + session_key
S-->>MP: 签发自家 token(如 JWT)
Note over MP: 之后所有请求带 token<br/>session_key 永远不出后端
三条铁律:
session_key绝不能下发到前端——它能解密用户敏感数据,泄漏等于把用户数据大门敞开。- code 只能用一次且 5 分钟过期,换取动作必须在后端做(secret 也不能进前端代码)。
- 前端维护的是自家 token,不是微信的 code。5.1 节 request 封装里带的
Authorization就是它。
踩坑故事十三:跟着老教程写 getUserProfile,提交审核被拒
现象:照着 2021 年的教程用 wx.getUserProfile 拿用户头像昵称,真机一调返回 getUserProfile:fail can only be invoked by user TAP gesture 或直接被拒审。
原因:微信在 2022 年回收了 getUserProfile 能力。现在获取头像昵称的官方姿势是两个表单组件:
<!-- 头像:button open-type="chooseAvatar" -->
<button open-type="chooseAvatar" bind:chooseavatar="onChooseAvatar">
<image src="{{avatarUrl || '/images/default-avatar.png'}}" />
</button>
<!-- 昵称:input type="nickname",拉起微信输入法半屏 -->
<input type="nickname" placeholder="请输入昵称" bind:blur="onNicknameBlur" />教训:这是小程序开发最重要的生存技能——微信 API 的废弃和变更极快,任何「照着教程写」的代码都要去官方文档核对一遍最新版本。关注「微信小程序开发者文档」的更新日志,比关注任何公众号都有用。
6.2 订阅消息:推送的正确打开方式
小程序没有 Android 那种「随便推」的通知系统,触达用户的正道是订阅消息:
flowchart LR
A["用户在小程序里<br/>主动点击按钮"] --> B["wx.requestSubscribeMessage<br/>弹出订阅授权框"]
B --> C{"用户怎么选?"}
C -->|"允许"| D["后端在服务通知里<br/>下发一次消息"]
C -->|"拒绝"| E["⚠️ 同一个模板<br/>短期内不能再弹"]
C -->|"勾选『总是保持以上选择』"| F["以后静默按上次选择执行"]
关键认知:订阅消息默认是一次性的——用户点一次允许,你只能推一次。长期订阅仅对政务、医疗等特定类目开放。所以正确的产品设计是「在最关键的动作上请求订阅」(下单、预约、打卡),而不是见缝插针地弹。
踩坑故事十四:订阅消息发出去了,用户没收到
按出现概率排查:
- 模板 ID 不对:模板在 mp 后台「订阅消息」里申请,
tmplIds写错或用了测试模板,发送直接报错。 - 关键词格式不匹配:模板里的
{{thing1.DATA}}(字符串)、{{amount2.DATA}}(金额)每种类型有严格格式限制,金额必须是数字、字符数超限都会发送失败——失败原因要去查发送接口的返回码,客户端无感知。 - 用户没订阅 / 订阅次数用完了:一次订阅只够推一次,推完额度清零。
- 跳转页面未发布:消息配置的
page路径必须是已发布版本里存在的页面,配了还在开发中的页面,用户点消息会跳到错误页。
6.3 分享与裂变:小程序的增长引擎
小程序生于社交,分享能力是它的原生优势:
// 页面级分享:定义了才有「转发」入口的内容
Page({
onShareAppMessage() {
return {
title: '这个记账小程序太好用了',
path: '/pages/home/home?from=share&uid=' + this.data.uid, // ⚠️ 路径带参做裂变归因
imageUrl: '/images/share-cover.png', // 5:4 比例,自定义封面
};
},
});应知应会的分寸:
- 分享无法拿到「是否分享成功」的回调了(早年有,已回收)——「分享成功就送优惠券」这种强制裂变做不了,这也是微信的刻意设计。
- 朋友圈:小程序不能直接分享到朋友圈(除了特定的「分享到朋友圈」Beta 能力且仅 Android),正道是生成海报图让用户自己发——3.1 节的
wxml-to-canvas就是干这个的。 - 裂变归因全靠 path 参数:谁分享的、从哪进来的,都编码在
onShareAppMessage的 path 里,落地页在onLoad(options)里解析。这套机制就是小程序增长黑客的全部基础。
6.4 支付:五步流程,前后端各管一半
微信支付是平台能力里链条最长的,画出来再写代码:
sequenceDiagram
participant MP as 小程序
participant S as 你的后端
participant WX as 微信支付
MP->>S: 1. 创建订单(商品 金额)
S->>WX: 2. 统一下单(用商户号密钥签名)
WX-->>S: prepay_id
S-->>MP: 3. 返回支付参数(二次签名)
MP->>WX: 4. wx.requestPayment 拉起收银台
WX-->>MP: 用户输密码完成
WX-->>S: 5. ⚠️ 支付结果异步回调(这才是到账依据)
踩坑故事十五:以前端回调为准发货,被刷单薅了羊毛
现象:wx.requestPayment 的 success 回调里直接通知后端「支付成功,发货」,上线一个月被恶意用户用篡改的客户端刷出大量未付款订单。
原因:前端的一切回调都不可信。唯一可信的到账凭证是微信支付服务器推送给你后端的异步回调(pay 通知),验签通过才算数。
教训:这条规则在 Android 内购、Stripe 支付里一模一样——客户端回调只负责 UI 表现,服务端回调才负责业务状态。凡是涉及钱的状态变更,永远以服务端到服务端的通知为准。
另外记住 1.2 节的连锁反应:虚拟商品(课程、会员)在 iOS 端不能走微信支付(苹果规则),要么引导安卓/H5 支付,要么走小程序虚拟支付通道并接受抽成——类目和主体在注册那天就决定了你有没有资格碰这块。
6.5 分包加载:2MB 限制的解法
小程序主包上限 2MB(总包 20MB),所有页面塞一个包很快就会撞线。答案是分包:
// app.json
{
"pages": ["pages/home/home", "pages/login/login"],
"subPackages": [
{
"root": "packageOrder",
"pages": ["pages/list/list", "pages/detail/detail"]
},
{
"root": "packageUser",
"pages": ["pages/profile/profile", "pages/setting/setting"]
}
],
"preloadRule": {
"pages/home/home": {
"network": "wifi",
"packages": ["packageUser"]
}
}
}flowchart TD
A["主包 ≤ 2MB<br/>首页 + tabBar 页 + 公共代码"] -->|"用户点进订单模块"| B["订单分包<br/>按需下载"]
A -->|"Wi-Fi 下预加载"| C["个人中心分包"]
style A fill:#e8f4e8
设计要点:首页和 tabBar 页必须在主包(启动就加载);低频功能(设置、关于、客服)果断拆分包;preloadRule 让「首页加载完、Wi-Fi 下静默下载个人中心包」,用户点过去秒开。
第七篇 质量与发布:从「能跑」到「敢上线」
功能做完只是上半场。小程序的发布比 Android 多了一道「平台审核」,这是它最独特的一关。
7.1 性能三板斧:setData、分包、图片
小程序性能问题的 90% 集中在这三件事上:
第一板斧:setData 瘦身(2.2 节第一性原理的兑现)
flowchart TD
A["页面卡顿"] --> B{"setData 传的什么?"}
B -->|"整个大列表"| C["改成路径更新<br/>'list[3].done': true"]
B -->|"高频调用<br/>onPageScroll / 倒计时"| D["合并 + 节流<br/>攒一批一次发"]
B -->|"传了视图用不到的字段"| E["data 里只放渲染需要的数据<br/>其余存 this 普通属性"]
// ❌ 每秒 setData 整个对象
this.setData({ form: this.data.form });
// ✅ 只传变化的字段
this.setData({ 'form.phone': value });
// ✅ 不参与渲染的数据根本别进 data
Page({
data: { list: [] },
onLoad() {
this._rawResponse = null; // 内部缓存,挂 this 上不挂 data
},
});第二板斧:分包——6.5 节已经讲过,启动速度的第一优化项。
第三板斧:图片——列表图一律压缩到展示尺寸的 2 倍以内、用 image 组件的 lazy-load、CDN 域名配进 downloadFile 白名单。首屏图太多时,骨架屏比 loading 转圈的感知快一倍。
踩坑故事十六:倒计时把列表页卡成 PPT
现象:团购列表每个卡片都有「距结束 xx:xx」倒计时,页面滑动掉帧严重。
原因:20 个卡片各自开了 setInterval,每秒 20 次 setData 跨线程传输——桥堵了。
解法:一个全局定时器,每秒算一次所有卡片的剩余时间,合并成一次 setData:
// 一个 timer 驱动全部卡片
this.timer = setInterval(() => {
const updates = {};
this.data.list.forEach((item, i) => {
updates[`list[${i}].remainText`] = formatRemain(item.endTime);
});
this.setData(updates); // 每秒只有这一次过桥
}, 1000);7.2 调试三板斧:vConsole、真机调试与体验版
- 开发者工具:模拟器 + Wxml 面板(看最终渲染树)+ AppData 面板(实时看每个页面的 data,排查 setData 问题的第一现场)。
- 真机调试:开发版小程序右上角胶囊菜单里打开「开发调试」,vConsole 会浮在页面上——手机上看到 console.log、网络请求、storage 内容。真机和模拟器的差异比你想的大:字体渲染、安全区、性能、个别 API 行为都不一样,发布前必须真机过一遍主流程。
- 体验版:上传代码后在 mp 后台设为体验版,添加体验成员(微信号),扫码即用。它是「发给产品和测试同学」的标准姿势,比 Android 装 APK 优雅得多。
踩坑故事十七:体验版打不开,群里一片「白屏了」
现象:上传体验版后,自己手机正常,测试同学扫码一直白屏。
排查路径:
- 对方的微信号加进「体验成员」了吗?
- 你上传后设为体验版了吗?(上传 ≠ 自动成为体验版,要在版本管理里手动选)
- 对方是不是打开了「调试模式」导致走了别的版本?(体验版打开调试后会加载开发版代码,版本就乱了)
教训:小程序有「开发版 / 体验版 / 线上版」三个版本共存,版本切换的开关藏在各处。发体验版给别人之前,用一个不在体验成员名单里的微信号扫一次——模拟用户的真实路径,这一分钟能省一小时的群聊排查。
7.3 审核与发布:小程序独有的最后一关
flowchart LR
A["开发者工具<br/>上传代码"] --> B["mp 后台<br/>提交审核"]
B --> C{"审核结果<br/>几小时到几天"}
C -->|"通过"| D["手动点『发布』<br/>全量上线"]
C -->|"拒绝"| E["按驳回原因改<br/>重新排队"]
D --> F["灰度发布<br/>按比例放量 可回退"]
提高一次过审率的实战清单:
- 类目和内容对齐(1.2 节):电商功能挂企业类目,个人号别碰交易。
- 给用户「不登录也能逛」的路径:审核员会拒绝「打开就强制授权手机号」的小程序。登录应该发生在「需要身份的那个动作」上(下单、收藏),而不是启动时。
- 测试账号:审核表单里提供测试账号密码或免密通道,审核员进不去你的核心流程必拒。
- 敏感功能自审:UGC 内容要有举报/审核机制说明;涉及医疗、金融的回答要有资质上传。
感悟:审核这道关常被骂,但它塑造了小程序生态和 App 生态的根本不同——小程序是「寄居」在微信里的,平台既是分发渠道也是监管者。接受这个设定,把审核标准当作产品设计的一部分(登录时机、隐私协议、类目选择),而不是发布前的拦路虎,你的过审率会高得多。
第八篇 AI 时代:怎么少写一半代码把小程序做出来
前七篇是「内功」,这一篇是「兵器」。我的结论和 Android 篇一致:AI 没有让小程序开发变简单,它让「懂行的人」快了 3-5 倍,让「不懂行的人」更快地写出能跑但烂尾的代码。区别在于你怎么用它。
8.1 AI 在小程序开发各环节的胜任度
flowchart LR
subgraph good["AI 做得极好 放心交出去"]
A1["单页面骨架<br/>wxml + wxss + Page({...})"]
A2["表单页 / 列表页<br/>配 vant-weapp 组件"]
A3["request 封装 / 工具函数"]
A4["单元测试与类型定义"]
end
subgraph mid["AI 能做 但你要盯着"]
B1["跨页面状态设计"]
B2["性能优化(setData/分包)"]
B3["组件库主题定制"]
end
subgraph bad["AI 做不好 还得靠人"]
C1["类目/资质/审核问题<br/>(政策天天变)"]
C2["真机才出现的 bug<br/>安全区 键盘 滚动"]
C3["微信 API 的最新变更<br/>(训练数据滞后)"]
end
第三格要特别说:微信 API 的变更速度(6.1 节 getUserProfile 的故事)恰好打在 AI 的软肋上——训练数据里全是旧 API 的示例。AI 给你写 wx.getUserInfo 时你要能一眼认出「这是 2021 年的写法」。
8.2 实战工作流:需求 → 小程序的 AI 协作五步法
第一步:先写「项目宪法」,再让 AI 写代码。 在项目里放一个 AGENTS.md:
# 项目约定
- 原生小程序(不用跨端框架),组件库 @vant/weapp,less 写样式
- 页面四文件结构:wxml/wxss/js/json,页面样式加命名空间防污染
- 全局状态用 mobx-miniprogram + mobx-miniprogram-bindings,组件配 computed 扩展
- 网络统一走 utils/request.js 封装,async/await,业务代码禁止裸 wx.request
- setData 只传变化字段的路径写法;不参与渲染的数据挂 this 普通属性
- 跳转口诀:tabBar 用 switchTab,自循环链路用 redirectTo,其他 navigateTo
- 回调一律箭头函数;定时器/绑定在 onUnload 里清理
- 登录态:wx.login 拿 code 给后端换 token,前端只存 token,禁存 session_key注意这份清单里的每一条,都对应着前面某一篇的某个坑——项目宪法本质上是踩坑笔记的浓缩。
第二步:页面级生成,而不是小程序级生成。 不要对 AI 说「给我写一个记账小程序」,要说「按项目约定写一个记账首页:顶部本月支出汇总卡片,下面是账单列表,用 van-cell,下拉刷新,每条显示分类图标、备注、金额」。一次一个页面,AI 的输出质量和你的可控性都最高。
第三步:让它先写数据结构和状态,再写 UI。 先生成页面的 data 结构、接口契约、状态流转,review 通过后再写 wxml——这正是第四、五篇的架构顺序。状态设计错了,UI 写得再漂亮也是白写。
第四步:报错直接喂回去,但分清两类问题。 模拟器里的 JS 报错、组件找不到这类代码级问题,贴给 AI 循环两三次基本能解;但 1.2 节那种账号类目问题、6.1 节那种 API 已废弃问题、真机上才出现的白屏——AI 帮不了,它没有你的 mp 后台权限,也没有真机。这就是为什么 AI 篇必须排在最后:它是放大器,放大的是你前七篇的水平。
第五步:人工 review 的五个必查点。 AI 生成的小程序代码,我每次都只细查五处:① setData 是不是整个大对象往里塞(性能重灾区);② 定时器和 store 绑定有没有在 onUnload 里清理(泄漏来源);③ 跳转 API 用对没有(tabBar 页 navigateTo 的静默失败 AI 经常犯);④ 请求有没有走统一封装、token 处理对不对;⑤ 用的微信 API 是不是已经废弃的版本。其他代码粗看即可——review 的精力要花在 AI 的已知短板上。
8.3 一个真实对比:Demo 小程序的工时变化
第三篇那个登录页 + 一个待办列表页的小程序 Demo,三年前我要写一整天(查 API、调样式、处理状态)。2026 年用 AI 的实际耗时:
pie title 一个双页面 Demo 的 2 小时去哪了
"写 AGENTS.md 和提示词" : 20
"AI 生成 + 我 review 修改" : 55
"AppID/类目/域名配置(AI帮不上)" : 25
"真机测试与细节打磨" : 20
注意那张饼图里最大的启示:省下来的时间没有消失,它转移到了「描述需求」和「审查代码」上,还有一部分转移到了 AI 永远碰不到的「平台事务」上——注册、类目、域名、审核。AI 时代的核心能力从「打字速度」变成了三样:
- 把模糊需求拆成精确技术任务的能力——你对第四篇架构的理解越深,给 AI 的指令就越准。
- 快速判断代码对错的能力——看不懂 setData 成本的人,review 不了 AI 写的小程序代码。
- 搞定平台事务的能力——类目、资质、审核、API 变更,这些「和微信打交道」的事,一件都外包不出去。
8.4 思考:AI 是放大器,不是替代品
小程序生态和 AI 有一个有趣的共同点:它们都在「消灭记忆型知识的价值」。小程序当年消灭了「会写 Objective-C/Java 才能做移动端」的门槛;AI 今天消灭了「背得出 API 参数才配写代码」的门槛。留下来的价值是同一种东西——判断型知识:
- 这个状态该放
data还是挂this上? - 这个跳转该用
redirectTo还是navigateTo? - 这个需求个人主体能不能做,会不会死在审核?
- AI 写的这段代码用的 API,是不是去年就被回收了?
所以这篇文章前七篇的内容,在 AI 时代不但没有过时,反而更重要了——它们是你在 AI 写完代码后,能说「对」或者「不对」的全部依据。
结语:学习路线收个尾
flowchart LR
A["第 1 周<br/>注册 AppID + 跑通模板<br/>+ 四件套语法"] --> B["第 2-3 周<br/>vant-weapp 组件库<br/>写 5 个静态页面"]
B --> C["第 1-2 个月<br/>生命周期 + request 封装<br/>+ 登录流程<br/>写一个联网 CRUD 小程序"]
C --> D["第 2-4 个月<br/>订阅消息/分享/支付/分包<br/>体验版 审核 发布上线"]
D --> E["持续<br/>把 AI 变成<br/>你的初级工程师"]
三条反直觉的建议作为结尾:
- 第一周死磕账号不丢人。类目、主体、域名、备案,这些「和微信打交道」的事才是真正的门槛,写代码反而是最简单的部分。这不是你笨,是这个生态的真实成本。
- 第一天就用真机预览。模拟器掩盖了安全区、键盘、性能和版本差异问题——你的用户分布在几千种机型的微信里,而小程序的真机预览只要扫个码,这是整个客户端开发里成本最低的真机测试,没有借口不用。
- 让 AI 写代码,让自己写笔记。每踩一个坑记下「现象、根因、下次怎么一眼认出」,这些笔记是 AI 时代你不可被替代的资产——因为 AI 知道所有答案,但它不知道微信上个月又改了哪个 API,以及你的审核上次为什么被拒。
祝你的 setData 永远轻盈,审核永远一次过,小程序在 iPhone SE 和折叠屏上都一样流畅。