From 6d84ede6acbbe59fd1bfd38c4c483f94e88c8f66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E9=95=87?= Date: Thu, 17 Oct 2024 16:47:23 +0800 Subject: [PATCH] feat: optimistic subpackage `@sa/alova` --- packages/alova/package.json | 5 +- packages/alova/src/fetch.ts | 2 + packages/alova/src/mock.ts | 1 + pnpm-lock.yaml | 18 +++ src/serviceAlova/api/auth.ts | 56 +++++++++ src/serviceAlova/api/index.ts | 2 + src/serviceAlova/api/route.ts | 20 +++ .../mocks/feature-users-20241014.ts | 56 +++++++++ src/serviceAlova/request/index.ts | 115 ++++++++++++++++++ src/serviceAlova/request/shared.ts | 53 ++++++++ src/serviceAlova/request/type.ts | 4 + 11 files changed, 331 insertions(+), 1 deletion(-) create mode 100644 packages/alova/src/fetch.ts create mode 100644 packages/alova/src/mock.ts create mode 100644 src/serviceAlova/api/auth.ts create mode 100644 src/serviceAlova/api/index.ts create mode 100644 src/serviceAlova/api/route.ts create mode 100644 src/serviceAlova/mocks/feature-users-20241014.ts create mode 100644 src/serviceAlova/request/index.ts create mode 100644 src/serviceAlova/request/shared.ts create mode 100644 src/serviceAlova/request/type.ts diff --git a/packages/alova/package.json b/packages/alova/package.json index a6d3fb15..19942b6e 100644 --- a/packages/alova/package.json +++ b/packages/alova/package.json @@ -3,7 +3,9 @@ "version": "0.1.0", "exports": { ".": "./src/index.ts", - "./client": "./src/client.ts" + "./fetch": "./src/fetch.ts", + "./client": "./src/client.ts", + "./mock": "./src/mock.ts" }, "typesVersions": { "*": { @@ -11,6 +13,7 @@ } }, "dependencies": { + "@alova/mock": "^2.0.7", "@sa/utils": "workspace:*", "alova": "3.0.20" } diff --git a/packages/alova/src/fetch.ts b/packages/alova/src/fetch.ts new file mode 100644 index 00000000..8511ce46 --- /dev/null +++ b/packages/alova/src/fetch.ts @@ -0,0 +1,2 @@ +import adapterFetch from 'alova/fetch'; +export default adapterFetch; diff --git a/packages/alova/src/mock.ts b/packages/alova/src/mock.ts new file mode 100644 index 00000000..f3aaf087 --- /dev/null +++ b/packages/alova/src/mock.ts @@ -0,0 +1 @@ +export * from '@alova/mock'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ba61d7b..7cd6784c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -162,6 +162,9 @@ importers: packages/alova: dependencies: + '@alova/mock': + specifier: ^2.0.7 + version: 2.0.7(alova@3.0.20) '@sa/utils': specifier: workspace:* version: link:../utils @@ -284,9 +287,17 @@ importers: packages: + '@alova/mock@2.0.7': + resolution: {integrity: sha512-4W8Ncsmj7cdjzZk7f2zFqc32aoYQNoDJS3z7W0nqAkTJ7KR8ZiGaHA5dJovyXnLphmTeyWS3yHMVWnesI7y4ig==} + peerDependencies: + alova: ^3.0.20 + '@alova/shared@1.0.5': resolution: {integrity: sha512-/a2Qm+xebQJ1OlIgpslK+UL1J7yhkt1/Mqdq58a22+fSVdANukmUcF4j4w1DF3lxZ04SrqP+2oJprJ8UOvM+9Q==} + '@alova/shared@1.0.6': + resolution: {integrity: sha512-W89j64InjFIsW/u5YmYvpXGWz8JerBAYWyu/Fc7xfc5B+95SSA3ybW4nyHacBUW6yYQyGZwa8S8bVPePqa7bmA==} + '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} @@ -4224,8 +4235,15 @@ packages: snapshots: + '@alova/mock@2.0.7(alova@3.0.20)': + dependencies: + '@alova/shared': 1.0.6 + alova: 3.0.20 + '@alova/shared@1.0.5': {} + '@alova/shared@1.0.6': {} + '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.5 diff --git a/src/serviceAlova/api/auth.ts b/src/serviceAlova/api/auth.ts new file mode 100644 index 00000000..e484ad2a --- /dev/null +++ b/src/serviceAlova/api/auth.ts @@ -0,0 +1,56 @@ +import { alova } from '../request'; + +/** + * Login + * + * @param userName User name + * @param password Password + */ +export function fetchLogin(userName: string, password: string) { + return alova.Post('/auth/login', { userName, password }); +} + +/** Get user info */ +export function fetchGetUserInfo() { + return alova.Get('/auth/getUserInfo'); +} + +/** Send captcha to target phone */ +export function sendCaptcha(phone: string) { + return alova.Post('/auth/sendCaptcha', { phone }); +} + +/** Verify captcha */ +export function verifyCaptcha(phone: string, code: string) { + return alova.Post('/auth/verifyCaptcha', { phone, code }); +} + +/** + * Refresh token + * + * @param refreshToken Refresh token + */ +export function fetchRefreshToken(refreshToken: string) { + return alova.Post( + '/auth/refreshToken', + { refreshToken }, + { + meta: { + authRole: 'refreshToken' + } + } + ); +} + +/** + * return custom backend error + * + * @param code error code + * @param msg error message + */ +export function fetchCustomBackendError(code: string, msg: string) { + return alova.Get('/auth/error', { + params: { code, msg }, + shareRequest: false + }); +} diff --git a/src/serviceAlova/api/index.ts b/src/serviceAlova/api/index.ts new file mode 100644 index 00000000..89f4e581 --- /dev/null +++ b/src/serviceAlova/api/index.ts @@ -0,0 +1,2 @@ +export * from './auth'; +export * from './route'; diff --git a/src/serviceAlova/api/route.ts b/src/serviceAlova/api/route.ts new file mode 100644 index 00000000..94f1c983 --- /dev/null +++ b/src/serviceAlova/api/route.ts @@ -0,0 +1,20 @@ +import { alova } from '../request'; + +/** get constant routes */ +export function fetchGetConstantRoutes() { + return alova.Get('/route/getConstantRoutes'); +} + +/** get user routes */ +export function fetchGetUserRoutes() { + return alova.Get('/route/getUserRoutes'); +} + +/** + * whether the route is exist + * + * @param routeName route name + */ +export function fetchIsRouteExist(routeName: string) { + return alova.Get('/route/isRouteExist', { params: { routeName } }); +} diff --git a/src/serviceAlova/mocks/feature-users-20241014.ts b/src/serviceAlova/mocks/feature-users-20241014.ts new file mode 100644 index 00000000..3746e130 --- /dev/null +++ b/src/serviceAlova/mocks/feature-users-20241014.ts @@ -0,0 +1,56 @@ +import { defineMock } from '@sa/alova/mock'; + +// you can separate the mock data into multiple files dependent on your project versions +export default defineMock({ + '[POST]/systemManage/addUser': () => { + return { + code: '0000', + msg: 'success', + data: null + }; + }, + '[POST]/systemManage/updateUser': () => { + return { + code: '0000', + msg: 'success', + data: null + }; + }, + '[DELETE]/systemManage/deleteUser': () => { + return { + code: '0000', + msg: 'success', + data: null + }; + }, + '[DELETE]/systemManage/batchDeleteUser': () => { + return { + code: '0000', + msg: 'success', + data: null + }; + }, + '[POST]/auth/sendCaptcha': () => { + return { + code: '0000', + msg: 'success', + data: null + }; + }, + '[POST]/auth/verifyCaptcha': () => { + return { + code: '0000', + msg: 'success', + data: null + }; + }, + '/mock/getLastTime': () => { + return { + code: '0000', + msg: 'success', + data: { + time: new Date().toLocaleTimeString() + } + }; + } +}); diff --git a/src/serviceAlova/request/index.ts b/src/serviceAlova/request/index.ts new file mode 100644 index 00000000..5b9ba105 --- /dev/null +++ b/src/serviceAlova/request/index.ts @@ -0,0 +1,115 @@ +import { createAlovaRequest } from '@sa/alova'; +import { createAlovaMockAdapter } from '@sa/alova/mock'; +import adapterFetch from '@sa/alova/fetch'; +import { useAuthStore } from '@/store/modules/auth'; +import { $t } from '@/locales'; +import { getServiceBaseURL } from '@/utils/service'; +import featureUsers20241014 from '../mocks/feature-users-20241014'; +import { getAuthorization, handleRefreshToken, showErrorMsg } from './shared'; +import type { RequestInstanceState } from './type'; + +const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y'; +const { baseURL } = getServiceBaseURL(import.meta.env, isHttpProxy); + +const state: RequestInstanceState = { + errMsgStack: [] +}; +const mockAdapter = createAlovaMockAdapter([featureUsers20241014], { + // using requestAdapter if not match mock request + httpAdapter: adapterFetch(), + + // response delay time + delay: 1000, + + // global mock toggle + enable: true, + matchMode: 'methodurl' +}); +export const alova = createAlovaRequest( + { + baseURL, + requestAdapter: import.meta.env.DEV ? mockAdapter : adapterFetch() + }, + { + onRequest({ config }) { + const Authorization = getAuthorization(); + config.headers.Authorization = Authorization; + config.headers.apifoxToken = 'XL299LiMEDZ0H5h3A29PxwQXdMJqWyY2'; + }, + tokenRefresher: { + async isExpired(response) { + const expiredTokenCodes = import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES?.split(',') || []; + const { code } = await response.clone().json(); + return expiredTokenCodes.includes(String(code)); + }, + async handler() { + await handleRefreshToken(); + } + }, + async isBackendSuccess(response) { + // when the backend response code is "0000"(default), it means the request is success + // to change this logic by yourself, you can modify the `VITE_SERVICE_SUCCESS_CODE` in `.env` file + const resp = response.clone(); + const data = await resp.json(); + return String(data.code) === import.meta.env.VITE_SERVICE_SUCCESS_CODE; + }, + async transformBackendResponse(response) { + return (await response.clone().json()).data; + }, + async onError(error, response) { + const authStore = useAuthStore(); + + let message = error.message; + let responseCode = ''; + if (response) { + const data = await response?.clone().json(); + message = data.msg; + responseCode = String(data.code); + } + + function handleLogout() { + showErrorMsg(state, message); + authStore.resetStore(); + } + + function logoutAndCleanup() { + handleLogout(); + window.removeEventListener('beforeunload', handleLogout); + state.errMsgStack = state.errMsgStack.filter(msg => msg !== message); + } + + // when the backend response code is in `logoutCodes`, it means the user will be logged out and redirected to login page + const logoutCodes = import.meta.env.VITE_SERVICE_LOGOUT_CODES?.split(',') || []; + if (logoutCodes.includes(responseCode)) { + handleLogout(); + throw error; + } + + // when the backend response code is in `modalLogoutCodes`, it means the user will be logged out by displaying a modal + const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || []; + if (modalLogoutCodes.includes(responseCode) && !state.errMsgStack?.includes(message)) { + state.errMsgStack = [...(state.errMsgStack || []), message]; + + // prevent the user from refreshing the page + window.addEventListener('beforeunload', handleLogout); + + window.$dialog?.error({ + title: $t('common.error'), + content: message, + positiveText: $t('common.confirm'), + maskClosable: false, + closeOnEsc: false, + onPositiveClick() { + logoutAndCleanup(); + }, + onClose() { + logoutAndCleanup(); + } + }); + throw error; + } + showErrorMsg(state, message); + throw error; + } + } +); diff --git a/src/serviceAlova/request/shared.ts b/src/serviceAlova/request/shared.ts new file mode 100644 index 00000000..8d3cf38f --- /dev/null +++ b/src/serviceAlova/request/shared.ts @@ -0,0 +1,53 @@ +import { useAuthStore } from '@/store/modules/auth'; +import { localStg } from '@/utils/storage'; +import { fetchRefreshToken } from '../api'; +import type { RequestInstanceState } from './type'; + +export function getAuthorization() { + const token = localStg.get('token'); + const Authorization = token ? `Bearer ${token}` : null; + + return Authorization; +} + +/** refresh token */ +export async function handleRefreshToken() { + const { resetStore } = useAuthStore(); + + const rToken = localStg.get('refreshToken') || ''; + const refreshTokenMethod = fetchRefreshToken(rToken); + + // set the refreshToken role, so that the request will not be intercepted + refreshTokenMethod.meta.authRole = 'refreshToken'; + + try { + const data = await refreshTokenMethod; + localStg.set('token', data.token); + localStg.set('refreshToken', data.refreshToken); + } catch (error) { + resetStore(); + throw error; + } +} + +export function showErrorMsg(state: RequestInstanceState, message: string) { + if (!state.errMsgStack?.length) { + state.errMsgStack = []; + } + + const isExist = state.errMsgStack.includes(message); + + if (!isExist) { + state.errMsgStack.push(message); + + window.$message?.error(message, { + onLeave: () => { + state.errMsgStack = state.errMsgStack.filter(msg => msg !== message); + + setTimeout(() => { + state.errMsgStack = []; + }, 5000); + } + }); + } +} diff --git a/src/serviceAlova/request/type.ts b/src/serviceAlova/request/type.ts new file mode 100644 index 00000000..5f5ce5c8 --- /dev/null +++ b/src/serviceAlova/request/type.ts @@ -0,0 +1,4 @@ +export interface RequestInstanceState { + /** the request error message stack */ + errMsgStack: string[]; +}