258 lines
6.8 KiB
JavaScript
258 lines
6.8 KiB
JavaScript
/**
|
||
* uni.request 统一封装
|
||
*
|
||
* 设计目标:
|
||
* 1. 自动兼容项目中所有 API 的成功状态判断(code === 200 / code === 1 / status === 200)
|
||
* 2. 支持自定义 header、超时、baseURL
|
||
* 3. Promise 化,调用简单
|
||
* 4. 支持请求/响应拦截器(便于后续统一注入 token、处理错误等)
|
||
* 5. 数据提取自动降级:优先取 res.data.data,不存在则取 res.data
|
||
*/
|
||
|
||
class Request {
|
||
/**
|
||
* @param {Object} config - 全局默认配置
|
||
* @param {string} config.baseURL - 基础 URL
|
||
* @param {number} config.timeout - 超时时间(ms),默认 30000
|
||
* @param {Object} config.header - 默认请求头
|
||
*/
|
||
constructor(config = {}) {
|
||
this.config = {
|
||
baseURL: '',
|
||
timeout: 30000,
|
||
header: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
...config
|
||
};
|
||
this.interceptors = {
|
||
request: [],
|
||
response: []
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 添加请求拦截器
|
||
* @param {Function} fn - (config) => config
|
||
*/
|
||
addRequestInterceptor(fn) {
|
||
if (typeof fn === 'function') {
|
||
this.interceptors.request.push(fn);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 添加响应拦截器
|
||
* @param {Function} fn - (response) => response
|
||
*/
|
||
addResponseInterceptor(fn) {
|
||
if (typeof fn === 'function') {
|
||
this.interceptors.response.push(fn);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 判断响应是否成功
|
||
*
|
||
* 兼容项目中实际存在的 4 种响应格式:
|
||
* 1. CMS API: res.data.code === 200
|
||
* 2. Auth API: res.statusCode === 200 && (res.data.status === 200 || res.data.code === 1)
|
||
* 3. Hdk v2 API: res.data.code === 1
|
||
* 4. 兜底: res.statusCode === 200(无业务状态码时)
|
||
*
|
||
* @param {Object} res - uni.request 的 success 回调参数
|
||
* @returns {boolean}
|
||
*/
|
||
isSuccess(res) {
|
||
// HTTP 层已失败(如 404/500)
|
||
if (res.statusCode >= 400) {
|
||
return false;
|
||
}
|
||
|
||
const data = res.data;
|
||
if (!data || typeof data !== 'object') {
|
||
// 无响应体时,以 HTTP 状态码为准
|
||
return res.statusCode === 200;
|
||
}
|
||
|
||
// 提取业务状态码(兼容 code / status 两种字段名)
|
||
const code = data.code !== undefined ? Number(data.code)
|
||
: data.status !== undefined ? Number(data.status)
|
||
: null;
|
||
|
||
if (code !== null && !isNaN(code)) {
|
||
return code === 200 || code === 1;
|
||
}
|
||
|
||
// 无业务状态码时,以 HTTP 状态码为准
|
||
return res.statusCode === 200;
|
||
}
|
||
|
||
/**
|
||
* 从响应中提取业务数据
|
||
*
|
||
* 降级策略:
|
||
* - 优先取 res.data.data(CMS 标准格式)
|
||
* - 不存在则取 res.data 本身(直接返回格式)
|
||
* - 空响应返回 null
|
||
*
|
||
* @param {Object} res - uni.request 的 success 回调参数
|
||
* @returns {any}
|
||
*/
|
||
extractData(res) {
|
||
const data = res.data;
|
||
if (!data || typeof data !== 'object') {
|
||
return data;
|
||
}
|
||
return data.data !== undefined ? data.data : data;
|
||
}
|
||
|
||
/**
|
||
* 从响应中提取错误信息
|
||
* @param {Object} res - uni.request 的 success 回调参数
|
||
* @returns {string}
|
||
*/
|
||
extractError(res) {
|
||
const data = res.data;
|
||
if (!data || typeof data !== 'object') {
|
||
return `请求失败 (HTTP ${res.statusCode})`;
|
||
}
|
||
return data.msg || data.message || data.error || `请求失败 (code: ${data.code || data.status})`;
|
||
}
|
||
|
||
/**
|
||
* 核心请求方法
|
||
*
|
||
* @param {Object} options - 请求配置
|
||
* @param {string} options.url - 请求地址(可相对 baseURL)
|
||
* @param {string} options.method - 请求方法:GET/POST/PUT/DELETE,默认 GET
|
||
* @param {Object} options.data - 请求数据
|
||
* @param {Object} options.header - 自定义请求头(会合并覆盖默认 header,大小写不敏感)
|
||
* @param {number} options.timeout - 本次请求单独设置的超时时间
|
||
* @returns {Promise<{success: true, data: any, body: any, raw: Object}>}
|
||
*/
|
||
request(options = {}) {
|
||
return new Promise((resolve, reject) => {
|
||
// 1. 合并配置:默认 → 实例配置 → 单次请求配置
|
||
// header 合并需大小写不敏感去重(如 Content-Type 与 content-type 是同一头)
|
||
const mergedHeader = {};
|
||
for (const [k, v] of Object.entries(this.config.header)) {
|
||
mergedHeader[k.toLowerCase()] = v;
|
||
}
|
||
for (const [k, v] of Object.entries(options.header || {})) {
|
||
mergedHeader[k.toLowerCase()] = v;
|
||
}
|
||
let config = {
|
||
...this.config,
|
||
...options,
|
||
header: mergedHeader
|
||
};
|
||
|
||
// 2. 拼接 baseURL(如果 url 不是完整 HTTP 地址)
|
||
if (config.baseURL && config.url && !config.url.startsWith('http')) {
|
||
config.url = config.baseURL.replace(/\/$/, '') + '/' + config.url.replace(/^\//, '');
|
||
}
|
||
|
||
// 3. 执行请求拦截器
|
||
try {
|
||
for (const fn of this.interceptors.request) {
|
||
config = fn(config) || config;
|
||
}
|
||
} catch (err) {
|
||
reject({ success: false, message: err.message || '请求拦截器异常', code: -2 });
|
||
return;
|
||
}
|
||
|
||
// 4. 发起请求
|
||
uni.request({
|
||
url: config.url,
|
||
method: (config.method || 'GET').toUpperCase(),
|
||
data: config.data,
|
||
header: config.header,
|
||
timeout: config.timeout,
|
||
success: (res) => {
|
||
// 5. 执行响应拦截器
|
||
try {
|
||
for (const fn of this.interceptors.response) {
|
||
res = fn(res) || res;
|
||
}
|
||
} catch (err) {
|
||
reject({ success: false, message: err.message || '响应拦截器异常', code: -3, raw: res });
|
||
return;
|
||
}
|
||
|
||
// 6. 判断成功/失败
|
||
if (this.isSuccess(res)) {
|
||
resolve({
|
||
success: true,
|
||
data: this.extractData(res),
|
||
body: res.data,
|
||
raw: res
|
||
});
|
||
} else {
|
||
reject({
|
||
success: false,
|
||
message: this.extractError(res),
|
||
code: res.data?.code ?? res.data?.status ?? res.statusCode,
|
||
raw: res
|
||
});
|
||
}
|
||
},
|
||
fail: (err) => {
|
||
reject({
|
||
success: false,
|
||
message: err.errMsg || '网络请求失败,请检查网络',
|
||
code: -1,
|
||
raw: err
|
||
});
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* GET 请求快捷方法
|
||
* @param {string} url
|
||
* @param {Object} data
|
||
* @param {Object} options
|
||
*/
|
||
get(url, data, options = {}) {
|
||
return this.request({ url, data, method: 'GET', ...options });
|
||
}
|
||
|
||
/**
|
||
* POST 请求快捷方法
|
||
* @param {string} url
|
||
* @param {Object} data
|
||
* @param {Object} options
|
||
*/
|
||
post(url, data, options = {}) {
|
||
return this.request({ url, data, method: 'POST', ...options });
|
||
}
|
||
|
||
/**
|
||
* PUT 请求快捷方法
|
||
*/
|
||
put(url, data, options = {}) {
|
||
return this.request({ url, data, method: 'PUT', ...options });
|
||
}
|
||
|
||
/**
|
||
* DELETE 请求快捷方法
|
||
*/
|
||
delete(url, data, options = {}) {
|
||
return this.request({ url, data, method: 'DELETE', ...options });
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 创建全局默认实例
|
||
// ============================================================
|
||
const http = new Request();
|
||
|
||
// ============================================================
|
||
// 导出
|
||
// ============================================================
|
||
export default http;
|
||
export { Request };
|