Skip to content

Commit 6d84ede

Browse files
committed
feat: optimistic subpackage @sa/alova
1 parent 24bb6d9 commit 6d84ede

File tree

11 files changed

+331
-1
lines changed

11 files changed

+331
-1
lines changed

packages/alova/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@
33
"version": "0.1.0",
44
"exports": {
55
".": "./src/index.ts",
6-
"./client": "./src/client.ts"
6+
"./fetch": "./src/fetch.ts",
7+
"./client": "./src/client.ts",
8+
"./mock": "./src/mock.ts"
79
},
810
"typesVersions": {
911
"*": {
1012
"*": ["./src/*"]
1113
}
1214
},
1315
"dependencies": {
16+
"@alova/mock": "^2.0.7",
1417
"@sa/utils": "workspace:*",
1518
"alova": "3.0.20"
1619
}

packages/alova/src/fetch.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import adapterFetch from 'alova/fetch';
2+
export default adapterFetch;

packages/alova/src/mock.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from '@alova/mock';

pnpm-lock.yaml

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/serviceAlova/api/auth.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { alova } from '../request';
2+
3+
/**
4+
* Login
5+
*
6+
* @param userName User name
7+
* @param password Password
8+
*/
9+
export function fetchLogin(userName: string, password: string) {
10+
return alova.Post<Api.Auth.LoginToken>('/auth/login', { userName, password });
11+
}
12+
13+
/** Get user info */
14+
export function fetchGetUserInfo() {
15+
return alova.Get<Api.Auth.UserInfo>('/auth/getUserInfo');
16+
}
17+
18+
/** Send captcha to target phone */
19+
export function sendCaptcha(phone: string) {
20+
return alova.Post<null>('/auth/sendCaptcha', { phone });
21+
}
22+
23+
/** Verify captcha */
24+
export function verifyCaptcha(phone: string, code: string) {
25+
return alova.Post<null>('/auth/verifyCaptcha', { phone, code });
26+
}
27+
28+
/**
29+
* Refresh token
30+
*
31+
* @param refreshToken Refresh token
32+
*/
33+
export function fetchRefreshToken(refreshToken: string) {
34+
return alova.Post<Api.Auth.LoginToken>(
35+
'/auth/refreshToken',
36+
{ refreshToken },
37+
{
38+
meta: {
39+
authRole: 'refreshToken'
40+
}
41+
}
42+
);
43+
}
44+
45+
/**
46+
* return custom backend error
47+
*
48+
* @param code error code
49+
* @param msg error message
50+
*/
51+
export function fetchCustomBackendError(code: string, msg: string) {
52+
return alova.Get('/auth/error', {
53+
params: { code, msg },
54+
shareRequest: false
55+
});
56+
}

src/serviceAlova/api/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './auth';
2+
export * from './route';

