跳到主要内容

完善请求库 Axios ②

视频演示

环境准备
  • 安装 Git
    • 设置换行符为 lf
    $ git config --global core.autocrlf input
    $ git config --global core.eol lf
  • 安装 Node.js
  • 安装 VSCode

拉取项目

# 从 GitHub 拉取项目(存在网络限制)
$ git clone --branch v0.0.6 https://github.com/geenln/vue3-elemnt-plus-ts.git

# 或者从 Gitee 拉取项目
$ git clone --branch v0.0.6 https://gitee.com/genin/vue3-elemnt-plus-ts.git

安装项目依赖

$ npm install

启动开发服务器

$ npm run dev

登录信息

创建生成随机数函数、解析转换json函数

src/common/tools.ts
const ZERO_ASCII = 48
const LOWER_A_ASCII = 97
const charLib = String.fromCharCode(
...new Array(10)
.fill(ZERO_ASCII)
.map((item, index) => item + index)
.concat(
new Array(26).fill(LOWER_A_ASCII).map((item, index) => item + index),
),
)

export const createRandomString = (length = 8) => {
const libLength = charLib.length
return new Array(length).fill('').reduce(str => {
const randomIndex = Math.floor(Math.random() * libLength)
return str + charLib.substring(randomIndex, randomIndex + 1)
}, '')
}

export const parseJSONSafely = (str: any): Record<string, any> | void => {
try {
return JSON.parse(str)
} catch (error) {
console.error('An error occurred while parsing the JSON string')
}
}

export const stringifyObjSafely = (
obj: Record<string, any>,
tabSpaces?: number,
): string => {
try {
if (typeof obj === 'string') {
return obj
}
return JSON.stringify(obj, null, tabSpaces)
} catch (error) {
console.error(error)
return 'stringify error'
}
}

创建用户类型声明

src/types/common.d.ts
export interface UserInfo {
username: string
token: string
}

安装加密库 crypto-js

npm install crypto-js
npm install -D @types/crypto-js

创建 auth.ts 文件管理用户信息

src/common/auth.ts
import CryptoJS from 'crypto-js'
import {createRandomString, parseJSONSafely, stringifyObjSafely} from './tools'
import {UserInfo} from '@/types/common'

const USER_INFO_KEY = 'ukV1'
const ENCRYPTION_KEY = 'ZRBMXNIODXM9P27Z'

const getUserInfoKey = () => {
return localStorage.getItem(USER_INFO_KEY)
}

const setUserInfoKey = () => {
const uk = createRandomString(4)
localStorage.setItem(USER_INFO_KEY, uk)
return uk
}

const encryptSafely = (str: string, key: string) => {
try {
return CryptoJS.AES.encrypt(str, key)
} catch (error) {
console.error(error)
return str
}
}

const decryptSafely = (str: string, key: string) => {
try {
const bytes = CryptoJS.AES.decrypt(str, key)
return bytes.toString(CryptoJS.enc.Utf8)
} catch (error) {
console.error(error)
return str
}
}

export const setUser = (user: UserInfo): void => {
const uk = setUserInfoKey()
const userInfo = encryptSafely(stringifyObjSafely(user), ENCRYPTION_KEY)
localStorage.setItem(uk, userInfo as string)
}

export const getUser = (): undefined | UserInfo => {
const uk = getUserInfoKey()
if (!uk) {
return
}
const user = localStorage.getItem(uk)
if (!user) {
return
}
const userInfoStr = decryptSafely(user, ENCRYPTION_KEY)
const userInfo = parseJSONSafely(userInfoStr) as UserInfo
return userInfo
}

export const removeUser = (): void => {
const uk = getUserInfoKey()
if (!uk) {
return
}
localStorage.removeItem(uk)
localStorage.removeItem(USER_INFO_KEY)
}

基础状态信息

src/store/modules/common.ts
import {getUser, removeUser, setUser} from '@/common/auth'
import {UserInfo} from '@/types/common'
import {defineStore} from 'pinia'

export const useCommonStore = defineStore('common', {
state: () => ({
user: (getUser() || {}) as UserInfo,
request_queue: 0,
abortControllers: [] as AbortController[],
}),
actions: {
updateUserInfo(userInfo = {} as any) {
const {logOut = false} = userInfo
if (logOut) {
removeUser()
} else {
setUser(userInfo)
}
this.user = userInfo
},
setReqChange(addOrDone: boolean) {
addOrDone ? ++this.request_queue : --this.request_queue
},
addAbortController(controller: AbortController) {
this.abortControllers.push(controller)
},
clearAbortControllers() {
this.abortControllers.forEach(controller => {
controller.abort()
})
this.abortControllers = []
},
removeAbortController(controller?: AbortController) {
if (!controller) {
return
}
const index = this.abortControllers.indexOf(controller)
if (index !== -1) {
this.abortControllers.splice(index, 1)
}
},
},
})

更新 vite-env.d.ts 文件添加 typeScript 语法提示

src/vite-env.d.ts
/// <reference types="vite/client" />
/// <reference types="../auto-imports.d.ts" />
/// <reference types="../components.d.ts" />
+import 'vue-router'
+import 'axios'

