Files

422 lines
12 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.
// 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);
});