/** * uni-app 网络请求封装 * 特性:请求/响应拦截、统一错误处理、超时控制、请求取消、Token 自动携带、环境切换 * 适配端:小程序、H5、App */ // ======================== 基础配置 ======================== // 环境配置(根据 NODE_ENV 切换 baseURL) const env = process.env.NODE_ENV || 'development'; const baseURLMap = { development: 'http://192.168.110.235:1860', // 开发环境 production: '', // 生产环境 test: 'http://192.168.110.235:1860' // 测试环境 }; // 默认基础请求地址 const DEFAULT_BASE_URL = baseURLMap[env]; const DEFAULT_BASE_URL_IMG = 'http://47.107.107.47:9000/zksy-file/'; // 默认请求配置 const DEFAULT_CONFIG = { baseURL: DEFAULT_BASE_URL, timeout: 10000, // 全局默认超时时间(10秒) header: { 'Content-Type': 'application/json;charset=UTF-8' // 默认请求头 }, showLoading: true, // 是否显示加载中提示(全局默认开启) loadingText: '加载中...', // 加载提示文字 withToken: true, // 是否自动携带 Token(全局默认开启) cancelRepeat: true // 是否取消重复请求(全局默认开启) }; // 存储请求取消令牌(key: 请求唯一标识,value: 取消函数) const cancelTokenMap = new Map(); // ======================== 工具函数 ======================== /** * 生成请求唯一标识 * @param {String} method - 请求方法(GET/POST 等) * @param {String} url - 请求地址 * @param {Object} data - 请求参数 * @returns {String} 请求唯一标识 */ function generateRequestKey(method, url, data) { return `${method.toUpperCase()}-${url}-${JSON.stringify(data)}`; } /** * 统一提示方法(兼容不同端的提示样式) * @param {String} msg - 提示文字 * @param {String} type - 提示类型(none/success/error) */ function showToast(msg, type = 'none') { uni.showToast({ title: msg, icon: type, duration: 2000, mask: true // 显示蒙层,防止穿透点击 }); } // ======================== 核心拦截逻辑 ======================== /** * 请求拦截器:处理请求前的统一逻辑 * @param {Object} header - 原始请求头 * @param {Boolean} withToken - 是否携带 Token * @returns {Object} 处理后的请求头 */ function requestInterceptor(header, withToken) { const finalHeader = { ...header }; // 自动携带 Token(从本地缓存读取) if (withToken) { const token = uni.getStorageSync('token'); if (token) { finalHeader.Authorization = `Bearer ${token}`; // 适配 JWT Token 格式 } } return finalHeader; } /** * 响应拦截器:处理响应后的统一逻辑 * @param {Object} response - 响应对象 * @returns {Promise} 处理后的结果 */ function responseInterceptor(response) { const { statusCode, data } = response; // 1. 处理 HTTP 状态码错误 if (statusCode !== 200) { let errMsg = ''; switch (statusCode) { case 401: errMsg = '登录失效,请重新登录'; // 清除 Token 并跳转到登录页(排除登录页本身) uni.removeStorageSync('token'); const pages = getCurrentPages(); const currentPage = pages[pages.length - 1]; if (currentPage.route !== 'pages/login/login') { uni.redirectTo({ url: '/pages/login/login' }); } break; case 403: errMsg = '权限不足,无法访问'; break; case 404: errMsg = '请求地址不存在'; break; case 500: errMsg = '服务器内部错误,请稍后重试'; break; default: errMsg = `请求失败(状态码:${statusCode})`; } showToast(errMsg, 'none'); return Promise.reject({ code: statusCode, msg: errMsg, raw: response }); } // 2. 处理业务状态码(根据后端约定调整) // 假设后端返回格式:{ code: 0, msg: '成功', data: {} } // const { code, msg, data: resData } = data; const { code, msg, token } = data; // 直接解析 token if (code === 200) { // 业务请求成功,返回核心数据 return Promise.resolve({ token, ...data }); // 确保返回有值 } else { // 业务请求失败,提示错误信息 showToast(msg || '操作失败', 'none'); return Promise.reject({ code, msg: msg || '操作失败', raw: data }); } } /** * 网络错误处理 * @param {Object} error - 错误对象 * @returns {Promise} 拒绝的 Promise */ function networkErrorHandler(error) { let errMsg = '网络请求失败'; switch (error.errMsg) { case 'request:fail timeout': errMsg = '请求超时,请检查网络'; break; case 'request:fail abort': errMsg = '请求已取消'; break; case 'request:fail network error': errMsg = '网络异常,请检查网络连接'; break; default: errMsg = `请求错误:${error.errMsg}`; } showToast(errMsg, 'none'); return Promise.reject({ code: -1, msg: errMsg, raw: error }); } // ======================== 核心请求方法 ======================== /** * 创建请求实例 * @param {Object} customConfig - 自定义全局配置(覆盖默认配置) * @returns {Function} 请求方法 */ function createRequest(customConfig = {}) { // 合并全局配置 const globalConfig = { ...DEFAULT_CONFIG, ...customConfig }; /** * 核心请求函数 * @param {Object} options - 单个请求配置 * @returns {Promise} 请求结果 */ const request = async (options) => { const { url, method = 'GET', data = {}, header = {}, showLoading = globalConfig.showLoading, loadingText = globalConfig.loadingText, timeout = globalConfig.timeout, withToken = globalConfig.withToken, cancelRepeat = globalConfig.cancelRepeat } = options; // 1. 验证必填参数 if (!url) { showToast('请求地址不能为空', 'none'); return Promise.reject({ code: -2, msg: '请求地址不能为空' }); } // 2. 显示加载中提示 let loadingTimer = null; if (showLoading) { // 延迟显示(避免请求过快导致提示闪烁) loadingTimer = setTimeout(() => { uni.showLoading({ title: loadingText, mask: true }); }, 200); } // 3. 处理重复请求 const requestKey = generateRequestKey(method, url, data); if (cancelRepeat && cancelTokenMap.has(requestKey)) { // 取消已存在的重复请求 cancelTokenMap.get(requestKey)('取消重复请求'); cancelTokenMap.delete(requestKey); } // 4. 创建取消令牌(用于取消请求) let cancelRequest; const cancelToken = new Promise((resolve) => { cancelRequest = resolve; cancelTokenMap.set(requestKey, resolve); }); try { // 5. 合并请求头(全局头 + 单个请求头 + 拦截器处理) const finalHeader = requestInterceptor({ ...globalConfig.header, ...header }, withToken); // 6. 发起请求(Promise 化 uni.request) const requestPromise = new Promise((resolve, reject) => { uni.request({ url: globalConfig.baseURL + url, // 拼接完整请求地址 method, data, header: finalHeader, timeout, success: resolve, fail: reject }); }); // 7. 同时监听请求和取消令牌(race: 谁先完成就执行谁) const result = await Promise.race([requestPromise, cancelToken]); // 8. 移除当前请求的取消令牌 cancelTokenMap.delete(requestKey); // 9. 关闭加载中提示 clearTimeout(loadingTimer); if (showLoading) uni.hideLoading(); // 10. 处理响应结果 return responseInterceptor(result); } catch (error) { // 11. 异常处理 cancelTokenMap.delete(requestKey); clearTimeout(loadingTimer); if (showLoading) uni.hideLoading(); // 区分是网络错误还是取消请求 if (typeof error === 'string') { // 取消请求的错误(主动取消) return Promise.reject({ code: -3, msg: error }); } else { // 网络错误(超时、断网等) return networkErrorHandler(error); } } }; // ======================== 快捷请求方法 ======================== /** * GET 请求快捷方法 * @param {String} url - 请求地址 * @param {Object} data - 请求参数 * @param {Object} options - 自定义配置 * @returns {Promise} 请求结果 */ request.get = (url, data = {}, options = {}) => { return request({ url, method: 'GET', data, ...options }); }; /** * POST 请求快捷方法 * @param {String} url - 请求地址 * @param {Object} data - 请求参数 * @param {Object} options - 自定义配置 * @returns {Promise} 请求结果 */ request.post = (url, data = {}, options = {}) => { return request({ url, method: 'POST', data, ...options }); }; /** * PUT 请求快捷方法 * @param {String} url - 请求地址 * @param {Object} data - 请求参数 * @param {Object} options - 自定义配置 * @returns {Promise} 请求结果 */ request.put = (url, data = {}, options = {}) => { return request({ url, method: 'PUT', data, ...options }); }; /** * DELETE 请求快捷方法 * @param {String} url - 请求地址 * @param {Object} data - 请求参数 * @param {Object} options - 自定义配置 * @returns {Promise} 请求结果 */ request.delete = (url, data = {}, options = {}) => { return request({ url, method: 'DELETE', data, ...options }); }; // ======================== 辅助方法 ======================== /** * 取消所有未完成的请求 * @param {String} reason - 取消原因 */ request.cancelAll = (reason = '取消所有请求') => { cancelTokenMap.forEach((cancel) => cancel(reason)); cancelTokenMap.clear(); }; /** * 获取当前全局配置 * @returns {Object} 全局配置 */ request.getConfig = () => { return { ...globalConfig }; }; /** * 更新全局配置 * @param {Object} newConfig - 新的全局配置 */ request.updateConfig = (newConfig) => { Object.assign(globalConfig, newConfig); }; return request; } // 创建默认请求实例(全局使用) const request = createRequest(); export const baseURL = DEFAULT_BASE_URL; export const imgBaseURL = DEFAULT_BASE_URL_IMG; export { baseURL as DEFAULT_BASE_URL }; export { imgBaseURL as DEFAULT_BASE_URL_IMG }; // 导出请求实例和创建方法 export default request; export { createRequest };