欢迎来到科站长!

JavaScript

当前位置: 主页 > 网络编程 > JavaScript

vue3+element-plus+vite实现动态路由菜单方式

时间:2025-01-25 09:16:18|栏目:JavaScript|点击:

目录

1. 环境搭建

1.1 新建一个vite搭建的vue3项目

先执行以下命令

npm create vite@latest my-project(你的项目名)

1.2 选择项目框架 vue

1.3 选择语言类型 ts

1.4 执行命令进入到新建的项目文件中

cd  my-project

1.5 下载依赖

npm i

下载项目中需要使用到的环境

npm install vue-router@4 pinia element-plus @element-plus/icons-vue

1.6 完善项目目录结构以及环境配置

1.6.1 先清空App.vue文件中内容,增加router-view作为路由出口





1.6.2 在src目录下新建文件夹layout,在该文件中新建文件AppLayout.vue (文件名看自己)

1.6.3 在src目录下分别新建文件夹store和router分别用来pinia状态管理和路由管理

1.6.3.1 router文件夹中新建两个文件一个index.ts用来初始化路由和存放静态路由一个dynamicRoutes.ts存放处理动态路由

// router/dynamicRoutes.ts
// 更新 initDynamicRoutes,确保 dynamicRoutes 被更新
import router from './index';
import { useRouteStore } from '@/store/index'; // 导入 store
import type { RouteRecordRaw, RouteRecordRedirectOption } from 'vue-router';

// 定义菜单项类型,确保 `name` 是 `string`
type MenuItem = Omit & {
  name: string; // 必须有 name 属性
  path: string; // 必须有 path 属性
  component?: () => Promise; // 用于动态加载组件的路径
  children?: MenuItem[]; // 子路由类型
  redirect?: string; // 调整 redirect 为更简单的 string 类型
  meta?: {
    title: string;
  };
};
// Vite 支持使用特殊的 import.meta.glob 函数从文件系统导入多个模块
const modules: Record Promise> = import.meta.glob('../views/**/**.vue');

// 初始化动态路由
export const initDynamicRoutes = (menuData: MenuItem[]) => {
  const routeStore = useRouteStore(); // 获取 store
  const routerList: MenuItem[] = [];
  const addedRoutes = new Set(); // 用于跟踪已添加的路由,防止重复添加

  // 递归处理路由
  const processRoutes = (routes: MenuItem[]): MenuItem[] => {
    return routes.map((item) => {
      if (addedRoutes.has(item.name)) return null; // 防止重复处理
      addedRoutes.add(item.name); // 标记路由为已处理

      const componentLoader = modules[`../views${item.component}.vue`];
      const route: MenuItem = {
        path: item.path,
        name: item.name as string,
        component: componentLoader , // 提供默认组件以防找不到
        meta: item.meta,
      };

      // 如果有子路由,递归处理
      if (item.children && item.children.length > 0) {
        route.children = processRoutes(item.children);
        route.redirect = route.children[0]?.path; // 默认重定向到第一个子路由
      } else {
        route.children = undefined; // 明确设置为 undefined
      }

      return route;
    }).filter((route) => route !== null) as MenuItem[]; // 过滤掉 null 项
  };

  // 顶级路由处理
  const parentRouter = processRoutes(menuData);

  // 根路由配置
  routerList.push({
    path: '/',
    name: 'home',
    component: () => import('../layout/AppLayout.vue'),
    children: parentRouter, // 顶级路由作为子路由
    redirect: parentRouter[0]?.path || '/', // 确保有默认重定向路径
  });
  
  // 将路由存储到 store 中
  routeStore.dynamicRoutes = routerList;
  // 添加路由到 Vue Router
  routerList.forEach((route) => {
    router.addRoute(route as RouteRecordRaw);
  });
};
// router/index.ts
import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router";
import { useRouteStore } from "@/store";
// 静态路由
const routes: RouteRecordRaw[] = [
  {
    path: "/login",
    name: "login",
    component: () => import("@/views/login/index.vue"),
  },
  {
    path: "/404",
    component: () => import("@/views/error-page/404.vue"),
  },
  {
    path: "/401",
    component: () => import("@/views/error-page/401.vue"),
  },
  // 匹配所有路径
  { path: "/:pathMatch(.*)", redirect: "/login" },
];