declare module '*.vue' {
import {DefineComponent} from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

+declare module 'vue-router' {
+ export interface RouteMeta {
+ authRequired?: boolean
+ }
+}

+declare module 'axios' {
+ interface InternalAxiosRequestConfig {
+ controller?: AbortController
+ doNotTriggerProgress?: boolean
+ }
+}

+interface ImportMetaEnv {
+ readonly VITE_API_BASE_URL: string
+}

更新路由文件

src/router/index.ts
+import {useCommonStore} from '@/store/modules/common'
import {RouteRecordRaw, createRouter, createWebHashHistory} from 'vue-router'

const defaultRouter: RouteRecordRaw[] = [
+ {
+ path: '/',
+ redirect: '/userInfo',
+ meta: {
+ authRequired: true,
+ },
+ },
+ {
+ path: '/login',
+ component: () => import('@/views/Login.vue'),
+ },
+ {
+ path: '/userInfo',
+ meta: {
+ authRequired: true,
+ },
+ component: () => import('@/views/UserInfo.vue'),
+ },
{
path: '/t1',
component: () => import('@/views/TestA.vue'),
},
{
path: '/t2',
component: () => import('@/views/TestB.vue'),
},
]

const router = createRouter({
history: createWebHashHistory(),
routes: [...defaultRouter],
})

+router.beforeEach((to, _from, next) => {
+ const {fullPath, meta} = to
+ const {authRequired = false} = meta
+ const commonStore = useCommonStore()

+ if (authRequired && !commonStore.user.token) {
+ toLogin(fullPath)
+ return
+ }
+ next()
+})

+export function toLogin(path?: string): void {
+ const commonStore = useCommonStore()
+ commonStore.updateUserInfo({logOut: true})
+ commonStore.clearAbortControllers()
+ const currentPath = router.currentRoute.value.path

+ currentPath !== '/login' &&
+ router.push({
+ path: '/login',
+ query: {to: path ? path : currentPath ?? undefined},
+ })
+}

export default router

请求管理

安装进度条库 NProgress

npm install nprogress
npm install -D @types/nprogress

安装工具库 Lodash

npm install lodash-es
npm install -D @types/lodash-es

在项目根目录创建 .env.development 文件存放环境变量

.env.development
VITE_API_BASE_URL=https://mock.presstime.cn/mock/6655e837dd3831604fff689f/example

创建 http2.ts

src/common/http2.ts
import {toLogin} from '@/router'
import {useCommonStore} from '@/store/modules/common'
import axios from 'axios'
import {ElNotification} from 'element-plus'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import 'element-plus/es/components/notification/style/css'

NProgress.configure({showSpinner: false, trickleSpeed: 200})
let respSet = new Set()
const resetRespSet = () => (respSet = new Set())

const instance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 20000,
})

instance.interceptors.request.use(
config => {
const commonStore = useCommonStore()
config.headers.Authorization = `Bearer ${commonStore.user.token}`

const controller = new AbortController()
config.signal = controller.signal
config.controller = controller
commonStore.addAbortController(controller)
return config
},
error => Promise.reject(error),
)

instance.interceptors.request.use(
config => {
if (!config.doNotTriggerProgress) {
const commonStore = useCommonStore()
if (!commonStore.request_queue) {
NProgress.start()
}
commonStore.setReqChange(true)
}
return config
},
error => Promise.reject(error),
)

const isTokenExpired = (data: any) => {
return ['BAD_TOKEN', 'TOKEN_TIME_OUT'].includes(data.code)
}

instance.interceptors.response.use(
response => {
if (!response.config.doNotTriggerProgress) {
setProgressBarDone()
}

const {code} = response.data

if (!respSet.has(code)) {
respSet.add(code)

if (isTokenExpired(response.data)) {
ElNotification.error('token过期')
toLogin()

window.setTimeout(resetRespSet, 100)
return Promise.reject(response.data)
}
}

const commonStore = useCommonStore()
const controller = response.config.controller
commonStore.removeAbortController(controller)
return response.data
},
error => Promise.reject(error),
)

async function setProgressBarDone() {
const commonStore = useCommonStore()
commonStore.setReqChange(false)
const queueLen = commonStore.request_queue
if (queueLen > 0) {
NProgress.inc()
} else {
NProgress.done()
}
}

export default instance

访问路由验证

创建 Login.vue、UserInfo.vue 页面

src/views/Login.vue
<script setup lang="ts">
import http2 from '@/common/http2'
import {useCommonStore} from '@/store/modules/common'
import {UserInfo} from '@/types/common'

const router = useRouter()
const route = useRoute()
const commonStore = useCommonStore()

const redirect = () => {
router.replace({
path: (route.query.to ?? '/userInfo').toString(),
})
}

const handleLogin = () => {
http2.post<UserInfo>('/login').then(res => {
commonStore.updateUserInfo(res.data)
redirect()
})
}
</script>

<template>
<h1>登录页</h1>
<ElButton @click="handleLogin">登录</ElButton>
</template>

src/views/UserInfo.vue
<script setup lang="ts">
import {useCommonStore} from '@/store/modules/common'
import http2 from '@/common/http2'
const commonStore = useCommonStore()

const handleTokenExpired = () => {
http2.post('/tokenTimeOut').then(res => {
console.info('res', res)
})
}
</script>

<template>
<h1>用户信息 {{ commonStore.user.username }}</h1>
<ElButton @click="handleTokenExpired">测试登录过期</ElButton>
</template>