Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: optimistic subpackage @sa/alova #646

Merged
merged 1 commit into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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[];
}
Loading