From b6635a5d5cfbae6e4845851765bcb6f6c9881646 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 2 Oct 2024 14:19:23 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E3=80=91WebSocket=20=E4=BD=BF=E7=94=A8=20refreshToken=20?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=EF=BC=8C=E8=A7=A3=E5=86=B3=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E5=88=B7=E6=96=B0=E8=AE=BF=E9=97=AE=E4=BB=A4=E7=89=8C=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sheep/hooks/useWebSocket.js | 25 +-- sheep/request/index.js | 348 ++++++++++++++++++------------------ 2 files changed, 184 insertions(+), 189 deletions(-) diff --git a/sheep/hooks/useWebSocket.js b/sheep/hooks/useWebSocket.js index 18316176..81bc0b39 100644 --- a/sheep/hooks/useWebSocket.js +++ b/sheep/hooks/useWebSocket.js @@ -1,6 +1,7 @@ import { onBeforeUnmount, reactive, ref } from 'vue'; import { baseUrl, websocketPath } from '@/sheep/config'; import { copyValueToTarget } from '@/sheep/util'; +import { getRefreshToken } from '@/sheep/request'; /** * WebSocket 创建 hook @@ -8,12 +9,8 @@ import { copyValueToTarget } from '@/sheep/util'; * @return {{options: *}} */ export function useWebSocket(opt) { - const getAccessToken = () => { - return uni.getStorageSync('token'); - }; - const options = reactive({ - url: (baseUrl + websocketPath).replace('http', 'ws') + '?token=' + getAccessToken(), // ws 地址 + url: (baseUrl + websocketPath).replace('http', 'ws') + '?token=' + getRefreshToken(), // ws 地址 isReconnecting: false, // 正在重新连接 reconnectInterval: 3000, // 重连间隔,单位毫秒 heartBeatInterval: 5000, // 心跳间隔,单位毫秒 @@ -22,12 +19,9 @@ export function useWebSocket(opt) { destroy: false, // 是否销毁 pingTimeout: null, // 心跳检测定时器 reconnectTimeout: null, // 重连定时器ID的属性 - onConnected: () => { - }, // 连接成功时触发 - onClosed: () => { - }, // 连接关闭时触发 - onMessage: (data) => { - }, // 收到消息 + onConnected: () => {}, // 连接成功时触发 + onClosed: () => {}, // 连接关闭时触发 + onMessage: (data) => {}, // 收到消息 }); const SocketTask = ref(null); // SocketTask 由 uni.connectSocket() 接口创建 @@ -58,7 +52,8 @@ export function useWebSocket(opt) { // 情况一:实例销毁 if (options.destroy) { options.onClosed(); - } else { // 情况二:连接失败重连 + } else { + // 情况二:连接失败重连 // 停止心跳检查 stopHeartBeat(); // 重连 @@ -140,10 +135,8 @@ export function useWebSocket(opt) { copyValueToTarget(options, opt); SocketTask.value = uni.connectSocket({ url: options.url, - complete: () => { - }, - success: () => { - }, + complete: () => {}, + success: () => {}, }); initEventListeners(); }; diff --git a/sheep/request/index.js b/sheep/request/index.js index 492f08c9..b6e4b9e0 100644 --- a/sheep/request/index.js +++ b/sheep/request/index.js @@ -12,224 +12,226 @@ 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, - // ... + // 显示操作成功消息 默认不显示 + showSuccess: false, + // 成功提醒 默认使用后端返回值 + successMsg: '', + // 显示失败消息 默认显示 + showError: true, + // 失败提醒 默认使用后端返回信息 + errorMsg: '', + // 显示请求时loading模态框 默认显示 + showLoading: true, + // loading提醒文字 + loadingMsg: '加载中', + // 需要授权才能请求 默认放开 + auth: false, + // ... }; // Loading全局实例 let LoadingInstance = { - target: null, - count: 0, + target: null, + count: 0, }; /** * 关闭loading */ function closeLoading() { - if (LoadingInstance.count > 0) LoadingInstance.count--; - if (LoadingInstance.count === 0) uni.hideLoading(); + 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, + 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) => { + (config) => { // 自定义处理【auth 授权】:必须登录的接口,则跳出 AuthModal 登录弹窗 - if (config.custom.auth && !$store('user').isLogin) { - showAuthModal(); - return Promise.reject(); - } + 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(); - }, - }); - } + 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(); + 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); - }, + 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); - } + (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(); + response.config.custom.showLoading && closeLoading(); // 自定义处理【error 错误提示】:如果需要显示错误提示,则显示错误提示 - if (response.data.code !== 0) { + 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, - }); + 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) { + // 自定义处理【showSuccess 成功提示】:如果需要显示成功提示,则显示成功提示 + if ( + response.config.custom.showSuccess && + response.config.custom.successMsg !== '' && + response.data.code === 0 + ) { uni.showToast({ - title: response.config.custom.successMsg, - icon: 'none', - }); - } + 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: + 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 - } + 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(); - } + 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; - }, + return false; + }, ); // Axios 无感知刷新令牌,参考 https://www.dashingdog.cn/article/11 与 https://segmentfault.com/a/1190000020210980 实现 -let requestList = [] // 请求队列 -let isRefreshToken = false // 是否正在刷新中 +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') + return Promise.reject('error'); } // 如果未认证,并且未进行刷新令牌,说明可能是访问令牌过期了 if (!isRefreshToken) { - isRefreshToken = true + isRefreshToken = true; // 1. 如果获取不到刷新令牌,则只能执行登出操作 - const refreshToken = getRefreshToken() + const refreshToken = getRefreshToken(); if (!refreshToken) { - return handleAuthorized() + return handleAuthorized(); } // 2. 进行刷新访问令牌 try { @@ -240,34 +242,34 @@ const refreshToken = async (config) => { throw new Error('刷新令牌失败'); } // 2.1 刷新成功,则回放队列的请求 + 当前请求 - config.header.Authorization = 'Bearer ' + getAccessToken() + config.header.Authorization = 'Bearer ' + getAccessToken(); requestList.forEach((cb) => { - cb() - }) - requestList = [] - return request(config) + cb(); + }); + requestList = []; + return request(config); } catch (e) { // 为什么需要 catch 异常呢?刷新失败时,请求因为 Promise.reject 触发异常。 // 2.2 刷新失败,只回放队列的请求 requestList.forEach((cb) => { - cb() - }) + cb(); + }); // 提示是否要登出。即不回放当前请求!不然会形成递归 - return handleAuthorized() + return handleAuthorized(); } finally { - requestList = [] - isRefreshToken = false + requestList = []; + isRefreshToken = false; } } else { // 添加到队列,等待刷新获取到新的令牌 return new Promise((resolve) => { requestList.push(() => { - config.header.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token 请根据实际情况自行修改 - resolve(request(config)) - }) - }) + config.header.Authorization = 'Bearer ' + getAccessToken(); // 让每个请求携带自定义token 请根据实际情况自行修改 + resolve(request(config)); + }); + }); } -} +}; /** * 处理 401 未登录的错误 @@ -279,22 +281,22 @@ const handleAuthorized = () => { // 登录超时 return Promise.reject({ code: 401, - msg: userStore.isLogin ? '您的登陆已过期' : '请先登录' - }) -} + msg: userStore.isLogin ? '您的登陆已过期' : '请先登录', + }); +}; /** 获得访问令牌 */ -const getAccessToken = () => { +export const getAccessToken = () => { return uni.getStorageSync('token'); -} +}; /** 获得刷新令牌 */ -const getRefreshToken = () => { +export const getRefreshToken = () => { return uni.getStorageSync('refresh-token'); -} +}; const request = (config) => { - return http.middleware(config); + return http.middleware(config); }; export default request;