src/serviceAlova/api/route.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { alova } from '../request';
2+
3+
/** get constant routes */
4+
export function fetchGetConstantRoutes() {
5+
return alova.Get<Api.Route.MenuRoute[]>('/route/getConstantRoutes');
6+
}
7+
8+
/** get user routes */
9+
export function fetchGetUserRoutes() {
10+
return alova.Get<Api.Route.UserRoute>('/route/getUserRoutes');
11+
}
12+
13+
/**
14+
* whether the route is exist
15+
*
16+
* @param routeName route name
17+
*/
18+
export function fetchIsRouteExist(routeName: string) {
19+
return alova.Get<boolean>('/route/isRouteExist', { params: { routeName } });
20+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { defineMock } from '@sa/alova/mock';
2+
3+
// you can separate the mock data into multiple files dependent on your project versions
4+
export default defineMock({
5+
'[POST]/systemManage/addUser': () => {
6+
return {
7+
code: '0000',
8+
msg: 'success',
9+
data: null
10+
};
11+
},
12+
'[POST]/systemManage/updateUser': () => {
13+
return {
14+
code: '0000',
15+
msg: 'success',
16+
data: null
17+
};
18+
},
19+
'[DELETE]/systemManage/deleteUser': () => {
20+
return {
21+
code: '0000',
22+
msg: 'success',
23+
data: null
24+
};
25+
},
26+
'[DELETE]/systemManage/batchDeleteUser': () => {
27+
return {
28+
code: '0000',
29+
msg: 'success',
30+
data: null
31+
};
32+
},
33+
'[POST]/auth/sendCaptcha': () => {
34+
return {
35+
code: '0000',
36+
msg: 'success',
37+
data: null
38+
};
39+
},
40+
'[POST]/auth/verifyCaptcha': () => {
41+
return {
42+
code: '0000',
43+
msg: 'success',
44+
data: null
45+
};
46+
},
47+
'/mock/getLastTime': () => {
48+
return {
49+
code: '0000',
50+
msg: 'success',
51+
data: {
52+
time: new Date().toLocaleTimeString()
53+
}
54+
};
55+
}
56+
});

src/serviceAlova/request/index.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { createAlovaRequest } from '@sa/alova';
2+
import { createAlovaMockAdapter } from '@sa/alova/mock';
3+
import adapterFetch from '@sa/alova/fetch';
4+
import { useAuthStore } from '@/store/modules/auth';
5+
import { $t } from '@/locales';
6+
import { getServiceBaseURL } from '@/utils/service';
7+
import featureUsers20241014 from '../mocks/feature-users-20241014';
8+
import { getAuthorization, handleRefreshToken, showErrorMsg } from './shared';
9+
import type { RequestInstanceState } from './type';
10+
11+
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
12+
const { baseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
13+
14+
const state: RequestInstanceState = {
15+
errMsgStack: []
16+
};
17+
const mockAdapter = createAlovaMockAdapter([featureUsers20241014], {
18+
// using requestAdapter if not match mock request
19+
httpAdapter: adapterFetch(),
20+
21+
// response delay time
22+
delay: 1000,
23+
24+
// global mock toggle
25+
enable: true,
26+
matchMode: 'methodurl'
27+
});
28+
export const alova = createAlovaRequest(
29+
{
30+
baseURL,
31+
requestAdapter: import.meta.env.DEV ? mockAdapter : adapterFetch()
32+
},
33+
{
34+
onRequest({ config }) {
35+
const Authorization = getAuthorization();
36+
config.headers.Authorization = Authorization;
37+
config.headers.apifoxToken = 'XL299LiMEDZ0H5h3A29PxwQXdMJqWyY2';
38+
},
39+
tokenRefresher: {
40+
async isExpired(response) {
41+
const expiredTokenCodes = import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES?.split(',') || [];
42+
const { code } = await response.clone().json();
43+
return expiredTokenCodes.includes(String(code));
44+
},
45+
async handler() {
46+
await handleRefreshToken();
47+
}
48+
},
49+
async isBackendSuccess(response) {
50+
// when the backend response code is "0000"(default), it means the request is success
51+
// to change this logic by yourself, you can modify the `VITE_SERVICE_SUCCESS_CODE` in `.env` file
52+
const resp = response.clone();
53+
const data = await resp.json();
54+
return String(data.code) === import.meta.env.VITE_SERVICE_SUCCESS_CODE;
55+
},
56+
async transformBackendResponse(response) {
57+
return (await response.clone().json()).data;
58+
},
59+
async onError(error, response) {
60+
const authStore = useAuthStore();
61+
62+
let message = error.message;
63+
let responseCode = '';
64+
if (response) {
65+
const data = await response?.clone().json();
66+
message = data.msg;
67+
responseCode = String(data.code);
68+
}
69+
70+
function handleLogout() {
71+
showErrorMsg(state, message);
72+
authStore.resetStore();
73+
}
74+
75+
function logoutAndCleanup() {
76+
handleLogout();
77+
window.removeEventListener('beforeunload', handleLogout);
78+
state.errMsgStack = state.errMsgStack.filter(msg => msg !== message);
79+
}
80+
81+
// when the backend response code is in `logoutCodes`, it means the user will be logged out and redirected to login page
82+
const logoutCodes = import.meta.env.VITE_SERVICE_LOGOUT_CODES?.split(',') || [];
83+
if (logoutCodes.includes(responseCode)) {
84+
handleLogout();
85+
throw error;
86+
}
87+
88+
// when the backend response code is in `modalLogoutCodes`, it means the user will be logged out by displaying a modal
89+
const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || [];
90+
if (modalLogoutCodes.includes(responseCode) && !state.errMsgStack?.includes(message)) {
91+
state.errMsgStack = [...(state.errMsgStack || []), message];
92+
93+
// prevent the user from refreshing the page
94+
window.addEventListener('beforeunload', handleLogout);
95+
96+
window.$dialog?.error({
97+
title: $t('common.error'),
98+
content: message,
99+
positiveText: $t('common.confirm'),
100+
maskClosable: false,
101+
closeOnEsc: false,
102+
onPositiveClick() {
103+
logoutAndCleanup();
104+
},
105+
onClose() {
106+
logoutAndCleanup();
107+
}
108+
});
109+
throw error;
110+
}
111+
showErrorMsg(state, message);
112+
throw error;
113+
}
114+
}
115+
);

src/serviceAlova/request/shared.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { useAuthStore } from '@/store/modules/auth';
2+
import { localStg } from '@/utils/storage';
3+
import { fetchRefreshToken } from '../api';
4+
import type { RequestInstanceState } from './type';
5+
6+
export function getAuthorization() {
7+
const token = localStg.get('token');
8+
const Authorization = token ? `Bearer ${token}` : null;
9+
10+
return Authorization;
11+
}
12+
13+
/** refresh token */
14+
export async function handleRefreshToken() {
15+
const { resetStore } = useAuthStore();
16+
17+
const rToken = localStg.get('refreshToken') || '';
18+
const refreshTokenMethod = fetchRefreshToken(rToken);
19+
20+
// set the refreshToken role, so that the request will not be intercepted
21+
refreshTokenMethod.meta.authRole = 'refreshToken';
22+
23+
try {
24+
const data = await refreshTokenMethod;
25+
localStg.set('token', data.token);
26+
localStg.set('refreshToken', data.refreshToken);
27+
} catch (error) {
28+
resetStore();
29+
throw error;
30+
}
31+
}
32+
33+
export function showErrorMsg(state: RequestInstanceState, message: string) {
34+
if (!state.errMsgStack?.length) {
35+
state.errMsgStack = [];
36+
}
37+
38+
const isExist = state.errMsgStack.includes(message);
39+
40+
if (!isExist) {
41+
state.errMsgStack.push(message);
42+
43+
window.$message?.error(message, {
44+
onLeave: () => {
45+
state.errMsgStack = state.errMsgStack.filter(msg => msg !== message);
46+
47+
setTimeout(() => {
48+
state.errMsgStack = [];
49+
}, 5000);
50+
}
51+
});
52+
}
53+
}

0 commit comments

Comments
 (0)