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 实例方法
| 方法 | 签名 | 说明 |
|---|---|---|
get | get(url, data, config) | GET 请求 |
post | post(url, data, config) | POST 请求 |
put | put(url, data, config) | PUT 请求 |
delete | delete(url, data, config) | DELETE 请求 |
patch | patch(url, data, config) | PATCH 请求 |
makeReq | makeReq(config, intereptorRes, intereptorReq) | 底层请求方法 |
refreshToken | refreshToken() | Token 刷新 |
setOptions | setOptions(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 |
|---|---|---|
| GET | URL query string | - |
| POST/PUT/DELETE/PATCH | Body | application/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.requestAPI 模块清单
主包导出模块(30个)
src/api/index.js 导出以下模块:
| 模块 | 说明 | 模块 | 说明 |
|---|---|---|---|
article | 文章 | aftersales | 售后 |
cart | 购物车 | cashier | 收银台 |
category | 分类 | item | 商品 |
member | 会员 | promotion | 促销 |
region | 地区 | trade | 交易/订单 |
user | 用户 | seckill | 秒杀 |
wx | 微信 | shop | 店铺 |
distribution | 分销 | track | 追踪 |
vip | VIP | group | 拼团 |
groupBy | 团购 | wheel | 大转盘 |
pointitem | 积分商品 | guide | 导购 |
liveroom | 直播 | purchase | 内购 |
wgts | 组件 | salesman | 业务员 |
im | 即时通讯 | design | 设计 |
分包专用模块(7个)
以下模块不从主包导出,分包内直接引入:
| 模块 | 说明 | 使用分包 |
|---|---|---|
community | 社区团购 | subpages/community/ |
dianwu | 店务POS | subpages/dianwu/ |
game | 游戏活动 | subpages/game-activity/ |
boost | 砍价 | boost/ |
mdugc | UGC | subpages/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 })