Axios封装
index.ts配置说明
const axios = new VAxios({
// 认证方案,例如: Bearer
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
authenticationScheme: '',
// 接口超时时间 单位毫秒
timeout: 10 * 1000,
// 接口可能会有通用的地址部分,可以统一抽取出来
prefixUrl: prefix,
headers: { 'Content-Type': ContentTypeEnum.JSON },
// 数据处理方式,见下方说明
transform,
// 配置项,下面的选项都可以在独立的接口请求中覆盖
requestOptions: {
// 默认将prefix 添加到url
joinPrefix: true,
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
isReturnNativeResponse: false,
// 需要对返回数据进行处理
isTransformRequestResult: true,
// post请求的时候添加参数到url
joinParamsToUrl: false,
// 格式化提交参数时间
formatDate: true,
// 消息提示类型
errorMessageMode: 'message',
// 接口地址
apiUrl: globSetting.apiUrl,
// 是否加入时间戳
joinTime: true,
// 忽略重复请求
ignoreCancelToken: true,
},
});
transform 数据处理说明
transform 抽象类定义
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
import type { RequestOptions, Result } from 'types/axios';
export interface CreateAxiosOptions extends AxiosRequestConfig {
authenticationScheme?: string; //代表什么?
transform?: AxiosTransform; // 数据处理类
requestOptions?: RequestOptions; // 请求数据
}
/**
* 抽象类 数据处理类
*/
export abstract class AxiosTransform {
// ? 请求直接拦截器函数 入参为请求参数和自定义请求参数
requestInterceptors?: (config: AxiosRequestConfig, options: CreateAxiosOptions) => AxiosRequestConfig;
// ? 请求拦截器错误处理
requestInterceptorsCatch?: (error: Error) => void;
// ? 处理请求之前的options参数的 加工函数
beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig;
// ? 处理请求失败的 加工函数
requestCatchHook?: (e: Error, options: RequestOptions) => Promise<any>;
// * 响应拦截器
responseInterceptors?: (res: AxiosResponse<any>) => AxiosResponse<any>;
// * 处理成功返回参数的 加工函数
transformRequestHook?: (res: AxiosResponse<Result>, options:RequestOptions) => any;
// * 响应拦截器错误处理
responseInterceptorsCatch?: (error: Error) => void;
}
transform数据处理实现
/**
* @description 数据处理,供外部自定义处理
*/
const transform: AxiosTransform = {
// 处理请求参数 config
beforeRequestHook: (config, options) => {
const { apiUrl, joinPrefix, urlPrefix } = options;
if (joinPrefix) {
config.url = `${urlPrefix}${config.url}`;
}
if (apiUrl && isString(apiUrl)) {
config.url = `${apiUrl}${config.url}`;
}
return config;
},
// 请求拦截器 处理
requestInterceptors: (config, options) => {
// ! 添加统一的token、统一处理 headers等
// const token = getToken();
// if (token && (config as Recordable)?.requestOptions?.withToken !== false) {
// // jwt token
// (config as Recordable).headers.Authorization = options.authenticationScheme
// ? `${options.authenticationScheme} ${token}`
// : token;
// }
return config;
},
// * 响应拦截器处理
responseInterceptors: (res) => {
return res;
},
// * 响应数据处理,可处理异常 比如网络超时,404 403等
transformRequestHook: (res, options) => {
const { isTransformResponse, isReturnNativeResponse } = options;
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
if (isReturnNativeResponse) {
return res;
}
// 不进行任何处理,直接返回
// 用于页面代码可能需要直接获取code,data,message这些信息时开启
if (!isTransformResponse) {
return res.data;
}
const { data } = res;
// 这里 code,result,message为 后台统一的字段,需要在 types.ts内修改为项目自己的接口返回格式
const { code, result, message } = data;
if (code !== ResultEnum.SUCCESS) {
new Error('sys.api.apiRequestFailed');
}
}
};
createAxios
import type { AxiosRequestConfig, AxiosInstance, AxiosResponse, AxiosError } from 'axios';
import type { RequestOptions, Result } from 'types/axios';
import type { CreateAxiosOptions } from './transformTyping';
import { cloneDeep } from 'lodash-es';
import { isFunction } from '../utils/is';
import axios from 'axios';
// import qs from 'qs';
export class VAxios {
// axios实例对象
private axiosInstance: AxiosInstance;
private readonly options: CreateAxiosOptions;
// 传入的参数,创建一个axios实例 暴露出调用方法
constructor(options: CreateAxiosOptions) {
this.options = options;
// 传入请求参数 创建一个axios实例
this.axiosInstance = axios.create(options);
// 定义定时器
this.setupInterceptors();
}
/**
* @description 获取自定义的axios数据处理对象
*/
private getTransform() {
const { transform } = this.options;
return transform;
}
/**
* @description 拦截器设置
*/
private setupInterceptors() {
// 获取axios处理函数
const transform = this.getTransform();
if (!transform) return;
// * 提取拦截器相关处理方法
const {
requestInterceptors,
requestInterceptorsCatch,
responseInterceptors,
responseInterceptorsCatch
} = transform;
// ? 请求拦截器默认设置
this.axiosInstance.interceptors.request.use((config: AxiosRequestConfig) => {
// TODO 忽略token部分未封装完毕
// 判断拦截器请求处理函数是否存在,是否为函数
if (requestInterceptors && isFunction(requestInterceptors)) {
config = requestInterceptors(config, this.options);
}
return config;
}, undefined);
// ! 请求拦截器异常获取
requestInterceptorsCatch &&
isFunction(requestInterceptorsCatch) &&
this.axiosInstance.interceptors.request.use(undefined, requestInterceptorsCatch);
// ? 响应拦截器默认设置
this.axiosInstance.interceptors.response.use((res: AxiosResponse) => {
// 判断处理函数是否存在
if(responseInterceptors && isFunction(responseInterceptors)) {
res = responseInterceptors(res);
}
return res;
});
// ! 响应拦截器异常处理
responseInterceptorsCatch &&
isFunction(responseInterceptorsCatch) &&
this.axiosInstance.interceptors.response.use(undefined, responseInterceptorsCatch)
}
/**
* @description 请求函数封装
* @config 默认传入 method,data等axios请求对象内容
* @options 请求函数额外得配置 追加前缀等
*/
request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
let conf: CreateAxiosOptions = cloneDeep(config);
// 获取数据处理函数 对象
const transform = this.getTransform();
// 获取自定义的请求参数
const { requestOptions } = this.options;
// 将全局自定义请求参数与指定的方法自定义参数合并
const opt: RequestOptions = Object.assign({}, requestOptions, options);
// 在请求开始之前处理请求参数
const { beforeRequestHook, requestCatchHook, transformRequestHook } = transform || {};
if (beforeRequestHook && isFunction(beforeRequestHook)) {
conf = beforeRequestHook(conf, opt);
}
// 请求参数
conf.requestOptions = opt;
// 发起请求
return new Promise((resolve, reject) => {
// ! AxiosResponse<Result> Result表示返回数据中data的格式
this.axiosInstance
.request<any, AxiosResponse<Result>>(conf)
.then((res: AxiosResponse<Result>) => {
// ? 若存在成功处理函数 则使用成果处理函数
if (transformRequestHook && isFunction(transformRequestHook)) {
try {
const ret = transformRequestHook(res, opt);
resolve(ret);
} catch (err) {
reject(err || new Error('request error'));
}
return;
}
resolve(res as unknown as Promise<T>);
})
.catch((e: Error | AxiosError) => {
// ? 判断是否存在请求异常处理的函数
if (requestCatchHook && isFunction(requestCatchHook)) {
reject(requestCatchHook(e, opt));
return;
}
if (axios.isAxiosError(e)) {
// rewrite error message from axios in here
}
reject(e);
});
});
}
// ! 每个请求函数
get<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
return this.request({ ...config, method: 'GET' }, options);
}
post<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
return this.request({ ...config, method: 'POST' }, options);
}
put<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
return this.request({ ...config, method: 'PUT' }, options);
}
delete<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
return this.request({ ...config, method: 'DELETE' }, options);
}
}
在createAxios中定义了基本的 请求拦截器、响应拦截器、接口请求封装。将具体的请求拦截、响应拦截成功、响应失败处理、数据转化等方法暴露到外部统一的transform对象上,方便不同接口的自定义要求。每个接口额外接收RequestOptions类的options参数来覆盖统一参数,供特殊接口单独化配置。
Mock
需要安装的插件 mockjs、vite-plugin-mock。
根目录下创建一个与src同级的目录mock
// mock/order/order.ts
import { MockMethod } from 'vite-plugin-mock'
import { OrderState } from '@/@types/order';
const OrderList: OrderState[] = [
{
orderId: '342937324834',
name: '焦糖瓜子',
userId: '001',
vaccineName: '脊灰(灭活sabin株)',
type: '免规疫苗',
deadLine: 2,
month: '03',
day: '16',
startTime: '15:30',
endTime: '16:00',
organ: '健康服务中心',
address: '南山区白石洲社康中心(沙河街道)',
},
{
orderId: '2245667785566',
name: '李萌萌',
userId: '002',
vaccineName: '乙肝(灭活sabin株)',
type: '免规疫苗',
deadLine: 6,
month: '11',
day: '20',
startTime: '10:30',
endTime: '11:00',
organ: '健康服务中心',
address: '南山区白石洲社康中心(沙河街道)',
},
];
export default [
{
url: '/api/orderList',
method: 'get',
response: () => {
return {
status: 1,
data: OrderList
}
}
}
] as MockMethod[];
创建mockjs的服务器
如果mockProdServer.ts位置不在src内,import.meta将会报错。需要解决
不需要再手动引入mock中的文件,使用vite中的import.meta.glob或者import.meta.globEager动态导入文件, 自动化配置mock接口
// src/mockProdServer.ts
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'
// import testModule from './order/order';
// ! 如果mockProdServer.ts位置不在src内,import.meta将会报错
// todo 处理import.meta的报错问题
const modules = import.meta.globEager('../mock/**/*.ts')
const mockModules: any[] = [];
Object.keys(modules).forEach((key) => {
if (key.includes('/_')) {
return;
}
console.log(modules[key]);
mockModules.push(...modules[key].default);
});
console.log('mockModules', mockModules);
export function setupProdMockServer() {
createProdMockServer([...mockModules])
}
vite.config.js配置
import { viteMockServe } from "vite-plugin-mock";
plugins: [
vue(),
viteMockServe({
mockPath: 'mock',
supportTs: true,
injectCode: `
import { setupProdMockServer } from './mockProdServer';
setupProdMockServer();
`
})
],
router配置
路由配置表
在router文件下创建routes文件,当前文件下的**modules** 文件专门用于不同模块或者系统定义路由配置信息。index.vue 则是使用import.meta.globEager进行动态路由导入,后续若再配置路由表则只需直接在 modules 文件下创建文件即可。
// about.ts
import type { AppRouteRecordRaw } from 'src/router/types';
import { LAYOUT } from '../../constant';
const dashboard: AppRouteRecordRaw = {
path: '/about',
name: 'About',
component: LAYOUT,
redirect: '/about/index',
meta: {
hideChildrenInMenu: true,
icon: 'simple-icons:about-dot-me',
title: '首页',
orderNo: 100000,
},
children: [
{
path: 'index',
name: 'AboutPage',
component: () => import('src/views/system/about/index.vue'),
meta: {
title: '关于',
hideMenu: true,
},
},
],
};
export default dashboard;
动态路由配置
动态路由配置:
// routes/index.ts
import type { AppRouteRecordRaw } from 'src/router/types';
// 获取文件
const modules = import.meta.globEager('/src/router/routes/modules/*.ts');
const routeModuleList: AppRouteRecordRaw[] = [];
Object.keys(modules).forEach((key) => {
const mod = modules[key].default || {};
const modList = Array.isArray(mod) ? [...mod] : [mod];
routeModuleList.push(...modList);
});
export default [
...routeModuleList
];
路由权限
对路由权限,界面权限等进行权限配置。当前方法被 main.ts 引入并执行,传入router
import type { Router } from 'vue-router';
// Don't change the order of creation
export function setupRouterGuard(router: Router) {
// createPageGuard(router);
// createPageLoadingGuard(router);
// createHttpGuard(router);
// createScrollGuard(router);
// createMessageGuard(router);
// createProgressGuard(router);
createPermissionGuard(router);
// createParamMenuGuard(router); // must after createPermissionGuard (menu has been built.)
// createStateGuard(router);
}
function createPermissionGuard(router: Router) {
// 路由权限校验
router.beforeEach(async (to, from, next) => {
console.log(to,from);
// 可根据实际情况,进行权限校验 根据登录状态、存入store中的数据等
next();
});
}
注册路由
// router/index.ts
import type { RouteRecordRaw } from 'vue-router';
import type { App } from 'vue';
import { createRouter, createWebHashHistory } from 'vue-router';
import routes from './routes';
// app router
export const router = createRouter({
history: createWebHashHistory(),
routes: routes as unknown as RouteRecordRaw[],
strict: true,
scrollBehavior: () => ({ left: 0, top: 0 }),
});
// config router: 接收app实例,注册router
export function setupRouter(app: App<Element>) {
app.use(router);
}
执行函数 注册router、权限配置
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { router, setupRouter } from 'src/router';
import { setupRouterGuard } from 'src/router/guard';
function bootStrap() {
const app = createApp(App);
// Configure routing 初始化路由
setupRouter(app);
// router权限控制
setupRouterGuard(router);
app.mount('#app')
}
bootStrap();
pinia store配置
userStore
当前userStore中主要保存用户信息、token。用户登录后可获取到token等信息,再调用getUserInfo来获取userInfo(主要是当前用户的角色信息)。登录成功后,匹配当前用户的菜单权限:调用permissionStore的buildRoutesAction,根据当前的权限类型配置,去分别获取不同的路由和菜单信息。
login
用户登录后可获取到token,登录成功设置token,用于request.interceptors中添加到后续每一个请求headers.Authorization属性中
async login(params: LoginParams & {
goHome?: boolean
}): Promise<UserInfoModel | null> {
try {
const { goHome = true, ...loginParams } = params;
const data = await loginApi(loginParams);
const { token } = data;
// 保存token,在请求拦截器中加入
this.setToken(token);
// 登录请求之后,处理用户信息和路由、菜单信息
return this.afterLoginAction(goHome);
} catch(error) {
return Promise.reject(error);
}
},
afterLoginAction 处理路由、菜单
登录成功之后调用调用permissionStore中的buildRoutesAction,根据当前的权限类型配置,去分别获取不同的路由和菜单信息。在实际业务场景中,可能只会单独从后端获取路由
async afterLoginAction(goHome: boolean): Promise<UserInfoModel | null> {
if (!this.getToken) return null;
// get user info
const userInfo = await this.getUserInfoAction();
const sessionTimeout = this.sessionTimeout;
if (sessionTimeout) {
this.setSessionTimeout(false);
} else {
// 获取菜单
const permissionStore = usePermissionStore();
if (!permissionStore.isDynamicAddedRoute) {
// 动态获取后端菜单信息
const routes = await permissionStore.buildRoutesAction();
routes.forEach((route) => {
// 将获取到的路由信息全部加入路由中
router.addRoute(route as unknown as RouteRecordRaw);
});
// router.addRoute(PAGE_NOT_FOUND_ROUTE as unknown as RouteRecordRaw);
permissionStore.setDynamicAddedRoute(true);
}
// 路由跳转
goHome && (await router.replace(userInfo?.homePath || PageEnum.BASE_HOME));
}
return userInfo;
}
permissionStore
permissionStore中主要是存储菜单信息。根据PermissionModeEnum来判断取哪类菜单,个人项目默认使用BACK后台数据。返回的路由中,需要根据权限进行匹配回显,也需要根据设置的meta属性的内容来筛选符合要求的route
async buildRoutesAction() {
// 获取后台返回的路由信息
const userStore = useUserStore();
let routes: AppRouteRecordRaw[] = [];
// toRaw将响应式数据提取成普通数据
const roleList = toRaw(userStore.getRoleList) || [];
// 固定使用后台路由, 可配置化获取
let enums = {
BACK: PermissionModeEnum.BACK,
ROLE: PermissionModeEnum.ROLE,
ROUTE_MAPPING: PermissionModeEnum.ROUTE_MAPPING
};
/**
*
* @param route 路由
* @returns 判断当前路由是否设置meta,限制角色访问
*/
const routeFilter = (route: AppRouteRecordRaw) => {
const { meta } = route;
const { roles } = meta || {};
if (!roles) return true;
return roleList.some((role) => (roles as Array<any>).includes(role));
};
/**
*
* @param route 路由
* @returns 判断当前路由是否需要被忽略
*/
const routeRemoveIgnoreFilter = (route: AppRouteRecordRaw) => {
const { meta } = route;
const { ignoreRoute } = meta || {};
return !ignoreRoute;
};
const permissionMode = enums['BACK'];
switch (permissionMode) {
// 后台方式控制
case PermissionModeEnum.BACK:
let routeList: AppRouteRecordRaw[] = [];
try {
this.changePermissionCode();
// 获取路由
routeList = (await getMenuList()) as AppRouteRecordRaw[];
} catch (error) {
console.error(error);
}
// Dynamically introduce components 动态引入组件信息
routeList = transformObjToRoute(routeList);
// 将路由信息转化为菜单信息,并保存进store中
const backMenuList = transformRouteToMenu(routeList);
this.setBackMenuList(backMenuList);
routeList = filter(routeList, routeRemoveIgnoreFilter);
routeList = routeList.filter(routeRemoveIgnoreFilter);
routeList = flatMultiLevelRoutes(routeList);
routes = [...routeList];
break;
// 前端方式控制(菜单和路由分开配置)
case PermissionModeEnum.ROLE:
// 初筛子节点是否符合要求
routes = filter(asyncRoutes, routeFilter);
// 筛选根节点是否符合要求
routes = asyncRoutes.filter(routeFilter);
routes = flatMultiLevelRoutes(routes);
break;
// 前端方式控制(菜单由路由配置自动生成)
case PermissionModeEnum.ROUTE_MAPPING:
routes = filter(asyncRoutes, routeFilter);
routes = routes.filter(routeFilter);
// 将路由信息转化为菜单信息
const menuList = transformRouteToMenu(routes, true);
// remove meta.ignoreRoute item
routes = filter(routes, routeRemoveIgnoreFilter);
routes = routes.filter(routeRemoveIgnoreFilter);
this.setFrontMenuList(menuList);
// Convert multi-level routing to level 2 routing
routes = flatMultiLevelRoutes(routes);
break;
}
return routes;
}
CSS
less全局引入
安装插件:npm install less postcss-less
加上 reference 可以解决页面内重复引用导致实际生成的 style 样式表重复的问题。
// vite.config.ts
import { defineConfig } from 'vite'
import path from 'path';
// https://vitejs.dev/config/
export default defineConfig({
css: {
preprocessorOptions: {
// 全局引入
less: {
modifyVars: {
hack: `true; @import (reference) "${path.resolve('src/design/config.less')}";`,
},
javascriptEnabled: true,
}
}
}
})
重点函数