Skip to content

Commit

Permalink
feat(projects): add request exception example page
Browse files Browse the repository at this point in the history
  • Loading branch information
honghuangdc committed Mar 23, 2024
1 parent 11a6a3b commit 41e8bc4
Show file tree
Hide file tree
Showing 11 changed files with 175 additions and 16 deletions.
12 changes: 12 additions & 0 deletions .env
Expand Up @@ -25,3 +25,15 @@ VITE_HTTP_PROXY=Y

# vue-router mode: hash | history | memory
VITE_ROUTER_HISTORY_MODE=history

# success code of backend service, when the code is received, the request is successful
VITE_SERVICE_SUCCESS_CODE=0000

# logout codes of backend service, when the code is received, the user will be logged out and redirected to login page
VITE_SERVICE_LOGOUT_CODES=8888,8889

# modal logout codes of backend service, when the code is received, the user will be logged out by displaying a modal
VITE_SERVICE_MODAL_LOGOUT_CODES=7777,7778

# token expired codes of backend service, when the code is received, it will refresh the token and resend the request
VITE_SERVICE_EXPIRED_TOKEN_CODES=9999,9998
10 changes: 10 additions & 0 deletions src/locales/langs/en-us.ts
Expand Up @@ -33,6 +33,7 @@ const local: App.I18n.Schema = {
search: 'Search',
switch: 'Switch',
tip: 'Tip',
trigger: 'Trigger',
update: 'Update',
updateSuccess: 'Update Success',
userCenter: 'User Center',
Expand All @@ -41,6 +42,14 @@ const local: App.I18n.Schema = {
no: 'No'
}
},
request: {
logout: 'Logout user after request failed',
logoutMsg: 'User status is invalid, please log in again',
logoutWithModal: 'Pop up modal after request failed and then log out user',
logoutWithModalMsg: 'User status is invalid, please log in again',
refreshToken: 'The requested token has expired, refresh the token',
tokenExpired: 'The requested token has expired'
},
theme: {
themeSchema: {
title: 'Theme Schema',
Expand Down Expand Up @@ -138,6 +147,7 @@ const local: App.I18n.Schema = {
'function_hide-child_one': 'Hide Child',
'function_hide-child_two': 'Two',
'function_hide-child_three': 'Three',
function_request: 'Request',
manage: 'System Manage',
manage_user: 'User Manage',
'manage_user-detail': 'User Detail',
Expand Down
10 changes: 10 additions & 0 deletions src/locales/langs/zh-cn.ts
Expand Up @@ -33,6 +33,7 @@ const local: App.I18n.Schema = {
search: '搜索',
switch: '切换',
tip: '提示',
trigger: '触发',
update: '更新',
updateSuccess: '更新成功',
userCenter: '个人中心',
Expand All @@ -41,6 +42,14 @@ const local: App.I18n.Schema = {
no: '否'
}
},
request: {
logout: '请求失败后登出用户',
logoutMsg: '用户状态失效,请重新登录',
logoutWithModal: '请求失败后弹出模态框再登出用户',
logoutWithModalMsg: '用户状态失效,请重新登录',
refreshToken: '请求的token已过期,刷新token',
tokenExpired: 'token已过期'
},
theme: {
themeSchema: {
title: '主题模式',
Expand Down Expand Up @@ -138,6 +147,7 @@ const local: App.I18n.Schema = {
'function_hide-child_one': '隐藏子菜单',
'function_hide-child_two': '菜单二',
'function_hide-child_three': '菜单三',
function_request: '请求',
manage: '系统管理',
manage_user: '用户管理',
'manage_user-detail': '用户详情',
Expand Down
1 change: 1 addition & 0 deletions src/router/elegant/imports.ts
Expand Up @@ -24,6 +24,7 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
"function_hide-child_three": () => import("@/views/function/hide-child/three/index.vue"),
"function_hide-child_two": () => import("@/views/function/hide-child/two/index.vue"),
"function_multi-tab": () => import("@/views/function/multi-tab/index.vue"),
function_request: () => import("@/views/function/request/index.vue"),
function_tab: () => import("@/views/function/tab/index.vue"),
home: () => import("@/views/home/index.vue"),
manage_menu: () => import("@/views/manage/menu/index.vue"),
Expand Down
10 changes: 10 additions & 0 deletions src/router/elegant/routes.ts
Expand Up @@ -117,6 +117,16 @@ export const generatedRoutes: GeneratedRoute[] = [
activeMenu: 'function_tab'
}
},
{
name: 'function_request',
path: '/function/request',
component: 'view.function_request',
meta: {
title: 'function_request',
i18nKey: 'route.function_request',
icon: 'carbon:network-overlay'
}
},
{
name: 'function_tab',
path: '/function/tab',
Expand Down
1 change: 1 addition & 0 deletions src/router/elegant/transform.ts
Expand Up @@ -157,6 +157,7 @@ const routeMap: RouteMap = {
"function_hide-child_three": "/function/hide-child/three",
"function_hide-child_two": "/function/hide-child/two",
"function_multi-tab": "/function/multi-tab",
"function_request": "/function/request",
"function_tab": "/function/tab",
"home": "/home",
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?",
Expand Down
74 changes: 58 additions & 16 deletions src/service/request/index.ts
Expand Up @@ -3,6 +3,7 @@ import { BACKEND_ERROR_CODE, createFlatRequest, createRequest } from '@sa/axios'
import { useAuthStore } from '@/store/modules/auth';
import { localStg } from '@/utils/storage';
import { getServiceBaseURL } from '@/utils/service';
import { $t } from '@/locales';
import { handleRefreshToken } from './shared';

const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
Expand Down Expand Up @@ -32,28 +33,55 @@ export const request = createFlatRequest<App.Service.Response, InstanceState>(
return config;
},
isBackendSuccess(response) {
// when the backend response code is "0000", it means the request is success
// you can change this logic by yourself
return response.data.code === '0000';
// 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
return response.data.code === import.meta.env.VITE_SERVICE_SUCCESS_CODE;
},
async onBackendFail(response, instance) {
// when the backend response code is not "0000", it means the request is fail
// for example: the token is expired, refresh token and retry request

const authStore = useAuthStore();

// when the backend response code is "8888", it means logout the system and redirect to login page
// the following code is an example, you can change it by yourself
const logoutCodes: (string | number)[] = ['8888'];
if (logoutCodes.includes(response.data.code)) {
function handleLogout() {
authStore.resetStore();
}

function logoutAndCleanup() {
handleLogout();
window.removeEventListener('beforeunload', handleLogout);
}

// 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(response.data.code)) {
handleLogout();
return null;
}

// when the backend response code is "9999", it means the token is expired, and refresh token
// the api `refreshToken` can not return the code "9999", otherwise it will be a dead loop, can return `logoutCodes`
const refreshTokenCodes: (string | number)[] = ['9999'];
if (refreshTokenCodes.includes(response.data.code) && !request.state.isRefreshingToken) {
// 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(response.data.code)) {
// prevent the user from refreshing the page
window.addEventListener('beforeunload', handleLogout);

window.$dialog?.error({
title: 'Error',
content: response.data.msg,
positiveText: $t('common.confirm'),
maskClosable: false,
onPositiveClick() {
logoutAndCleanup();
},
onClose() {
logoutAndCleanup();
}
});

return null;
}

// when the backend response code is in `expiredTokenCodes`, it means the token is expired, and refresh token
// the api `refreshToken` can not return error code in `expiredTokenCodes`, otherwise it will be a dead loop, should return `logoutCodes` or `modalLogoutCodes`
const expiredTokenCodes = import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES?.split(',') || [];
if (expiredTokenCodes.includes(response.data.code) && !request.state.isRefreshingToken) {
request.state.isRefreshingToken = true;

const refreshConfig = await handleRefreshToken(response.config);
Expand All @@ -74,13 +102,27 @@ export const request = createFlatRequest<App.Service.Response, InstanceState>(
// when the request is fail, you can show error message

let message = error.message;
let backendErrorCode = '';

// show backend error message
// get backend error message and code
if (error.code === BACKEND_ERROR_CODE) {
message = error.response?.data?.msg || message;
backendErrorCode = error.response?.data?.code || '';
}

window.$message?.error(message);
// the error message is displayed in the modal
const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || [];
if (modalLogoutCodes.includes(backendErrorCode)) {
return;
}

// when the token is expired, refresh token and retry request, so no need to show error message
const expiredTokenCodes = import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES?.split(',') || [];
if (expiredTokenCodes.includes(backendErrorCode)) {
return;
}

window.$message?.error?.(message);
}
}
);
Expand Down
9 changes: 9 additions & 0 deletions src/typings/app.d.ts
Expand Up @@ -279,6 +279,7 @@ declare namespace App {
search: string;
switch: string;
tip: string;
trigger: string;
update: string;
updateSuccess: string;
userCenter: string;
Expand All @@ -287,6 +288,14 @@ declare namespace App {
no: string;
};
};
request: {
logout: string;
logoutMsg: string;
logoutWithModal: string;
logoutWithModalMsg: string;
refreshToken: string;
tokenExpired: string;
};
theme: {
themeSchema: { title: string } & Record<UnionKey.ThemeScheme, string>;
layoutMode: { title: string } & Record<UnionKey.ThemeLayoutMode, string>;
Expand Down
2 changes: 2 additions & 0 deletions src/typings/elegant-router.d.ts
Expand Up @@ -31,6 +31,7 @@ declare module "@elegant-router/types" {
"function_hide-child_three": "/function/hide-child/three";
"function_hide-child_two": "/function/hide-child/two";
"function_multi-tab": "/function/multi-tab";
"function_request": "/function/request";
"function_tab": "/function/tab";
"home": "/home";
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?";
Expand Down Expand Up @@ -117,6 +118,7 @@ declare module "@elegant-router/types" {
| "function_hide-child_three"
| "function_hide-child_two"
| "function_multi-tab"
| "function_request"
| "function_tab"
| "home"
| "manage_menu"
Expand Down
30 changes: 30 additions & 0 deletions src/typings/env.d.ts
Expand Up @@ -27,6 +27,36 @@ declare namespace Env {
readonly VITE_ICON_LOCAL_PREFIX: 'local-icon';
/** backend service base url */
readonly VITE_SERVICE_BASE_URL: string;
/**
* success code of backend service
*
* when the code is received, the request is successful
*/
readonly VITE_SERVICE_SUCCESS_CODE: string;
/**
* logout codes of backend service
*
* when the code is received, the user will be logged out and redirected to login page
*
* use "," to separate multiple codes
*/
readonly VITE_SERVICE_LOGOUT_CODES: string;
/**
* modal logout codes of backend service
*
* when the code is received, the user will be logged out by displaying a modal
*
* use "," to separate multiple codes
*/
readonly VITE_SERVICE_MODAL_LOGOUT_CODES: string;
/**
* token expired codes of backend service
*
* when the code is received, it will refresh the token and resend the request
*
* use "," to separate multiple codes
*/
readonly VITE_SERVICE_EXPIRED_TOKEN_CODES: string;
/**
* other backend service base url
*
Expand Down
32 changes: 32 additions & 0 deletions src/views/function/request/index.vue
@@ -0,0 +1,32 @@
<script setup lang="ts">
import { $t } from '@/locales';
import { fetchCustomBackendError } from '@/service/api';
async function logout() {
await fetchCustomBackendError('8888', $t('request.logoutMsg'));
}
async function logoutWithModal() {
await fetchCustomBackendError('7777', $t('request.logoutWithModalMsg'));
}
async function refreshToken() {
await fetchCustomBackendError('9999', $t('request.tokenExpired'));
}
</script>

<template>
<NSpace vertical :size="16">
<NCard :title="$t('request.logout')" :bordered="false" size="small" segmented class="card-wrapper">
<NButton @click="logout">{{ $t('common.trigger') }}</NButton>
</NCard>
<NCard :title="$t('request.logoutWithModal')" :bordered="false" size="small" segmented class="card-wrapper">
<NButton @click="logoutWithModal">{{ $t('common.trigger') }}</NButton>
</NCard>
<NCard :title="$t('request.refreshToken')" :bordered="false" size="small" segmented class="card-wrapper">
<NButton @click="refreshToken">{{ $t('common.trigger') }}</NButton>
</NCard>
</NSpace>
</template>

<style scoped></style>

0 comments on commit 41e8bc4

Please sign in to comment.