小程序的网络请求不仅是简单的 wx.request 调用,更牵涉并发控制、错误处理、数据缓存、跨端数据一致性等系统工程问题。一套健壮的请求层设计,能显著降低业务代码复杂度、提升弱网体验、统一异常处理口径。本文将从 wx.request 的底层行为讲起,逐步构建「请求封装 → 拦截器 → Repository 数据层 → 状态管理 → 缓存策略」的完整网络与数据架构。
一、wx.request 深入
wx.request 是小程序最核心的网络 API,其完整参数远超多数开发者的日常使用范围:
const requestTask = wx.request({
url: 'https://api.example.com/v1/products', // 必须是已配置的合法域名
method: 'POST', // GET/POST/PUT/DELETE 等
data: { category: 'electronics', page: 1 }, // 请求体
header: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + getToken()
},
dataType: 'json', // 默认 json,无需手动 JSON.parse
responseType: 'text', // text / arraybuffer
enableHttp2: true, // 开启 HTTP/2
enableQuic: true, // 开启 QUIC
timeout: 15000, // 单位毫秒
success(res) {
// res.statusCode:HTTP 状态码
// res.data:响应体(dataType=json 时已解析)
// res.header:响应头
},
fail(err) {
// 网络错误、超时、域名非法等
},
complete() {}
});
关键约束与行为:
- 域名白名单:
https://接口必须配置在小程序后台「开发管理 → 服务器域名」中,开发阶段可勾选「不校验合法域名」。 - 并发上限:同一个小程序全局同时最多 10 个
wx.request请求(含上传/下载),超出部分排队等待。 - 返回值
RequestTask:可用于主动取消或监听响应头。
1.1 RequestTask 与取消
// 可取消的请求
const task = wx.request({ url: '...', ... });
// 场景:页面卸载时取消未完成请求
Page({
onLoad() {
this._task = wx.request({ url: '/api/slow', success: this.onData });
},
onUnload() {
this._task?.abort(); // 主动取消,触发 fail(errMsg 含 abort)
}
});
一句话:
wx.request返回的RequestTask等价于浏览器中的AbortController.signal取消句柄,页面销毁时abort()是防泄漏的标配动作。
二、并发控制与请求取消
2.1 并发限制器
全局 10 个并发上限共享给所有页面,密集场景(如图片墙、批量查询)需要自建并发池:
// utils/concurrency.js
class ConcurrencyPool {
constructor(limit = 6) {
this.limit = limit;
this.queue = [];
this.active = 0;
}
add(taskFactory) {
return new Promise((resolve, reject) => {
this.queue.push({ taskFactory, resolve, reject });
this._drain();
});
}
_drain() {
while (this.active < this.limit && this.queue.length > 0) {
const { taskFactory, resolve, reject } = this.queue.shift();
this.active++;
taskFactory()
.then(resolve)
.catch(reject)
.finally(() => { this.active--; this._drain(); });
}
}
}
2.2 竞态处理
高频场景(搜索框输入、下拉刷新)要防止「过期响应覆盖新响应」:
Page({
data: { keyword: '', list: [] },
async onSearch(e) {
const keyword = e.detail.value;
this._requestSeq = (this._requestSeq || 0) + 1;
const seq = this._requestSeq;
const list = await request('/api/search', { keyword });
if (seq !== this._requestSeq) return; // 已被更新的请求取代
this.setData({ list });
}
});
一句话:并发控制解决「资源竞争」,请求序号(或取消)解决「响应竞态」,二者共同保证数据新鲜度与稳定性。
三、请求拦截器与统一错误处理
3.1 拦截器实现
小程序没有内置 axios 式拦截器,通常封装一层 request 包装函数,在发送前注入 Token、在响应后统一处理错误:
// utils/request.js
const pending = new Map(); // 用于去重与取消
function request(options) {
const { url, method = 'GET', data, header = {}, skipAuth = false } = options;
// 请求拦截:注入登录态
const token = wx.getStorageSync('token');
const finalHeader = {
'Content-Type': 'application/json',
...header,
...(token && !skipAuth ? { Authorization: 'Bearer ' + token } : {})
};
return new Promise((resolve, reject) => {
wx.request({
url, method, data,
header: finalHeader,
timeout: 15000,
success(res) {
// 响应拦截:统一处理业务码
const body = res.data;
if (res.statusCode >= 200 && res.statusCode < 300 && body.code === 0) {
resolve(body.data);
} else if (res.statusCode === 401) {
handleUnauthorized(); // 触发登录态刷新或强制登录
reject(new Error('登录已过期'));
} else {
showToast(body.msg || '服务异常');
reject(new Error(body.msg));
}
},
fail(err) {
showToast(err.errMsg.includes('abort') ? '已取消' : '网络异常');
reject(err);
}
});
});
}
module.exports = { request };
3.2 错误分级处理
| 错误类型 | 判断依据 | 处理策略 |
|---|---|---|
| HTTP 4xx | statusCode 400-499 | 业务参数/权限问题,提示后不重试 |
| HTTP 5xx | statusCode 500-599 | 服务异常,可重试 1-2 次 |
| 401/403 | statusCode 401 | 刷新 Token 后重放请求 |
| 网络错误 | fail 回调 | 提示弱网,进入离线兜底 |
| 业务错误 | 自定义 code !== 0 | 按业务码提示 |
一句话:拦截器的价值在于把「所有接口的公共逻辑」收敛到一处:注入 Token、判定业务码、提示错误、处理 401,让业务代码只关心数据本身。
四、数据层设计:Repository 模式
当页面增多、数据源变杂(接口、缓存、云开发、本地 DB),需要把「数据的获取方式」从「页面」中抽离,形成 Repository 数据仓库。
4.1 分层结构
pages/
order/ # 页面层:只关心 UI 与交互
order.js
repositories/
orderRepo.js # 数据仓库层:封装数据来源与缓存
services/
api.js # 基础请求封装
// repositories/orderRepo.js
const { request } = require('../services/api');
class OrderRepo {
async getOrderList(params) {
const cacheKey = `order_list_${params.page}`;
const cached = wx.getStorageSync(cacheKey);
if (cached && Date.now() - cached.ts < 60 * 1000) {
return cached.data; // 读缓存
}
const data = await request({ url: '/api/orders', data: params });
wx.setStorageSync(cacheKey, { data, ts: Date.now() }); // 写缓存
return data;
}
async createOrder(payload) {
return request({ url: '/api/orders', method: 'POST', data: payload });
}
}
module.exports = new OrderRepo();
4.2 Repository 收益
- 单一数据入口:页面不再直接拼 URL,改动数据源只改 Repository。
- 可测试:Repository 可被 mock,便于单元测试。
- 缓存统一:缓存策略收敛在数据层,页面无感知。
- 切换后端:从自建后端切到云开发,只改 Repository 实现,页面零改动。
一句话:Repository 模式的本质是「面向接口编程」:页面依赖的是「获取订单列表」这个能力,而不是「这个 URL + 这个参数」。
五、云开发与自建后端的取舍
网络层选型是架构决策,直接影响请求封装、鉴权与运维方式:
| 维度 | 自建后端(HTTPS API) | 云开发(wx.cloud) |
|---|---|---|
| 请求方式 | wx.request + 自有封装 | wx.cloud.callFunction / wx.cloud.database |
| 域名配置 | 需后台配置服务器域名 | 无需配置,天然白名单 |
| 鉴权 | 自建 Token 体系 | 云函数自动携带身份 |
| 运维 | 服务器/证书/扩容自理 | 平台托管 |
| 灵活性 | 可接入任意技术栈/第三方 | 深度绑定腾讯云生态 |
| 学习成本 | 需懂后端 | 前端可独立完成 |
混合架构同样常见:核心交易走自建后端保证可控性,图片/文件走云存储 CDN,实时消息走云函数订阅推送。
// 混合架构示例:自建 API 为主,云开发辅助
const { request } = require('../services/api'); // 自建
const cloud = require('../services/cloud'); // 云开发
// 订单走自建后端(支付敏感)
orderRepo.createOrder(payload);
// 文件走云存储(CDN 加速)
wx.cloud.uploadFile({ cloudPath, filePath });
六、缓存与离线策略
6.1 缓存分层
| 层级 | 载体 | 生命周期 | 适用 |
|---|---|---|---|
| 内存缓存 | 全局对象 | 一次会话 | 频繁访问的配置 |
| Storage 缓存 | wx.setStorageSync | 持久化 | 列表/详情兜底 |
| 服务端缓存 | CDN / 服务端 Redis | 服务端控制 | 静态数据、聚合数据 |
6.2 Stale-While-Revalidate
先展示旧数据、后台静默更新,是弱网体验的最优解:
// 先读缓存秒开,再后台刷新
async function loadProducts() {
const cached = wx.getStorageSync('products');
if (cached) this.setData({ products: cached }); // 秒开
const fresh = await request({ url: '/api/products' });
wx.setStorageSync('products', fresh);
this.setData({ products: fresh });
}
6.3 离线兜底
- 首屏数据本地化:把上次成功数据作为离线快照。
- 写操作队列化:弱网时把请求写入待发队列,网络恢复后重放。
// 待发队列示例
const offlineQueue = wx.getStorageSync('offline_queue') || [];
function enqueueWrite(action) {
offlineQueue.push(action);
wx.setStorageSync('offline_queue', offlineQueue);
// 监听 wx.onNetworkStatusChange 恢复时重放
wx.onNetworkStatusChange((res) => {
if (res.isConnected) flushQueue();
});
}
一句话:缓存的终极目标是「秒开 + 不错数据」:读缓存保证秒开,后台刷新保证数据新鲜,写队列保证弱网不丢。
七、与状态管理的协同
请求层与状态管理(MobX 或全局 Store)的边界要清晰:数据层负责「取数」,状态层负责「分发」。
// store/product.js —— 以 MobX 为例
const { observable, action } = require('mobx-miniprogram');
class ProductStore {
@observable list = [];
@observable loading = false;
@action async fetchList() {
this.loading = true;
try {
this.list = await productRepo.getProductList(); // 数据层取数
} finally {
this.loading = false;
}
}
}
页面订阅 Store 的 loading 状态驱动骨架屏,而不是每个页面各自维护一套 loading 标志。
八、常见问题
- 超时设置不生效:部分真机上
timeout最小粒度、系统网络层优先级高于配置,需真机验证。 - 域名不在白名单:报
url not in domain list,检查后台配置与「不校验合法域名」开关。 Content-Type不匹配:data为对象时建议显式声明application/json,否则后端可能收不到 body。- 请求被重复发送:快速点击触发多个相同请求,可在拦截器层做 key 去重。
- 并发超 10 被卡住:检查是否有请求未
complete(如被abort但仍占用并发槽位)。
九、总结
| 环节 | 方案 | 关键收益 |
|---|---|---|
| 基础请求 | wx.request + RequestTask | 取消、监听、并发控制 |
| 请求封装 | 拦截器注入 Token / 统一错误 | 收敛公共逻辑 |
| 数据层 | Repository 模式 | 单一入口、可测试、可换源 |
| 架构选型 | 自建后端 vs 云开发 | 可控性与运维成本的平衡 |
| 缓存离线 | 内存/Storage/服务端三层 | 秒开体验与弱网兜底 |
| 状态协同 | 数据层取数、Store 分发 | 关注点分离 |
网络与数据层是决定小程序体验上限的「基础设施」。从 wx.request 的正确使用,到拦截器、Repository、缓存分层,再到云开发与自建后端的合理取舍,逐步把请求从「散落的 wx.request 调用」升级为「工程化的数据架构」,才能支撑起复杂业务在真实网络环境下的稳定表现。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。