小程序网络请求与数据层设计:wx.request、拦截器与状态管理

深入剖析 wx.request 的参数、并发限制与请求取消,讲解请求拦截器与统一错误处理,并给出 repository 数据层模式、云开发与自建后端的选型取舍,以及缓存与离线策略的最佳实践。

小程序的网络请求不仅是简单的 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 4xxstatusCode 400-499业务参数/权限问题,提示后不重试
HTTP 5xxstatusCode 500-599服务异常,可重试 1-2 次
401/403statusCode 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 调用」升级为「工程化的数据架构」,才能支撑起复杂业务在真实网络环境下的稳定表现。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「miniprogram」更多文章

  1. 小程序动画与 Canvas 实践:交互动效、海报生成与可视化
  2. 微信支付与交易闭环:统一下单、回调与退款
  3. 小程序分包加载与性能优化进阶:主包瘦身、预下载与按需注入