Skip to content

05 - API与请求层 ​

核心请求封装 ​

API 类(src/api/req.js) ​

req.js 是整个请求层的核心,封装了一个 API 类,处理完整的请求生命周期:

js
const api = new API({
  baseURL: process.env.APP_BASE_URL
})

export default api
export { API }

API 实例方法 ​

方法签名说明
getget(url, data, config)GET 请求
postpost(url, data, config)POST 请求
putput(url, data, config)PUT 请求
deletedelete(url, data, config)DELETE 请求
patchpatch(url, data, config)PATCH 请求
makeReqmakeReq(config, intereptorRes, intereptorReq)底层请求方法
refreshTokenrefreshToken()Token 刷新
setOptionssetOptions(opts)动态更新配置

请求生命周期 ​

完整流程图 ​

组件调用 api.get('/user/info', { page: 1 })
  │
  ├─→ getReqUrl(url)
  │    └─ 拼接 baseURL + url(http 开头则直接使用)
  │
  ├─→ intereptorReq (请求拦截)
  │    ├─ company_id 注入(多租户)
  │    ├─ country_code / language 注入(多语言)
  │    ├─ Token 选择(mall vs merchant)
  │    ├─ 请求序列化(GET→query / POST→body)
  │    └─ authorizer-appid header(微信)
  │
  ├─→ request(options)  ← 底层请求适配(H5/微信)
  │
  └─→ intereptorRes (响应拦截)
       ├─ statusCode === 200 && status_code === 0 → 成功
       ├─ statusCode === 401 && code === 401001 → Token 刷新
       ├─ statusCode === 401 && code === 401002 → 用户被禁用
       └─ statusCode === 401 → 登出并跳转登录页

URL 拼接 ​

js
getReqUrl(url) {
  if (url.startsWith('http')) return url  // 完整 URL 直接使用
  return this.options.baseURL + url       // 否则拼接 baseURL
}

company_id 解析(多租户) ​

js
setOptions(opts) {
  let company_id = process.env.APP_COMPANY_ID

  // H5 环境:从域名提取 company_id
  // m38.shopex123.com → company_id = 38
  if (Taro.getEnv() == Taro.ENV_TYPE.WEB && global.location) {
    const match = global.location.hostname.match(/^m(\d+)\./)
    if (match && match[1]) company_id = match[1]
  }

  // 微信小程序:从 ext.json 覆盖
  if (isWeixin) {
    const extConfig = getExtConfigData()
    if (extConfig.company_id) options.company_id = extConfig.company_id
  }
}

语言/国家码注入 ​

js
const langMap = {
  zhcn: 'zh-CN',
  en: 'en-CN',
  zhtw: 'zh-TW',
  ar: 'ar-SA'
}

query['country_code'] = langMap[lang || process.env.APP_COUNTRY_CODE]

Token 选择逻辑 ​

js
// intereptorReq 中:
const token = useMallToken
  ? Taro.getStorageSync(SG_TOKEN)        // 强制使用商城会员 Token
  : getS().getAuthToken()                 // 自动选择(根据当前模块)

// Spx.getAuthToken() 内部:
getAuthToken() {
  if (isMerchantModule()) {
    return this.get(MERCHANT_TOKEN)  // 商户模块用商户 Token
  }
  return this.get(SG_TOKEN)           // 其他用会员 Token
}

useMallToken 参数:当请求需要强制使用商城会员 Token 时(如商户入驻页证照上传走会员接口),传入 useMallToken: true:

js
api.post('/upload', data, { useMallToken: true })

请求序列化 ​

方法参数位置Content-Type
GETURL query string-
POST/PUT/DELETE/PATCHBodyapplication/x-www-form-urlencoded

使用 qs.stringify 序列化:

js
// GET
url = `${url}?${qs.stringify(data)}`

// POST
body = qs.stringify(data)
header['content-type'] = 'application/x-www-form-urlencoded'

Token 刷新机制 ​

触发条件 ​