// 创建路由
const router = createRouter({
  history: createWebHashHistory(), // 路由模式
  routes, // 静态路由
});

// 路由守卫:初始化时跳转到上次访问的页面
window.addEventListener('DOMContentLoaded', () => {
  const routeStore = useRouteStore()

  const beforeReloadRoute = sessionStorage.getItem('beforeReloadRoute')
  if (beforeReloadRoute) {
    const to = JSON.parse(beforeReloadRoute)
    routeStore.beforeRouter = to.path
    // 清除保存的路由信息
    sessionStorage.removeItem('beforeReloadRoute')
    // 导航回刷新前的路由
    router.replace(to)
    const keys = Object.keys(to)
    if (keys.includes('name')) {
      sessionStorage.setItem('roterPath', JSON.stringify(to.name))
    }
  }
})

// 在页面即将刷新时保存当前路由信息
window.addEventListener('beforeunload', () => {
  const currentRoute = JSON.stringify(router.currentRoute.value)
  sessionStorage.setItem('beforeReloadRoute', currentRoute)
})


export default router;

1.6.3.2 实现路由持久化和白名单,需要在src目录下新建一个permission.ts文件

import { createVNode, render } from 'vue';
import { initDynamicRoutes } from '@/router/dynamicRoutes';
import router from './router/index';
import loadingBar from '@/component/loadingBar.vue';
import Cookies from 'js-cookie'; // 引入 js-cookie
import { useRouteStore } from '@/store/index';
import menuData from '/public/dynamicRouter.json'; // 导入动态菜单数据

const whileList = ['/login']; // 白名单
const Vnode = createVNode(loadingBar);
render(Vnode, document.body);

router.beforeEach(async (to, from, next) => {
  const routeStore = useRouteStore(); // 获取 Pinia 中的路由状态
  const token = Cookies.get('token'); // 从 cookie 获取 token

  // 判断是否有 token,存在则说明用户已登录
  if (token) {
    // 检查是否已经加载过动态路由
    if (routeStore.dynamicRoutes.length === 0) {
      // 检查是否有持久化的动态路由
      const persistedRoutes = sessionStorage.getItem('dynamicRoutes');  // 使用 sessionStorage
      if (persistedRoutes) {
        // 如果有持久化的动态路由,直接从 sessionStorage 加载
        const routerList = JSON.parse(persistedRoutes);
        initDynamicRoutes(routerList); // 动态初始化路由
        routeStore.setDynamicRoutes(routerList); // 将动态路由存入 Pinia
        next({ ...to, replace: true }); // 确保动态路由加载后再跳转
        Vnode.component?.exposed?.startLoading(); // 启动加载条
      } else {
        // 如果没有持久化的动态路由,则使用静态的 dynamicRouter.json
        const dynamicRoutes = initDynamicRoutes(menuData); // 动态初始化路由
        if (dynamicRoutes !== undefined) {
          routeStore.setDynamicRoutes(dynamicRoutes); // 将动态路由存入 Pinia
          sessionStorage.setItem('dynamicRoutes', JSON.stringify(dynamicRoutes)); // 存储动态路由到 sessionStorage
          next({ ...to, replace: true }); // 确保动态路由加载后再跳转
          Vnode.component?.exposed?.startLoading(); // 启动加载条
        } else {
          next('/login'); // 如果没有动态路由信息,跳转到登录页面
        }
      }
    } else {
      next(); // 如果已经加载过动态路由,直接跳转
    }
  } else {
    // 如果没有 token,判断是否在白名单中
    if (whileList.includes(to.path)) {
      next(); // 白名单路由放行
    } else {
      next('/login'); // 否则跳转到登录页
    }
  }
});

router.afterEach(() => {
  Vnode.component?.exposed?.endLoading(); // 结束加载条
});

1.6.3.2 store文件夹下新建文件index.ts初始化pinia仓

