422 lines
12 KiB
JavaScript
422 lines
12 KiB
JavaScript
// 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<string>}
|
||
*/
|
||
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<string>}
|
||
*/
|
||
refreshToken: refreshToken,
|
||
|
||
accept: setAccept,
|
||
|
||
};
|
||
|
||
exports("http", http);
|
||
}); |