/** * 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 };