baimacms/scratch/test_request.mjs

258 lines
6.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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.dataCMS 标准格式)
* - 不存在则取 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 };