当响应 statusCode === 401 且 data.code === HTTP_STATUS.TOKEN_NEEDS_REFRESH (401001) 时触发 Token 刷新。

刷新流程 ​

请求 A 返回 401 (TOKEN_NEEDS_REFRESH)
  │
  ├─→ refreshToken()
  │    ├─ 检查 refreshTokenPromise(防并发刷新)
  │    ├─ 设置 isRefreshingToken = true
  │    ├─ 用当前 Token 调用 /token/refresh
  │    └─ 从响应 header Authorization 解析新 Token
  │         └─ getS().setAuthToken(newToken)
  │
  ├─→ 刷新成功
  │    └─ 重试原请求(makeReq 递归,noPending: true 防止再次进入等待队列)
  │
  └─→ 刷新期间其他请求(请求 B、C...)
       └─→ isRefreshingToken === true → 进入 pendingReq 等待队列
            └─ 刷新完成后,队列中的请求用新 Token 继续执行

核心代码 ​

js
async makeReq(config) {
  const res = await this.request(options)

  // Token 需要刷新
  if (res.statusCode === 401 && res.data?.data?.code === HTTP_STATUS.TOKEN_NEEDS_REFRESH) {
    const refreshed = await this.refreshToken()
    if (!refreshed) return Promise.reject(...)
    // 用新 Token 重试
    return this.makeReq({ ...config, noPending: true })
  }

  // 正在刷新中,其他请求进入等待队列
  if (this.isRefreshingToken && !config.noPending) {
    return this.pendingReq(config)
  }

  return intereptorRes(res)
}

Token 刷新方法 ​

js
async refreshToken() {
  // 防并发:如果已有刷新 Promise,复用
  if (this.refreshTokenPromise) return this.refreshTokenPromise

  this.isRefreshingToken = true
  this.refreshTokenPromise = (async () => {
    const token = getS().getAuthToken()
    await this.makeReq({
      header: { Authorization: `Bearer ${token}` },
      method: 'get',
      url: this.getReqUrl('/token/refresh'),
      noPending: true  // 防止递归刷新
    }, (res) => {
      // 新 Token 从响应 header 的 Authorization 字段解析
      const newToken = parseRefreshTokenFromResponse(res)
      if (newToken) getS().setAuthToken(newToken)
    })
  })()

  await this.refreshTokenPromise
  this.isRefreshingToken = false
  this.refreshTokenPromise = null
}

登出逻辑 ​

handleLogout ​

当响应 statusCode === 401(非 Token 刷新场景)时触发登出:

js
handleLogout() {
  this.requestQueue.destroy()  // 清空等待队列
  getS().logout()              // 清除 Token 和 userInfo

  // 根据模块跳转到对应登录页
  if (isMerchantModule()) → '/subpages/merchant/login'
  else if (goodsShelvesModule) → '/subpages/guide/index'
  else if (purchaseModule) → '/subpages/purchase/member'
  else → '/subpages/member/index'
}

Spx.logout() ​

js
logout() {
  this.delete(SG_TOKEN)
  this.delete(SG_USER_INFO)
  store.dispatch(clearUserInfo())
  // 兼容:同时清除 Storage
  Taro.removeStorageSync(SG_TOKEN)
  Taro.removeStorageSync(SG_USER_INFO)
}

HTTP 状态码常量 ​

src/api/consts.js:

js
export const HTTP_STATUS = {
  SUCCESS: 200,
  CREATED: 201,
  ACCEPTED: 202,
  CLIENT_ERROR: 400,
  UNAUTHORIZED: 401,
  FORBIDDEN: 403,
  NOT_FOUND: 404,
  SERVER_ERROR: 500,
  BAD_GATEWAY: 502,
  SERVICE_UNAVAILABLE: 503,
  GATEWAY_TIMEOUT: 504,
  BUSINESS_ERROR: 422,
  TOKEN_NEEDS_REFRESH: 401001,  // Token 需要刷新
  USER_FORBIDDEN: 401002         // 用户被禁用
}

