You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

301 lines
8.4 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* Shopro-request
* @description api模块管理loading配置请求拦截错误处理
*/
import Request from 'luch-request';
import { apiPath, baseUrl, tenantId } from '@/sheep/config';
import $store from '@/sheep/store';
import $platform from '@/sheep/platform';
import { showAuthModal } from '@/sheep/hooks/useModal';
import AuthUtil from '@/sheep/api/member/auth';
import { getTerminal } from '@/sheep/util/const';
const options = {
// 显示操作成功消息 默认不显示
showSuccess: false,
// 成功提醒 默认使用后端返回值
successMsg: '',
// 显示失败消息 默认显示
showError: true,
// 失败提醒 默认使用后端返回信息
errorMsg: '',
// 显示请求时loading模态框 默认显示
showLoading: true,
// loading提醒文字
loadingMsg: '加载中',
// 需要授权才能请求 默认放开
auth: false,
// ...
};
// Loading全局实例
let LoadingInstance = {
target: null,
count: 0,
};
/**
* 关闭loading
*/
function closeLoading() {
if (LoadingInstance.count > 0) LoadingInstance.count--;
if (LoadingInstance.count === 0) uni.hideLoading();
}
/**
* @description 请求基础配置 可直接使用访问自定义请求
*/
const http = new Request({
baseURL: baseUrl + apiPath,
timeout: 8000,
method: 'GET',
header: {
Accept: 'text/json',
'Content-Type': 'application/json;charset=UTF-8',
platform: $platform.name,
},
// #ifdef APP-PLUS
sslVerify: false,
// #endif
// #ifdef H5
// 跨域请求时是否携带凭证cookies仅H5支持HBuilderX 2.6.15+
withCredentials: false,
// #endif
custom: options,
});
/**
* @description 请求拦截器
*/
http.interceptors.request.use(
(config) => {
// 自定义处理【auth 授权】:必须登录的接口,则跳出 AuthModal 登录弹窗
if (config.custom.auth && !$store('user').isLogin) {
showAuthModal();
return Promise.reject();
}
// 自定义处理【loading 加载中】:如果需要显示 loading则显示 loading
if (config.custom.showLoading) {
LoadingInstance.count++;
LoadingInstance.count === 1 &&
uni.showLoading({
title: config.custom.loadingMsg,
mask: true,
fail: () => {
uni.hideLoading();
},
});
}
// 增加 token 令牌、terminal 终端、tenant 租户的请求头
const token = getAccessToken();
if (token) {
config.header['Authorization'] = token;
}
config.header['terminal'] = getTerminal();
config.header['Accept'] = '*/*';
config.header['tenant-id'] = tenantId;
return config;
},
(error) => {
return Promise.reject(error);
},
);
/**
* @description 响应拦截器
*/
http.interceptors.response.use(
(response) => {
// 约定:如果是 /auth/ 下的 URL 地址,并且返回了 accessToken 说明是登录相关的接口,则自动设置登陆令牌
if (response.config.url.indexOf('/member/auth/') >= 0 && response.data?.data?.accessToken) {
$store('user').setToken(response.data.data.accessToken, response.data.data.refreshToken);
}
// 自定处理【loading 加载中】:如果需要显示 loading则关闭 loading
response.config.custom.showLoading && closeLoading();
// 自定义处理【error 错误提示】:如果需要显示错误提示,则显示错误提示
if (response.data.code !== 0) {
// 特殊:如果 401 错误码,则跳转到登录页 or 刷新令牌
if (response.data.code === 401) {
// return refreshToken(response.config);
}
// 错误提示
if (response.config.custom.showError) {
uni.showToast({
title: response.data.msg || '服务器开小差啦,请稍后再试~',
icon: 'none',
mask: true,
});
}
}
// 自定义处理【showSuccess 成功提示】:如果需要显示成功提示,则显示成功提示
if (response.config.custom.showSuccess
&& response.config.custom.successMsg !== ''
&& response.data.code === 0) {
uni.showToast({
title: response.config.custom.successMsg,
icon: 'none',
});
}
// 返回结果:包括 code + data + msg
return Promise.resolve(response.data);
},
(error) => {
const userStore = $store('user');
const isLogin = userStore.isLogin;
let errorMessage = '网络请求出错';
if (error !== undefined) {
switch (error.statusCode) {
case 400:
errorMessage = '请求错误';
break;
case 401:
errorMessage = isLogin ? '您的登陆已过期' : '请先登录';
// 正常情况下,后端不会返回 401 错误,所以这里不处理 handleAuthorized
break;
case 403:
errorMessage = '拒绝访问';
break;
case 404:
errorMessage = '请求出错';
break;
case 408:
errorMessage = '请求超时';
break;
case 429:
errorMessage = '请求频繁, 请稍后再访问';
break;
case 500:
errorMessage = '服务器开小差啦,请稍后再试~';
break;
case 501:
errorMessage = '服务未实现';
break;
case 502:
errorMessage = '网络错误';
break;
case 503:
errorMessage = '服务不可用';
break;
case 504:
errorMessage = '网络超时';
break;
case 505:
errorMessage = 'HTTP 版本不受支持';
break;
}
if (error.errMsg.includes('timeout')) errorMessage = '请求超时';
// #ifdef H5
if (error.errMsg.includes('Network'))
errorMessage = window.navigator.onLine ? '服务器异常' : '请检查您的网络连接';
// #endif
}
if (error && error.config) {
if (error.config.custom.showError === false) {
uni.showToast({
title: error.data?.msg || errorMessage,
icon: 'none',
mask: true,
});
}
error.config.custom.showLoading && closeLoading();
}
return false;
},
);
// Axios 无感知刷新令牌,参考 https://www.dashingdog.cn/article/11 与 https://segmentfault.com/a/1190000020210980 实现
let requestList = [] // 请求队列
let isRefreshToken = false // 是否正在刷新中
const refreshToken = async (config) => {
// 如果当前已经是 refresh-token 的 URL 地址,并且还是 401 错误,说明是刷新令牌失败了,直接返回 Promise.reject(error)
if (config.url.indexOf('/member/auth/refresh-token') >= 0) {
return Promise.reject('error')
}
// 如果未认证,并且未进行刷新令牌,说明可能是访问令牌过期了
if (!isRefreshToken) {
isRefreshToken = true
// 1. 如果获取不到刷新令牌,则只能执行登出操作
const refreshToken = getRefreshToken()
if (!refreshToken) {
return handleAuthorized()
}
// 2. 进行刷新访问令牌
try {
const refreshTokenResult = await AuthUtil.refreshToken(refreshToken);
if (refreshTokenResult.code !== 0) {
// 如果刷新不成功,直接抛出 e 触发 2.2 的逻辑
// noinspection ExceptionCaughtLocallyJS
throw new Error('刷新令牌失败');
}
// 2.1 刷新成功,则回放队列的请求 + 当前请求
config.header.Authorization = 'Bearer ' + getAccessToken()
requestList.forEach((cb) => {
cb()
})
requestList = []
return request(config)
} catch (e) {
// 为什么需要 catch 异常呢?刷新失败时,请求因为 Promise.reject 触发异常。
// 2.2 刷新失败,只回放队列的请求
requestList.forEach((cb) => {
cb()
})
// 提示是否要登出。即不回放当前请求!不然会形成递归
return handleAuthorized()
} finally {
requestList = []
isRefreshToken = false
}
} else {
// 添加到队列,等待刷新获取到新的令牌
return new Promise((resolve) => {
requestList.push(() => {
config.header.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token 请根据实际情况自行修改
resolve(request(config))
})
})
}
}
/**
* 处理 401 未登录的错误
*/
const handleAuthorized = () => {
const userStore = $store('user');
userStore.logout(true);
showAuthModal();
// 登录超时
return Promise.reject({
code: 401,
msg: userStore.isLogin ? '您的登陆已过期' : '请先登录'
})
}
/** 获得访问令牌 */
const getAccessToken = () => {
return uni.getStorageSync('token');
}
/** 获得刷新令牌 */
const getRefreshToken = () => {
return uni.getStorageSync('refreshToken');
}
const request = (config) => {
return http.middleware(config);
};
export default request;