Skip to content

Commit

Permalink
feat(packages): optimistic subpackage @sa/alova (#646)
Browse files Browse the repository at this point in the history
  • Loading branch information
JOU-amjs authored Oct 17, 2024
1 parent 24bb6d9 commit af2fcae
Show file tree
Hide file tree
Showing 11 changed files with 331 additions and 1 deletion.
5 changes: 4 additions & 1 deletion packages/alova/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@
"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": {
"*": {
"*": ["./src/*"]
}
},
"dependencies": {
"@alova/mock": "^2.0.7",
"@sa/utils": "workspace:*",
"alova": "3.0.20"
}
Expand Down
2 changes: 2 additions & 0 deletions packages/alova/src/fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import adapterFetch from 'alova/fetch';
export default adapterFetch;
1 change: 1 addition & 0 deletions packages/alova/src/mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from '@alova/mock';
18 changes: 18 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 56 additions & 0 deletions src/serviceAlova/api/auth.ts
Original file line number Diff line number Diff line change
@@ -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<Api.Auth.LoginToken>('/auth/login', { userName, password });
}

/** Get user info */
export function fetchGetUserInfo() {
return alova.Get<Api.Auth.UserInfo>('/auth/getUserInfo');
}

/** Send captcha to target phone */
export function sendCaptcha(phone: string) {
return alova.Post<null>('/auth/sendCaptcha', { phone });
}

/** Verify captcha */
export function verifyCaptcha(phone: string, code: string) {
return alova.Post<null>('/auth/verifyCaptcha', { phone, code });
}

/**
* Refresh token
*
* @param refreshToken Refresh token
*/
export function fetchRefreshToken(refreshToken: string) {
return alova.Post<Api.Auth.LoginToken>(
'/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
});
}
2 changes: 2 additions & 0 deletions src/serviceAlova/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './auth';
export * from './route';
20 changes: 20 additions & 0 deletions src/serviceAlova/api/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { alova } from '../request';

/** get constant routes */
export function fetchGetConstantRoutes() {
return alova.Get<Api.Route.MenuRoute[]>('/route/getConstantRoutes');
}

/** get user routes */
export function fetchGetUserRoutes() {
return alova.Get<Api.Route.UserRoute>('/route/getUserRoutes');
}

/**
* whether the route is exist
*
* @param routeName route name
*/
export function fetchIsRouteExist(routeName: string) {
return alova.Get<boolean>('/route/isRouteExist', { params: { routeName } });
}
56 changes: 56 additions & 0 deletions src/serviceAlova/mocks/feature-users-20241014.ts
Original file line number Diff line number Diff line change
@@ -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()
}
};
}
});
115 changes: 115 additions & 0 deletions src/serviceAlova/request/index.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
);
53 changes: 53 additions & 0 deletions src/serviceAlova/request/shared.ts
Original file line number Diff line number Diff line change
@@ -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);
}
});
}
}
4 changes: 4 additions & 0 deletions src/serviceAlova/request/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface RequestInstanceState {
/** the request error message stack */
errMsgStack: string[];
}

0 comments on commit af2fcae

Please sign in to comment.