底层请求适配 ​

weapp 和 h5 两个平台的 Taro.request 行为有差异,需要适配:

H5 适配 ​

js
if (isWeb) {
  return async (...args) => {
    let res = await Taro.request(...args).catch(e => e)
    // fetch 失败时转换为统一格式
    if (e instanceof global.Response) {
      res = {
        data: await e.json(),
        statusCode: e.status,
        header: e.headers
      }
    }
    return res
  }
}

默认(微信) ​

js
return Taro.request

API 模块清单 ​

主包导出模块(30个) ​

src/api/index.js 导出以下模块:

模块说明模块说明
article文章aftersales售后
cart购物车cashier收银台
category分类item商品
member会员promotion促销
region地区trade交易/订单
user用户seckill秒杀
wx微信shop店铺
distribution分销track追踪
vipVIPgroup拼团
groupBy团购wheel大转盘
pointitem积分商品guide导购
liveroom直播purchase内购
wgts组件salesman业务员
im即时通讯design设计

分包专用模块(7个) ​

以下模块不从主包导出,分包内直接引入:

模块说明使用分包
community社区团购subpages/community/
dianwu店务POSsubpages/dianwu/
game游戏活动subpages/game-activity/
boost砍价boost/
mdugcUGCsubpages/mdugc/
delivery配送subpages/delivery/
merchant商户入驻subpages/merchant/

使用示例 ​

js
// 主包模块
import { cart, item, member } from '@/api'

const cartList = await cart.getList({ page: 1 })

// 分包专用模块
import communityApi from '@/api/community'
const groupList = await communityApi.getGroupList()

Storage 常量 ​

src/consts/localstorage.js:

js
export const SG_TOKEN = 'token'                        // 会员 Token
export const SG_DIANWU_TOKEN = 'dianwu_token'          // 店务 Token
export const MERCHANT_TOKEN = 'merchant_token'         // 商户 Token
export const SG_POLICY = 'policy_info'                  // 隐私协议
export const SG_SHARER_UID = 'distribution_shop_id'    // 推广用户ID
export const SG_TRACK_PARAMS = 'trackParams'           // 追踪参数
export const SG_QW_USERID = 'work_userid'              // 企微用户ID
export const SG_USER_INFO = 'userinfo'                 // 用户信息
export const SG_MEIQIA = 'meiqia'                      // 美洽客服配置
export const SG_YIQIA = 'echat'                        // 一洽客服配置
export const SG_APP_CONFIG = 'settingInfo'             // APP配置
export const SG_SHOW_ADD_TIP = 'addTipIsShow'          // 添加指引
export const SG_ROUTER_PARAMS = 'routerParams'         // 路由参数缓存
export const SG_GUIDE_PARAMS = 'guideParams'           // 导购参数
export const SG_GUIDE_PARAMS_UPDATETIME = 'guideParams_updatetime'
export const SG_GUIDE_PARAMS_EXPRESSTIME = 'guideParams_expresstime'
export const SG_CHECK_STORE_RULE = 'check_store_rule'  // 进店规则
export const SG_BROWSE_HISTORY_ITEMS = 'browse_history_items' // 浏览记录

新增 API 模块步骤 ​

1. 创建模块文件 ​

js
// src/api/myfeature.js
import api from './req'

export default {
  getList(params) {
    return api.get('/myfeature/list', params)
  },
  getDetail(id) {
    return api.get(`/myfeature/detail`, { id })
  },
  create(data) {
    return api.post('/myfeature/create', data)
  }
}

2. 决定导出位置 ​

  • 主包使用 → 在 src/api/index.js 中添加导出
  • 仅分包使用 → 不在 index.js 导出,分包内直接 import from '@/api/myfeature'

3. 主包导出 ​

js
// src/api/index.js
export { default as myfeature } from './myfeature'

使用:

js
import { myfeature } from '@/api'
const list = await myfeature.getList({ page: 1 })