微信小程序与 H5 调用第三方 API 的正确姿势
正确姿势是先划清信任边界:密钥与写操作一律放服务端,由前端通过自建 BFF 代理调用;小程序配置合法域名并设定超时重试,H5 用同源代理规避 CORS 预检,同时统一错误处理规则与限流缓存。
- 密钥、计费、写操作类接口必须走 BFF,Key 只放服务端
- 小程序需预先配置 HTTPS 合法域名,超时设 4~6 秒并限次重试
- H5 带自定义请求头会触发 CORS 预检,应用同源代理而非前端绕过
- 只对 429 与 5xx 做有限退避重试,非幂等 POST 不要自动重试
- BFF 统一 {code,message,traceId} 错误结构,并加限流与短 TTL 缓存
为什么“能跑”不等于“能上线”#
很多团队第一次在微信小程序或 H5 里接第三方 API,做法都很直接:把文档里的 curl 示例改成一个 wx.request 或 fetch,本地看到 JSON 返回就认为完成了。演示阶段确实没问题,但真实流量一上来,问题会集中暴露:密钥被打进代码包、请求域名没进白名单、跨域预检失败、超时没有上限、失败重试把配额打爆。近期技术社区关于应用审核与客户端透明度的讨论,无论具体事实如何,都指向同一个工程常识:客户端交付物不是保险箱,写进小程序包或前端 bundle 的字符串,都要假设它可能被看到。本文按“边界划分 → 小程序 → H5 → BFF 收口 → 错误契约 → 检查清单”的顺序,给出一套可以直接落地的做法。
一、先划信任边界:哪些调用不能放在前端#
第三方 API 的调用可以粗暴分成两类:公开只读、低配额、无敏感副作用的,和带计费、带写操作、带密钥的。前者可以前端直连,后者必须经过自己的服务端。判断标准不是“接口复不复杂”,而是“密钥泄露后损失有多大”。
- API Key、签名盐值、支付回调校验逻辑,一律只放服务端环境变量。
- 需要 POST 写数据、扣减额度、发送通知的接口,走自建 BFF,前端只拿业务结果。
- 需要在客户端做重试的接口,先确认它是否幂等;非幂等接口不要自动重试。
- 小程序云函数、Serverless 函数可以作为轻量 BFF,但同样要配置密钥与超时。
以 IP 查询为例,IP 查询接口的调用形状是 https://www.api.plus/api/get?apikey=YOUR_KEY&ip=8.8.8.8。在本地调试时直连看响应没问题,但生产环境更推荐让前端请求自己的 /api/ip,由服务端注入 Key 并做参数校验。
二、小程序侧:合法域名、超时预算与统一封装#
微信小程序的网络请求有硬性约束:请求域名必须是 HTTPS、已备案,并提前配置到小程序的 request 合法域名列表;开发者工具里的“不校验合法域名”只对本地调试有效,真机与线上不会放行。另一个常见坑是超时:wx.request 默认超时不一定符合你的页面体验,建议显式设置 4~6 秒,并给重试设置上限。
示例说明:以下代码仅用于逻辑验证,生产环境需补充异常处理、超时与重试、日志与监控告警及降级策略。
const API_BASE = 'https://www.api.plus/api/get';
function requestJSON(url, data, options) {
const opts = options || {};
const timeout = opts.timeout || 5000;
const maxRetry = opts.maxRetry == null ? 2 : opts.maxRetry;
return new Promise(function (resolve, reject) {
function attempt(left) {
wx.request({
url: url,
method: 'GET',
data: data,
timeout: timeout,
success: function (res) {
if (res.statusCode === 200) {
resolve(res.data);
return;
}
if ((res.statusCode === 429 || res.statusCode >= 500) && left > 0) {
setTimeout(function () { attempt(left - 1); }, 600 * (maxRetry - left + 1));
return;
}
reject(new Error('HTTP ' + res.statusCode));
},
fail: function (err) {
if (left > 0) {
setTimeout(function () { attempt(left - 1); }, 600 * (maxRetry - left + 1));
return;
}
reject(err);
}
});
}
attempt(maxRetry);
});
}
requestJSON(API_BASE, { apikey: 'YOUR_KEY', ip: '8.8.8.8' })
.then(function (data) { console.log(data); })
.catch(function (err) { console.error(err.message); });这段封装刻意做了三件事:只对 429 和 5xx 做有限退避重试、把超时作为参数暴露、把错误统一成 Error。页面层只需要处理 loading、空态和失败提示,不需要关心底层是第几次重试。注意上面的 YOUR_KEY 仅用于演示接口形状,真实项目应改为请求自有 BFF。
三、H5 侧:CORS、预检与可取消请求#
H5 比小程序更自由,但也更容易踩跨域。浏览器把请求分成简单请求和需要预检的请求:一旦你带上 X-API-Key 这类自定义头,或者把 Content-Type 设成 application/json,浏览器就会先发 OPTIONS 预检。如果第三方接口没有返回对应的 Access-Control-Allow-Origin 和 Access-Control-Allow-Headers,响应会被浏览器拦下,Network 面板里看起来“请求成功”,代码里却拿不到数据。遇到这种情况,不要试图在前端绕,正确做法是同源 BFF 代理。
async function getJSON(url, timeoutMs) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs || 5000);
try {
const res = await fetch(url, {
method: 'GET',
signal: controller.signal,
credentials: 'omit',
headers: { 'Accept': 'application/json' }
});
if (!res.ok) throw new Error('HTTP ' + res.status);
return await res.json();
} finally {
clearTimeout(timer);
}
}
getJSON('https://www.api.plus/api/dongman?type=json', 4000)
.then((data) => console.log(data))
.catch((err) => console.warn(err.name === 'AbortError' ? '请求超时' : err.message));图片类接口还有额外细节。以随机动漫壁纸接口为例,type=json 返回的是元数据,真正的图片由 <img> 加载。图片请求不受 CORS 限制,但如果后续要用 canvas 处理,就会遇到画布污染问题;需要处理图像时,应让图片走同源代理或在服务端完成裁剪。另外,随机接口不要在同一页面高频轮询,建议加本地缓存和节流,避免把配额消耗在用户反复切换上。
四、用 BFF 收口:代理、鉴权与限流#
BFF(Backend for Frontend)不需要很重,一个路由就能解决密钥和跨域两个问题。下面是一个 Node.js + Express 的最小示例,核心是:校验入参、从环境变量读取 Key、设置上游超时、透传状态码。
const express = require('express');
const app = express();
const API_KEY = process.env.API_PLUS_KEY;
app.get('/api/ip', async (req, res) => {
const ip = String(req.query.ip || '').trim();
if (!ip || ip.length > 45) {
return res.status(400).json({ code: 'BAD_IP', message: 'ip 参数不合法' });
}
const target = 'https://www.api.plus/api/get?apikey=' +
encodeURIComponent(API_KEY) + '&ip=' + encodeURIComponent(ip);
try {
const r = await fetch(target, { signal: AbortSignal.timeout(4000) });
const body = await r.text();
res.status(r.status).type('application/json').send(body);
} catch (err) {
res.status(504).json({ code: 'UPSTREAM_TIMEOUT', message: '上游超时' });
}
});
app.listen(3000);| 维度 | 小程序直连 | H5 直连 | 自建 BFF |
|---|---|---|---|
| 密钥暴露面 | 代码包可被解包 | bundle 可被查看 | 仅服务端可见 |
| 跨域问题 | 无 CORS,但有域名白名单 | 受 CORS 与预检约束 | 同源,无跨域 |
| 限流与缓存 | 难统一 | 难统一 | 可集中做 |
| 适用场景 | 公开低配额接口 | 公开低配额接口 | 计费、写操作、多端复用 |
代理层还应该做两件容易被忽略的事:按用户或 IP 做简单限流,避免单个前端把额度打满;对稳定的查询结果做短 TTL 缓存,例如同一 IP 的归属地在几分钟内复用。这样即使上游抖动,前端体验也可控。
五、错误契约与可观测性#
前端调用第三方 API 时,最忌讳把上游返回原样透传给用户。建议在 BFF 层统一错误结构,例如 { code, message, traceId },并区分“参数错误”“上游超时”“额度不足”“鉴权失败”几类。对于需要提交表单或 JSON 体的接口,可以参考站内POST 示例接口,重点检查 Content-Type、字段类型和重复提交保护。
- 日志里记录上游状态码、耗时和 traceId,但不要打印完整 Key。
- 非幂等 POST 请求不要自动重试;需要重试时由服务端生成幂等键。
- 把 429 和 5xx 当作可恢复错误,把 4xx 参数错误当作不可恢复错误。
- 前端对超时给出明确文案,不要一直转圈。
更多接口形态与响应约定,可以在API 文章中心按主题检索;遇到接入问题,也可以通过技术支持反馈。
六、上线前检查清单#
- 小程序后台已配置 request 合法域名,线上包没有依赖“不校验域名”开关。
- API Key 只存在于服务端环境变量,前端代码与构建产物中搜索不到。
- 所有请求都有显式超时,重试次数有上限,非幂等请求已排除自动重试。
- H5 已确认目标接口的 CORS 响应头,或已改为同源代理。
- 图片、随机类接口有缓存与节流,避免重复消耗配额。
- 错误码、日志、traceId 已串起来,能在分钟级定位上游故障。
总结#
微信小程序和 H5 调用第三方 API,真正要解决的不是“怎么发请求”,而是信任边界、域名与跨域约束、超时与重试预算、错误契约这四件事。公开低配额接口可以直连,但涉及密钥和计费的调用应统一收口到 BFF;这样既降低泄露风险,也让限流、缓存和排障有地方落地。如果你还没有可用的 Key,可以在 API.PLUS 注册账号获取免费 API Key,先用 IP 查询和随机壁纸接口把整条链路跑通,再按本文的清单逐步加固。