Vue-Vben-Admin项目实践学习

7,028 阅读10分钟

Axios封装

vben文档地址

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

  需要安装的插件 mockjsvite-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配置

   image.png

路由配置表

  在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(主要是当前用户的角色信息)。登录成功后,匹配当前用户的菜单权限:调用permissionStorebuildRoutesAction,根据当前的权限类型配置,去分别获取不同的路由和菜单信息。

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,
      }
    }
  }
})

重点函数

github.com/KTDefTnT/vb…