写过几年小程序,被各种 API 坑过无数回之后,我最大的感受是:微信小程序API 这套东西,官方文档其实写得很全,但问题在于它太“平”了——几百个接口平铺在那里,每一条都是“接口名 + 参数 + 示例”,你照着抄依然会踩出一堆莫名其妙的错。这篇教程我不想把文档搬一遍,而是按照一条真实开发链路来走:从注册账号、看懂项目结构,到发起第一次网络请求,再到登录换 token、上传图片、发订阅消息、拉起支付,最后把高频报错一个个拆开讲清楚。适合两类人看:一是刚入门、想系统过一遍小程序 API 的开发者,二是被某个报错卡住、想快速找到排查思路的人。看完你至少能少走两三个月的弯路。
1. 先把“小程序API”这个概念讲清楚
1.1 API不是函数名,是一套“能力交换协议”
很多初学者的误区是:把 API 当成一本函数字典,准备背下来。实际上 API 的本质是“能力交换协议”。你调用wx.request,并不是调用一个普通的 JS 函数——你真正做的是把参数打包交给微信客户端,由微信客户端完成域名校验、DNS 解析、HTTPS 握手、网络请求、响应解析,最后再通过success回调把结果交回你的业务代码。
这个理解非常重要,因为它决定了你排查问题的方向。比如wx.request报了url not in domain list,很多人第一反应是“代码写错了”,但实际上你的 JS 代码根本没机会执行——请求在微信客户端那一层就被拦下来了。API 是一层封装,微信把系统能力封装好给你用,那么系统层的行为规则(域名白名单、HTTPS、端口限制)也必须遵守,你在浏览器里那套“随便请求”的直觉在这里不通用。
我用一个酒店前台的类比:你想订餐、洗衣、叫车,不用自己跑到厨房、洗衣房和车库里操作,对前台说需求,前台帮你办好再告诉结果。小程序 API 就是这个前台。你只需要学会“怎么对前台说话”,但不用自己造厨房。
1.2 小程序API和网页API的三处本质差异
第一,运行环境不同。网页的 fetch/ajax 跑在浏览器里,浏览器本身就是一个开放的网络客户端;小程序 API 跑在微信客户端提供的运行时环境里,所有网络请求都要经过微信的校验和代理,所以它有更严格的域名、协议、端口限制。
第二,能力范围不同。小程序 API 里有一大半是“能力型”接口:获取用户位置、读取剪贴板、调用摄像头、播放音频、拉起支付、振动手机、订阅消息。这些能力在普通网页里默认是不存在的,小程序通过wx.*这一层把它们暴露给开发者。
第三,鉴权模型不同。网页登录靠的是 Session/Cookie,小程序靠的是wx.login拿 code,再用 code 换 session_key,最后换成你自己后端的 token。很多新手会把网页那套登录思路直接搬过来,结果越写越别扭,后面第 4 节我专门讲这条链路。
1.3 你实际会碰到的三类API
第一类是微信内置 API(wx.开头的接口),登录、请求、存储、支付、订阅消息都在这层,这也是这篇教程的主角。
第二类是你自己的后端业务 API,小程序通过wx.request访问,数据格式、鉴权逻辑都由你自己定。
第三类是第三方 API,比如地图、AI 模型、内容安全识别。这类接口有个共同特点:通常不能从小程序前端直接调,要么需要你自己的后端转发,要么需要先申请密钥再做服务端调用。很多“为什么我照着文档调 AI 接口却报 400/401”的问题,本质上是把第三类接口的调用方式搞混了。
2. 开工前的三件事:注册、工具链、AppID
2.1 注册小程序账号,拿到这把“身份证”
开发小程序第一步不是写代码,而是去微信公众平台注册一个小程序账号。注册完成后,在“开发管理-开发设置”里能看到 AppID(小程序唯一标识)和 AppSecret(小程序密钥)。
AppID 是你调用所有微信 API 的“身份证”,wx.login、wx.requestPayment、订阅消息全都绕不开它。AppSecret 则要牢记一条铁律:AppSecret 只能保存在你自己的后端服务器上,绝对不能出现在小程序前端代码里。为什么?因为小程序前端代码打包后是可以被反编译的,任何人拿到 AppSecret,就可以冒充你的小程序后端去调用微信的开放接口,后果极其严重。
2.2 开发者工具:你的主战场
下载微信开发者工具,用小程序账号扫码登录。新建项目时填 AppID,不要用“测试号”模式。测试号虽然方便,但它没有合法的 AppID,很多 API(支付、订阅消息、getUserProfile)根本跑不通,到时候你会分不清是代码问题还是权限问题。
开发者工具里最常被忽略的是右上角的“详情-本地设置”,里面有“不校验合法域名”的开关。开发阶段可以勾上,这样本地调试不用配置域名白名单也能发请求,但上线前千万别勾着,不然后台会直接拦截请求。
另外我强烈建议在真机上调试。开发者工具里模拟器用的 PC 的网络环境和系统 API,和真实手机差距不小。特别是定位、摄像头、蓝牙这类硬件 API,模拟器只能模拟个壳,真机上才会暴露真正的问题。涉及基础库版本的地方也要注意,每个基础库版本对应一批 API 的新增和废弃,建议在“详情-基本信息”里确认最低基础库版本,别选得太新,否则大量用户手机上的微信没法运行。
2.3 项目目录里和API最相关的三个文件
app.js:全局逻辑入口,App()函数里可以放全局数据,很多项目会把登录初始化放在这里的onLaunch生命周期里执行。app.json:全局配置,页面路由、窗口样式、网络超时时间、requiredPrivateInfos都要在这里声明。- 每个页面的
.js文件:页面逻辑,所有wx.*调用基本都发生在页面或公共模块里。
有一个配置我要单独提醒:如果你使用了位置类 API,除了要在app.json里配置permission字段,从某个基础库版本开始还需要在requiredPrivateInfos里声明具体用哪个位置接口,比如getLocation、chooseLocation。漏掉这个声明,API 会返回permission denied。这就是典型的文档平铺、实际踩坑才会知道的知识点。
3. 流量入口里的第一课:wx.request 网络请求
3.1 wx.request 基础用法
wx.request是使用频率最高的 API,没有之一。基础用法长这样:
wx.request({ url: 'https://api.example.com/user/info', method: 'GET', data: { id: 123 }, header: { 'content-type': 'application/json', 'Authorization': 'Bearer ' + wx.getStorageSync('token') }, timeout: 10000, success(res) { console.log('状态码', res.statusCode) console.log('响应数据', res.data) }, fail(err) { console.error('请求失败', err) } })几个容易忽略的参数:timeout如果不设置,默认是 60 秒,真实业务里这个时间太长,我的习惯是统一设为 10 秒;dataType默认是json,如果你的接口返回的是纯文本或 HTML,要改成text或者手动处理;method支持 GET/POST/PUT/DELETE 等常见方法,但注意小程序对部分 HTTP 方法的使用跟服务端配置有关,比如 PUT 和 DELETE 在有些服务端框架里需要额外处理跨域预检。
3.2 域名白名单:新手最容易撞的墙
wx.request请求的 URL 必须满足三个条件:
- 必须使用 HTTPS 协议;
- 域名必须在小程序后台“开发管理-开发设置-服务器域名”里配置过;
- 域名不能是 IP 地址,不能带端口号(少数情况除外)。
我第一次做小程序时,后端开发图省事给了一个http://192.168.1.100:8080的接口,我在模拟器里关了“不校验合法域名”后调通了,结果一上真机就失败。后来才明白:真机上微信客户端会强制校验域名白名单,开发工具里的开关只在模拟器生效。解决方法是后端尽快上 HTTPS,然后把正式域名(注意是https://协议)加到白名单里。小程序后台可以配置最多 20 个 request 合法域名,足够了。
如果你有多个环境(测试、预发、正式),我的经验是用环境变量控制BASE_URL,而不是在代码里写死。
3.3 封装一个带鉴权、带超时的request函数
裸写wx.request在业务里会非常痛苦,每个页面都要重复写 header、处理 token 过期。我建议在项目一开始就做一个简单的 Promise 封装:
// utils/request.js const BASE_URL = 'https://api.example.com' function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { const token = wx.getStorageSync('token') wx.request({ url: BASE_URL + path, method, data, timeout: 10000, header: { 'content-type': 'application/json', 'Authorization': token ? `Bearer ${token}` : '' }, success(res) { if (res.statusCode >= 200 && res.statusCode < 300) { resolve(res.data) } else if (res.statusCode === 401) { // token 失效,统一跳转重登录 handleTokenExpired() reject(new Error('unauthorized')) } else { reject(new Error(`server error: ${res.statusCode}`)) } }, fail(err) { reject(err) } }) }) } module.exports = { request }封装的好处是:全项目统一处理鉴权 header、统一错误码、统一超时时间。等到业务复杂了,你还可以在拦截器里加统一的埋点日志,后端只要看一个字段格式,排查问题效率高很多。这个封装模式几乎适合所有小程序项目,我自己的每个项目都是从这个文件开始的。
4. 登录换 token:code2Session 完整链路
4.1 为什么不能用 wx.getUserInfo 代替登录
很多年前微信确实可以靠wx.getUserInfo直接拿到用户资料,但现在getUserInfo已经拿不到真实的头像和昵称了,返回的都是默认值。而且就算拿到了头像昵称,也不能证明“这个人是谁”——头像昵称是用户自己填的,不是验明正身的凭证。
真正能确认用户身份的是 OpenID,它是微信用户在某个小程序下的唯一标识。获取 OpenID 的官方流程就是wx.login + code2Session,这也是登录链路的核心。
4.2 完整流程拆解
流程分两步:第一步,小程序前端调用wx.login获取一个临时凭证 code;第二步,把 code 发给自己的后端,后端拿 code 去微信的jscode2session接口换 OpenID 和 session_key。整个过程大致是这样的:
wx.login({ success(res) { if (!res.code) { wx.showToast({ title: '登录失败', icon: 'none' }) return } // 把 code 交给自己的后端 wx.request({ url: 'https://api.example.com/auth/login', method: 'POST', data: { code: res.code }, success(res) { const { token } = res.data wx.setStorageSync('token', token) } }) } })后端收到 code 后,用 AppID 和 AppSecret 请求微信接口:
// Node.js 后端示例 const axios = require('axios') async function code2Session (code) { const { data } = await axios.get('https://api.weixin.qq.com/sns/jscode2session', { params: { appid: YOUR_APPID, secret: YOUR_APPSECRET, js_code: code, grant_type: 'authorization_code' } }) // data.openid 是用户唯一标识 // data.session_key 是会话密钥,用于解密手机号等敏感信息 return data }拿到openid后,后端去数据库查这个用户是否存在,如果不存在就创建一条新记录,然后生成你自己的业务 token 返回给前端。token 的有效期、刷新策略都是你后端自己控制的,和微信没有直接关系。
4.3 token 存哪、过期了怎么办
前端把 token 用wx.setStorageSync('token', token)存到本地缓存里。wx.setStorageSync这个 API 是同步版的存储接口,数据会持久化到本地,小程序杀掉重开之后还在。
但要注意:不要把 openid、session_key 这类敏感信息直接存到本地缓存。openid 相当于用户的身份证号,理论上有了 openid 就能冒充这个用户向后端发起请求。所以前端只需要存“业务 token”,用 token 去请求业务接口,让后端根据 token 解析出用户身份。
token 过期是另一个常见坑。业务 token 一般都有有效期(比如 2 小时、7 天),过期后请求会返回 401。我习惯的做法是在封装 request 的拦截器里统一处理:遇到 401 就清掉本地 token,然后跳转到登录页重新执行wx.login。这种体验虽然简单粗暴,但在大部分小程序里是够用的。
如果要更顺滑,可以做成“静默刷新”:后端签发 token 时同时给一个 refresh_token,前端发现 token 过期时用 refresh_token 换新 token,换完接着执行原请求。这套逻辑在小程序里完全可行,实现前要想好 refresh_token 本身过期的兜底方案,不然会形成死循环。
5. 高频业务API的实战姿势
5.1 本地缓存:setStorageSync 别乱放敏感数据
小程序本地存储有三个常用接口:wx.setStorageSync、wx.getStorageSync、wx.removeStorageSync,它们都是同步版,业务代码里直接用很方便。存储上限是 10MB,对大部分业务足够。
缓存字段命名我建议统一加前缀,比如user_info、cart_list,方便排查和清理。另外所有缓存本质上都是明文存储,密码、密钥、身份证号这类敏感信息不要放进去。如果确实需要缓存一些隐私字段,至少要在后端做一层加密,前端只保存加密后的密文。
还有一个细节:小程序的缓存是跟着用户微信账号走的,同一个微信用户在同一个手机上换账号登录,A 账号写入的缓存,B 账号也可能读到。所以如果业务涉及多账号切换,登录、登出时一定要把相关缓存清干净,别用“先读缓存,没有再拉接口”的逻辑,否则会串号。
5.2 图片选择与上传:chooseMedia + uploadFile 配合
图片上传是几乎所有内容型小程序都绕不开的功能。现在的推荐做法是用wx.chooseMedia选择图片,然后用wx.uploadFile上传:
wx.chooseMedia({ count: 1, mediaType: ['image'], sourceType: ['album', 'camera'], sizeType: ['compressed'], success(res) { const tempFilePath = res.tempFiles[0].tempFilePath wx.uploadFile({ url: 'https://api.example.com/upload', filePath: tempFilePath, name: 'file', header: { 'Authorization': `Bearer ${wx.getStorageSync('token')}` }, success(uploadRes) { // 注意:uploadFile 返回的 data 是字符串,需要 JSON.parse const data = JSON.parse(uploadRes.data) console.log(data) } }) } })两个最容易踩的坑:一是wx.uploadFile的name字段必须和后端接口约定的表单字段名一致,否则后端拿不到文件;二是uploadFile的响应data是字符串,不是对象,忘掉JSON.parse会导致前端拿到[object Object]之类的字符串数据,进而引发诡异的渲染问题。
另外,如果一次要传多张图,需要注意wx.uploadFile一次只能传一个文件。多图上传要自己写循环,或者用异步并发控制,避免一次并发十几张把后端打爆。我一般会加上 3 个并发限制,上传进度可以用uploadTask.onProgressUpdate监听,给用户展示进度条。
5.3 订阅消息:一次性订阅如何设计
订阅消息是小程序触达用户最重要的手段。基础用法:
wx.requestSubscribeMessage({ tmplIds: ['模板ID'], success(res) { if (res['模板ID'] === 'accept') { console.log('用户同意订阅') } else { console.log('用户拒绝') } } })这里有个关键机制:小程序订阅消息默认是“一次性”的。用户点一次同意,只允许你下发一条消息,用完之后想再发就得让用户再订阅一次。很多团队把订阅消息当成推送通知来用,上线后发现发不出第二条,就是这个机制导致的。
设计订阅行为时,不要一进页面就弹订阅框,用户大概率会拒绝。正确姿势是把订阅动作放在“用户明确有预期获得通知”的场景,比如下单成功后询问“是否接收订单状态通知”,这时用户同意率会高很多。下单这种场景天然可以配合订阅:用户点了同意,你先记录订阅授权,订单状态变化时调用后端接口下发消息。如果订阅次数是动态的,后端可以累计用户的授权次数,每次发消息消耗一次。
5.4 支付:requestPayment 的参数从哪来
小程序支付的标准流程是:前端不直接生成支付参数,而是先把订单信息发给自己的后端,后端调用微信支付接口生成预支付单,拿到paySign等参数返回给前端,前端再调wx.requestPayment拉起收银台:
wx.requestPayment({ timeStamp: payData.timeStamp, nonceStr: payData.nonceStr, package: payData.package, signType: 'RSA', paySign: payData.paySign, success() { // 支付成功 }, fail(err) { // 用户取消或支付失败 } })这里最容易出错的是package这个字段名。它是 JS 的保留字,但在 API 参数里就是叫package,不要自己改名。支付回调建议以后端的payNotify回调为准,不要只信前端的success——前端支付成功后页面可能被杀进程,这时候应以服务端收到的微信支付通知为准去更新订单状态。
6. 高频报错的排查链路:从400到handshake failed
6.1 400 类错误:参数结构、字段名、模型名对不上
小程序里遇到 400 错误,先别急着看后端代码,先检查三件事:请求 URL 的路径对不对、请求方法是不是后端约定的方法、请求体字段名和类型是不是后端约定的结构。
这两年随着 AI 接口普及,“400 invalid schema”之类的报错越来越常见。比如调某个模型接口时,model字段拼错了、参数里多传了一个不支持的字段、或者某个字段类型从 string 传成了 array,都会回 400。举个例子,后端大模型接口要求messages是数组,你传成了对象,它就会非常明确地告诉你 schema 不合法。还有一种情况是model名称写错或者部署版本不支持,经常看到“the supported api model names are xxx”这类提示,说明官方模型列表里没有你填的那个名字——这类错误往往后面会附上完整的可用模型清单,先仔细读报错文本,它已经告诉你答案了。
排查这类问题的链路是:先看报错内容本身给出的提示词,再核对请求参数是否严格按照接口文档的 JSON 结构传,最后用接口文档里的示例数据先跑通一遍,确认“示例能通、自己的数据不能通”,那就逐字段比对差异。
6.2 WebSocket handshake failed:upgrade header为空的真相
用wx.connectSocket做实时通信时,常见报错是handshake failed due to invalid upgrade header: null。这个报错的直接含义是:WebSocket 握手阶段,服务端期望的Upgrade请求头没对上。
我踩过的几个原因按概率排:
- 协议没对上:前端用了
ws://,而微信要求域名必须 HTTPS,WebSocket 必须用wss://。写成ws://基本必报握手失败。 - 路径或端口问题:WebSocket 地址里带了工具不支持的内容,或者服务端只监听了
/ws路径,前端连的是根路径。 - 鉴权头格式问题:小程序
wx.connectSocket支持传入header,但能自定义的 header 有限,有些自定义 header 会被微信客户端过滤掉,导致服务端鉴权失败后直接拒绝升级请求,表现也是握手失败。
排查时建议先开开发者工具的 Network 面板,看握手请求的实际状态码。如果是 401/403,说明是鉴权问题;如果是 404,说明路径不对;如果直接看不到握手请求,大概率是 URL 本身就不满足wss://的要求。连接成功后,记得用onSocketMessage接收数据,用onSocketClose处理断线重连。断线重连的逻辑一定要做,移动端网络切换非常频繁,WebSocket 连接很容易被系统杀掉。
6.3 顶部导航栏高度:一个不是报错的“适配问题”
很多自定义导航栏的项目都会遇到同一个问题:右上角胶囊按钮(胶囊按钮就是微信在导航栏右侧固定的那个胶囊形状按钮,开发者无法隐藏)的位置在不同手机上不一样,自定义标题怎么放都对不齐。
解法是用wx.getMenuButtonBoundingClientRect()拿到胶囊按钮的位置和尺寸,再结合系统状态栏高度计算导航栏高度:
const menu = wx.getMenuButtonBoundingClientRect() const { statusBarHeight } = wx.getSystemInfoSync() const navBarHeight = (menu.top - statusBarHeight) * 2 + menu.height这套公式的意思是:胶囊按钮顶部到状态栏底部的距离,乘以 2,再加上胶囊自身高度,就是自定义导航栏的整体高度。因为这个位置的胶囊是系统固定渲染的,以它为基准做出来的自定义导航栏标题才能保证所有机型上都能垂直居中。手机型号越奇葩,这个公式的价值越大,它比任何“固定 44px”的做法都靠谱。
6.4 真机和开发者工具行为不一致
这是最常见的“玄学”:模拟器里一切正常,真机上就报错。主要原因有三个:
一是域名校验。模拟器勾了“不校验合法域名”,真机不认这个开关,必须是合法 HTTPS 域名。
二是基础库版本差异。开发者工具默认用的是最新基础库,但用户手机上的微信版本五花八门。某个 API 在低版本基础库里不存在,调用就会报xxx is not a function。解决方案是在app.json里设置合理的miniprogramRoot配套基础库编译版本,或者用wx.canIUse做能力检测,在低版本上做降级处理。
三是权限弹窗的差异。定位、相册、麦克风等权限在模拟器里可能直接通过,真机上必须走用户的系统授权弹窗。所以要养成习惯:每个可能用到敏感权限的功能,都要做好“用户拒绝授权”的分支处理。
7. 安全红线:这些坑千万别踩
7.1 appsecret 出现在前端等于裸奔
我见过不止一个新手的项目,把AppSecret直接写在app.js里。前面说过,前端代码可以被反编译,AppSecret一旦泄露,别人就能冒充你的后端调用微信接口,比如用你的session_key解密用户手机号、发送订阅消息、访问用户数据。正确做法是 AppSecret 只存在后端环境变量或密钥管理服务里,前端任何地方都不出现。
7.2 域名校验不是摆设
有的团队为了省事,开发时一直勾着“不校验合法域名”,上线前忘记去掉,结果正式版本在用户手机上所有请求全部失败。这个校验是微信为了安全强制要求的,不是可以绕过的配置。开发阶段可以放松,但发布前一定要去后台把正式域名配好,并且用真机跑一遍完整的核心流程。
7.3 鉴权与防刷
登录时,后端要校验 code 是否有效、是否已使用过;业务接口要对每个 token 做身份校验,不能因为“小程序比较小众”就省略。还有内容安全接口,如果业务里包含用户生成的文本或图片,建议接入security.msgSecCheck/security.imgSecCheck这类内容安全能力,避免出现合规风险。
最后分享一个我个人的习惯:每次新项目拿到手,第一件事不是写业务,而是先花一个下午把wx.request封装、登录流程、错误处理、日志上报这四件事做好。项目越大,你会发现这四件“前戏”越是救命的基础设施。API 本身只是一个个零件,真正决定一次开发顺不顺利的,是你把这些零件组装成流水线的能力。