// Layui 轻量级请求模块(基于 Fetch API + layui layer) // 支持文件上传 // // 使用示例: // layui.use(['http'], function(){ var req = layui.request; req.post('/api/upload', formData).then(...).catch(...) }); layui.define(["layer", "setter"], function (exports) { "use strict"; var setter = layui.setter, layer = layui.layer; let isRefreshing = false; let refreshQueue = []; let loadIndex; // 默认配置 const defaultConfig = { baseURL: '', timeout: 30000, headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, withCredentials: true, }; /** * 刷新 Tokens * @returns {Promise} */ async function refreshToken() { const { refresh_token } = setter.getTokens(); if (!refresh_token) { throw new Error("No refresh token"); } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), defaultConfig.timeout); try { var Accept = defaultConfig.headers['Accept'] || 'application/json'; const response = await fetch(defaultConfig.baseURL + "/api/refresh", { method: "POST", headers: { 'Content-Type': 'application/json', 'Accept': Accept, ...defaultConfig.headers, }, credentials: defaultConfig.withCredentials ? 'include' : 'same-origin', signal: controller.signal, body: JSON.stringify({ refresh_token }), }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const res = await response.json(); if (res.code === 200) { setter.saveTokens({ access_token: res.access || res.access_token, refresh_token: res.refresh || res.refresh_token || refresh_token, }); return res.access || res.access_token; } else { throw new Error(res.msg || "Refresh failed"); } } catch (error) { if (error.name === 'AbortError') { throw new Error('Request timeout'); } throw error; } } /** * 重试请求 * @param {Object} options - fetch 配置选项 * @returns {Promise} */ async function retryRequest(options) { const { access_token } = setter.getTokens(); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), options.timeout || defaultConfig.timeout); try { // 构建请求头 const headers = { ...defaultConfig.headers, ...options.headers }; const { access_token: newToken } = setter.getTokens(); if (newToken) { headers['Authorization'] = `Bearer ${newToken}`; } // 如果是 FormData,删除 Content-Type 让浏览器自动设置 if (options.data instanceof FormData) { delete headers['Content-Type']; } const fetchOptions = { method: options.method || 'GET', headers, credentials: defaultConfig.withCredentials ? 'include' : 'same-origin', signal: controller.signal, }; // 添加 body(如果不是 GET/HEAD 请求) if (options.method !== 'GET' && options.method !== 'HEAD') { fetchOptions.body = options.data instanceof FormData ? options.data : JSON.stringify(options.data); } const response = await fetch(options.url, fetchOptions); clearTimeout(timeoutId); // 处理响应头中的 token const authHeader = response.headers.get('Authorization'); if (authHeader && authHeader.startsWith('Bearer ')) { const newToken = authHeader.substring(7); setter.saveTokens({ access_token: newToken }); } let data; const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { data = await response.json(); } else if (contentType && contentType.includes('text/')) { data = await response.text(); } else { data = await response.blob(); } if (!response.ok) { const error = new Error(response.statusText || 'Request failed'); error.status = response.status; error.data = data; throw error; } return data; } catch (error) { if (error.name === 'AbortError') { error.message = 'Request timeout'; } throw error; } } /** * Fetch 请求核心方法 * @param {Object} options - fetch 配置选项 * @returns {Promise} */ async function _Request(options) { const { access_token } = setter.getTokens(); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), options.timeout || defaultConfig.timeout); try { // 构建请求头 const headers = { ...defaultConfig.headers, ...options.headers }; if (access_token) { headers['Authorization'] = `Bearer ${access_token}`; } // 如果是 FormData,删除 Content-Type 让浏览器自动设置 if (options.data instanceof FormData) { delete headers['Content-Type']; } const fetchOptions = { method: options.method || 'GET', headers, credentials: defaultConfig.withCredentials ? 'include' : 'same-origin', signal: controller.signal, }; // 添加 body(如果不是 GET/HEAD 请求) if (options.method !== 'GET' && options.method !== 'HEAD') { fetchOptions.body = options.data instanceof FormData ? options.data : JSON.stringify(options.data); } const response = await fetch(options.url, fetchOptions); clearTimeout(timeoutId); // 处理响应头中的 token const authHeader = response.headers.get('Authorization'); if (authHeader && authHeader.startsWith('Bearer ')) { const newToken = authHeader.substring(7); setter.saveTokens({ access_token: newToken }); } let data; const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { data = await response.json(); } else if (contentType && contentType.includes('text/')) { data = await response.text(); } else { data = await response.blob(); } // 检查响应数据中的 token if (data && (data.access_token || data.access)) { setter.saveTokens({ access_token: data.access_token || data.access, refresh_token: data.refresh_token || data.refresh, }); } if (!response.ok) { if (response.status === 401 && !options._retry) { // 401 错误,尝试刷新 token return new Promise((resolve, reject) => { refreshQueue.push({ resolve, reject, options }); if (!isRefreshing) { isRefreshing = true; refreshToken() .then(() => { const queue = [...refreshQueue]; refreshQueue = []; queue.forEach(({ resolve, reject, options }) => { options._retry = true; retryRequest(options) .then(resolve) .catch(reject); }); isRefreshing = false; }) .catch((refreshError) => { refreshQueue.forEach(({ reject }) => reject(refreshError)); refreshQueue = []; isRefreshing = false; // 清除本地存储并跳转登录 localStorage.clear(); layer.msg("登录已过期,请重新登录", { icon: 2, time: 2000 }); setTimeout(() => { window.top.location.href = "./login.html"; // if (window === window.parent) { // window.location.href = "./login.html"; // } else { // window.parent.location.href = "./login.html"; // } }, 2000); reject(refreshError); }); } }); } else { const error = new Error(response.statusText || 'Request failed'); error.status = response.status; error.data = data; throw error; } } return data; } catch (error) { if (error.name === 'AbortError') { error.message = 'Request timeout'; } throw error; } }; async function setAccept(accept = "json") { switch (accept) { case 'xml': defaultConfig.headers['Accept'] = `application/xml`; break; case 'html': defaultConfig.headers['Accept'] = `text/html`; break; default: defaultConfig.headers['Accept'] = `application/json`; break; } } // 暴露接口 var http = { /** * 配置请求默认参数 * @param {Object} cfg - 配置对象 * @returns {Object} 当前配置 */ config: function (cfg) { Object.assign(defaultConfig, cfg || {}); return defaultConfig; }, /** * 通用请求方法 * @param {Object} options - fetch 配置选项 * @returns {Promise} */ request: _Request, /** * GET 请求 * @param {string} url - 请求地址 * @param {Object} params - URL 参数 * @param {Object} options - fetch 配置选项 * @returns {Promise} */ get: function (url, params, options = {}) { // 处理 URL 参数 if (params && Object.keys(params).length > 0) { const queryString = new URLSearchParams(params).toString(); url += (url.includes('?') ? '&' : '?') + queryString; } options.method = "GET"; options.url = defaultConfig.baseURL + url; return _Request(options); }, /** * POST 请求 * @param {string} url - 请求地址 * @param {Object|FormData} data - 请求数据或 FormData 对象 * @param {Object} options - fetch 配置选项 * @returns {Promise} */ post: function (url, data, options = {}) { options.method = "POST"; options.url = defaultConfig.baseURL + url; options.data = data; return _Request(options); }, /** * PUT 请求 * @param {string} url - 请求地址 * @param {Object|FormData} data - 请求数据或 FormData 对象 * @param {Object} options - fetch 配置选项 * @returns {Promise} */ put: function (url, data, options = {}) { options.method = "PUT"; options.url = defaultConfig.baseURL + url; options.data = data; return _Request(options); }, /** * DELETE 请求 * @param {string} url - 请求地址 * @param {Object} params - URL 参数 * @param {Object} options - fetch 配置选项 * @returns {Promise} */ delete: function (url, params, options = {}) { // 处理 URL 参数 if (params && Object.keys(params).length > 0) { const queryString = new URLSearchParams(params).toString(); url += (url.includes('?') ? '&' : '?') + queryString; } options.method = "DELETE"; options.url = defaultConfig.baseURL + url; return _Request(options); }, /** * 上传文件(简化版) * @param {string} url - 上传地址 * @param {File|File[]} files - 文件对象或文件数组 * @param {Object} params - 其他参数 * @param {Object} options - fetch 配置选项 * @returns {Promise} */ upload: function (url, files, params = {}, options = {}) { const formData = new FormData(); // 添加文件 if (Array.isArray(files)) { files.forEach((file, index) => { formData.append(`file${index}`, file); }); } else if (files) { formData.append('file', files); } // 添加其他参数 Object.keys(params).forEach(key => { formData.append(key, params[key]); }); options.method = "POST"; options.url = defaultConfig.baseURL + url; options.data = formData; return _Request(options); }, /** * 刷新 token * @returns {Promise} */ refreshToken: refreshToken, accept: setAccept, }; exports("http", http); });