// store/index.ts
import { createPinia } from 'pinia';
import { useRouteStore } from './useRouteStore';
import { useUserStore } from './tokenStore';

// 创建 pinia 实例
const pinia = createPinia();

// 将所有 store 模块暴露
export { pinia, useRouteStore, useUserStore };

1.6.3.2 store文件夹下新建文件useRouteStore.ts处理存储动态路由文件

import { defineStore } from 'pinia';
import { ref } from 'vue';
import { initDynamicRoutes } from "@/router/dynamicRoutes"; // 导入初始化动态路由的方法
import type { RouteRecordRaw, RouteRecordRedirectOption } from 'vue-router';

// 定义菜单项类型,确保 `name` 是 `string`
type MenuItem = Omit & {
  name: string; // 必须有 name 属性
  path: string; // 必须有 path 属性
  component?: () => Promise; // 用于动态加载组件的路径
  children?: MenuItem[]; // 子路由类型
  redirect?: string; // 调整 redirect 为更简单的 string 类型
  meta?: {
    title: string;
  };
};
// 定义路由数据 Store
export const useRouteStore = defineStore('route', () => {
  // 存储菜单数据
  const menuData = ref([]); // 根据你的菜单数据结构调整类型

  // 存储动态路由数据
  const dynamicRoutes = ref([]);

  // 存储是否已初始化路由的状态
  const isRoutesInitialized = ref(false);

  // 存储上一次页面刷新的路由
  const beforeRouter = ref('');

  // 初始化动态路由
  const setDynamicRoutes = (menu: any[]) => {
    // 只在未初始化路由时执行
    if (!isRoutesInitialized.value) {
      // 调用 initDynamicRoutes 函数来生成动态路由
      initDynamicRoutes(menu);

      // 将菜单数据存储到状态中
      menuData.value = menu;

      // 设置已初始化状态
      isRoutesInitialized.value = true;
    }
  };

  // 获取动态路由
  const getDynamicRoutes = () => {
    return dynamicRoutes.value;
  };

  // 更新动态路由
  const setUpdatedDynamicRoutes = (routes: MenuItem[]) => {
    dynamicRoutes.value = routes;
  };

  return {
    menuData,
    dynamicRoutes,
    isRoutesInitialized, // 公开这个状态,方便其他地方判断
    setDynamicRoutes,
    getDynamicRoutes,
    setUpdatedDynamicRoutes, // 更新动态路由的函数
    beforeRouter
  };
});

1.6.4 在src目录下新建文件夹plugins,在该文件夹中新建文件element-plus.ts

/* Element-plus组件库 */
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import zhCn from 'element-plus/es/locale/lang/zh-cn'

import { App } from 'vue'

export default {
  install (app: App) {
    app.use(ElementPlus, {
      locale: zhCn
    })
  }
}

1.6.5 需要来配置main.ts,vite.config.ts以及tsconfig.json

1.6.5.1 main.ts配置

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router/index";
import ElementPlus from "./plugins/element-plus";
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
import { pinia } from '@/store/index';  // 导入 store
// 创建 Pinia 实例

// 路由拦截 路由发生变化修改页面title
router.beforeEach((to, from, next) => {
  if (to.meta.title) {
    document.title = to.meta.title;
  }
  next();
});
const app = createApp(App);
// // 自动注册全局组件

app.use(router).use(ElementPlus).use(pinia).mount("#app");
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component);
}

1.6.5.2 vite.config.ts配置

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
    // 设置别名 方便路径引入
      '@': path.resolve(__dirname, 'src'),
    }
  }
})

1.6.5.3 tsconfig.json配置

{ 
  "compilerOptions": 
  { 
    "target": "ESNext", 
    "useDefineForClassFields": true, 
    "module": "ESNext", 
    "moduleResolution": "Node", 
    "strict": true, 
    "jsx": "preserve", 
    "sourceMap": true, 
    "resolveJsonModule": true, 
    "isolatedModules": true, 
    "esModuleInterop": true, 
    "lib": ["ESNext", "DOM"], 
    "skipLibCheck": true, 
    "noEmit": true,
    "paths": {
      "@/*": ["./src/*"]  // 配置路径别名,不做配置会报错
    }
    //就是这个没有设置导致的
     }, 
    // "extends": "./tsconfig.extends.json",
    "include": ["src/**/*.tsx","src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"],
    "references": [{ "path": "./tsconfig.node.json" }]
}

