154 lines
6.1 KiB
JavaScript
154 lines
6.1 KiB
JavaScript
class FetchWrapper {
|
||
constructor(baseURL, options = {}, contentTypeMappings = {}, onError = console.error) {
|
||
this.baseURL = baseURL;
|
||
this.contentTypeMappings = contentTypeMappings;
|
||
this.defaultContentType = 'application/json';
|
||
this.onError = onError;
|
||
this.defaultOptions = {
|
||
...{
|
||
method: 'GET',
|
||
headers: new Headers({
|
||
'Content-Type': this.defaultContentType
|
||
}),
|
||
mode: 'cors', // 指定是否允许跨域请求,默认为cors
|
||
cache: 'default', // 请求的缓存模式
|
||
credentials: 'same-origin', // 请求的凭据模式,默认同源请求时发送cookies
|
||
parseResponseHandler: {
|
||
'application/json': response => response.json(),
|
||
'text/*': response => response.text(),
|
||
'image/*': response => response.blob(),
|
||
'application/octet-stream': response => response.blob(),
|
||
'*/*': response => response.arrayBuffer(), // 默认处理程序
|
||
},
|
||
...options
|
||
}
|
||
};
|
||
}
|
||
|
||
|
||
|
||
async fetch(url, options = {}) {
|
||
// 合并默认选项与传入的options
|
||
const mergedOptions = { ...this.defaultOptions, ...options };
|
||
|
||
// 根据URL自动设置Content-Type(如果有匹配的映射)
|
||
const matchedMapping = Object.entries(this.contentTypeMappings).find(([pattern, type]) => url.endsWith(pattern));
|
||
if (matchedMapping && !mergedOptions.headers.has('Content-Type')) {
|
||
mergedOptions.headers['Content-Type'] = matchedMapping[1];
|
||
}
|
||
// 根据请求方法处理数据
|
||
if (['POST', 'PUT', 'PATCH'].includes(mergedOptions.method)) {
|
||
if (typeof mergedOptions.body === 'object') {
|
||
mergedOptions.body = JSON.stringify(mergedOptions.body);
|
||
// 如果Content-Type未设置或默认为application/json,则需要在这里设置
|
||
if (!mergedOptions.headers.has('Content-Type')) {
|
||
mergedOptions.headers.set('Content-Type', 'application/json');
|
||
}
|
||
} else if (mergedOptions.body !== null && typeof mergedOptions.body !== 'string') {
|
||
throw new Error('非GET请求时, body必须是一个对象或字符串');
|
||
}
|
||
} else if (mergedOptions.method === 'GET' && mergedOptions.body) {
|
||
console.warn('GET请求不支持发送请求体, 已忽略提供的body参数');
|
||
delete mergedOptions.body;
|
||
}
|
||
|
||
url = new URL(url, this.baseURL).href;
|
||
|
||
return fetch(url, mergedOptions)
|
||
.then(async response => {
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
// console.warn(`HTTP warning! status: ${response.status}`);
|
||
}
|
||
|
||
const contentType = response.headers.get('content-type').split(';')[0].trim();
|
||
const parseResponseHandler = this.getParseResponseHandler(contentType);
|
||
|
||
const responseData = await parseResponseHandler(response);
|
||
return formatResponse(response, responseData);
|
||
})
|
||
.catch(error => {
|
||
error.options = mergedOptions;
|
||
error.url = url;
|
||
error.timestamp = Date.now();
|
||
error.onFunction = 'fetch';
|
||
this.onError(error.message);
|
||
throw error;
|
||
});
|
||
}
|
||
|
||
getParseResponseHandler(contentType) {
|
||
if (contentType.startsWith('text/')) {
|
||
return this.defaultOptions.parseResponseHandler['text/*'] || this.defaultOptions.parseResponseHandler['*/*'];
|
||
}
|
||
if (contentType.startsWith('image/')) {
|
||
return this.defaultOptions.parseResponseHandler['image/*'] || this.defaultOptions.parseResponseHandler['*/*'];
|
||
}
|
||
return this.defaultOptions.parseResponseHandler[contentType] || this.defaultOptions.parseResponseHandler['*/*'];
|
||
}
|
||
|
||
async fetchWithRetry(url, options = {}, maxRetries = 3, retryDelayBaseMs = 1000) {
|
||
let remainingAttempts = maxRetries;
|
||
const delays = [];
|
||
|
||
for (let i = 0; i < maxRetries; i++) {
|
||
delays.push(retryDelayBaseMs * Math.pow(2, i));
|
||
}
|
||
|
||
const attemptFetch = async (retryIndex = 0) => {
|
||
try {
|
||
return await this.fetch(url, options);
|
||
} catch (error) {
|
||
if (remainingAttempts > 1) {
|
||
console.log(`请求失败,剩余重试次数:${remainingAttempts - 1}`);
|
||
setTimeout(() => attemptFetch(retryIndex + 1), delays[retryIndex]);
|
||
remainingAttempts--;
|
||
return;
|
||
} else {
|
||
this.onError(error.message);
|
||
throw error;
|
||
}
|
||
}
|
||
};
|
||
|
||
return attemptFetch();
|
||
}
|
||
|
||
// 可以为不同的HTTP方法提供便捷的方法,如:
|
||
async post(url, body, options = {}) {
|
||
return this.fetch(url, { ...options, method: 'POST', body });
|
||
}
|
||
|
||
async get(url, options = {}) {
|
||
return this.fetch(url, { ...options, method: 'GET' });
|
||
}
|
||
|
||
async put(url, body, options = {}) {
|
||
return this.fetch(url, { ...options, method: 'PUT', body });
|
||
}
|
||
|
||
async patch(url, body, options = {}) {
|
||
return this.fetch(url, { ...options, method: 'PATCH', body });
|
||
}
|
||
|
||
async delete(url, options = {}) {
|
||
return this.fetch(url, { ...options, method: 'DELETE' });
|
||
}
|
||
};
|
||
|
||
function formatResponse(response, data) {
|
||
return {
|
||
status: response.status,
|
||
message: response.statusText,
|
||
data,
|
||
};
|
||
}
|
||
|
||
// 检查是否在Node.js环境中
|
||
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
|
||
// Node.js环境,使用CommonJS
|
||
module.exports = FetchWrapper;
|
||
} else if (typeof window !== 'undefined') {
|
||
// 浏览器环境,直接暴露到全局作用域(不推荐,但简单示例)
|
||
// window.FetchWrapper = FetchWrapper;
|
||
} |