超时+无感刷新token+单一实例的FetchManager
# github link
# 实现思路
超时机制
无感刷新 token
单一实例机制
# 实现代码
点击 展开/收起 FetchManager 代码
import qs from 'query-string';
class FetchManager {
constructor() {
this.isRefreshing = false; // 是否正在刷新 token
this.pendingRequests = []; // 等待的请求队列
this.defaultHeaders = {
'Content-Type': 'application/x-www-form-urlencoded',
};
}
async fetchRequest(url, options = {}) {
this.url = url;
this.timeout = options.timeout || 600 * 1000; // 超时时间 10分钟
this.method = options.method || 'GET';
this.headers = { ...this.defaultHeaders, ...options.headers };
this.body = qs.stringify(options.body) || null;
this.signal = options.signal || null;
// 错误处理
this.onError =
options.onError || ((errorMessage) => console.error(errorMessage));
// 警告处理
this.onWarning =
options.onWarning || ((warnMessage) => console.warn(warnMessage));
// 重新登录
this.onLogin = options.onLogin || function () {};
try {
return await this.execute();
} catch (error) {
if (error.message === '401 Unauthorized') {
const newToken = await this.refreshToken();
this.headers.Authorization = `Bearer ${newToken}`;
return this.execute(); // 重新执行请求
}
throw error;
}
}
// 带超时机制的请求方法
async fetchWithTimeout() {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new Error('Request timed out'));
}, this.timeout);
fetch(this.url, {
method: this.method,
headers: this.headers,
body: this.body,
signal: this.signal,
})
.then((response) => {
clearTimeout(timeoutId);
if (response.status === 401) {
reject(new Error('401 Unauthorized'));
} else {
resolve(response);
}
})
.catch((error) => {
clearTimeout(timeoutId);
reject(error);
});
});
}
// 执行请求
async execute() {
try {
const jwt = localStorage.getItem('quantanalysis_jwt');
if (jwt) {
this.headers.Authorization = `Bearer ${jwt}`;
}
const response = await this.fetchWithTimeout();
const responseStr = await response.text();
const data = JSON.parse(responseStr || '{}');
if (data?.error) {
this.onError(data.error);
return {};
}
if (data?.warning) {
this.onWarning(data.warning);
return {};
}
return data;
} catch (error) {
this.handleRequestError(error);
if (error.message === 'Request timed out') {
this.onError('请求超时:', error);
} else {
this.onError('请求错误:', error);
}
return {};
}
}
// 刷新 token 的方法
async refreshToken() {
if (this.isRefreshing) {
return new Promise((resolve, reject) => {
this.pendingRequests.push({ resolve, reject });
});
}
this.isRefreshing = true;
try {
const jwtLong = localStorage.getItem('quantanalysis_jwt_long');
if (!jwtLong) {
throw new Error('长jwt找不到,无法刷新token');
}
const response = await fetch('/mysql/refreshToken', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${jwtLong}`, // 携带长期 token
},
});
const data = await response.json();
if (!data.ok) {
throw new Error('刷新 token 失败');
}
if (data?.token) {
localStorage.setItem('quantanalysis_jwt', data.token);
this.headers.Authorization = `Bearer ${data.token}`;
this.isRefreshing = false;
// 重新执行所有挂起的请求,这里是将第一个阻塞 refreshToken 的请求拿到的结果直接返回出去
// 这样其他请求就不需要真的去执行 refreshToken 发起 fetch 请求拿到 token 了
this.pendingRequests.forEach(({ resolve }) => resolve(data.token));
this.pendingRequests = [];
return data.token;
} else {
throw new Error('刷新 token 失败, 返回结果有误');
}
} catch (error) {
this.isRefreshing = false;
this.pendingRequests.forEach(({ reject }) => reject(error));
this.pendingRequests = [];
localStorage.removeItem('quantanalysis_jwt');
localStorage.removeItem('quantanalysis_jwt_long');
// 重新登录,这里可以写页面跳转之类的,外部传入即可
this.onLogin();
throw error;
}
}
}
// 导出单一实例
export default new FetchManager();
点击 展开/收起 使用示例 代码
// 请求接口的方法
export const fetchRequest = async (url, options = {}) => {
return await fetchManager.fetchRequest(url, {
...options,
method: options.method,
body: options.body,
timeout: 10000,
onWarning: (warningMessage) => {
message.warning({ content: warningMessage, key: String(warningMessage) });
},
onError: (errorMessage) => {
message.error({ content: errorMessage, key: String(errorMessage) });
},
onLogin: () => {
browserHistory.push('/login');
},
});
};
// 判断 nickId 是否正确进入过 room
const { success } = await fetchRequest('/mysql/validateNickIdByRoomId', {
method: 'post',
body: {
roomId,
nickId,
},
});
const { data } = await fetchRequest('/mysql/getLoginLog', {
method: 'get',
});