1.6.5.4 此外vue3在插件引入时有些时候会报错无法找到模块"xxx"的声明文件,此时需要在src目录下新建一个env.d.ts文件

 /// 
// 类型补充、环境变量
declare module "*.vue" {
  import type { DefineComponent } from "vue";
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
  const component: DefineComponent<{}, {}, any>;
  export default component;
}

// eslint-disable-next-line no-unused-vars
interface ImportMetaEnv {
  readonly VITE_APP_TITLE: string;
  readonly VITE_API_BASEURL: string;
  // 更多环境变量...
}

// 如果遇到路径缺失找不到的情况
// 无法找到模块“xxx”的声明文件,就将该模块加入到下列代码中进行补充声明
declare module "xxxx";

1.7 因为考虑是纯前端模拟后端给的路由数据

所以我自己模拟一个json文件,需在public文件夹中新建dynamicRouter.json来存放模拟后端返回的路由数据,后期从接口获取可进行更改

[
  {
    "path": "/principle", 
    "name": "principle", 
    "component": "/principle/index",
    "meta": {
      "title": "Vue3响应式原理"
    }
  },
  {
    "path": "/ref",
    "name": "ref", 
    "meta": {
      "title": "ref类"
    },
    "children": [
      {
        "path": "/ref/index", 
        "name": "ref", 
        "component": "/ref/common/ref", 
        "meta": {
          "title": "ref"
        }
      },
      {
        "path": "/ref/toRaw", 
        "name": "toRaw", 
        "component": "/ref/common/toRaw", 
        "meta": {
          "title": "toRaw"
        }
      },
      {
        "path": "/ref/toRef", 
        "name": "toRef", 
        "component": "/ref/common/toRef", 
        "meta": {
          "title": "toRef"
        }
      },
      {
        "path": "/ref/toRefs", 
        "name": "toRefs", 
        "component": "/ref/common/toRefs", 
        "meta": {
          "title": "toRefs"
        }
      },
      {
        "path": "/ref/isRef", 
        "name": "isRef", 
        "component": "/ref/no-common/isRef", 
        "meta": {
          "title": "isRef"
        }
      },
      {
        "path": "/ref/Ref", 
        "name": "Ref", 
        "component": "/ref/no-common/Ref", 
        "meta": {
          "title": "Ref"
        }
      },
      {
        "path": "/ref/shallowRef", 
        "name": "shallowRef", 
        "component": "/ref/no-common/shallowRef", 
        "meta": {
          "title": "shallowRef"
        }
      },
      {
        "path": "/ref/triggerRef", 
        "name": "triggerRef", 
        "component": "/ref/no-common/triggerRef", 
        "meta": {
          "title": "triggerRef"
        }
      }
    ]
  }
]

如下是文件对应的位置

到目前为止整体的环境已经搭建完善,大概结构如下

2. 在views文件夹下新建文件夹login

在其中新建文件index.vue



3. layout中制作动态路由菜单






总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持科站长。

上一篇:vue图片压缩与批量上传方式

栏    目:JavaScript

下一篇:Windows中彻底删除Node.js环境(以及npm)的方法

本文标题:vue3+element-plus+vite实现动态路由菜单方式

本文地址:https://fushidao.cc/wangluobiancheng/3097.html

广告投放 | 联系我们 | 版权申明

申明:本站所有的文章、图片、评论等,均由网友发表或上传并维护或收集自网络,属个人行为,与本站立场无关。

如果侵犯了您的权利,请与我们联系,我们将在24小时内进行处理、任何非本站因素导致的法律后果,本站均不负任何责任。

联系QQ:257218569 | 邮箱:257218569@qq.com

Copyright © 2018-2025 科站长 版权所有冀ICP备14023439号