两行代码完美解决vue3动态路由刷新404的问题

8,100 阅读3分钟

前端动态方案

  • 模拟动态路由

本质上是前端路由已经存在,且已经填加好了,通过路由拦截配合后端权限字段或者直接本地判断进行权限认证。

优点:对后端依赖相对比较少。难度相对比较低

缺点:

  1. 本质不太安全,因为路由是真实存在的
  2. 每次跳转都需要在路由拦截进行权限判断,性能可能会有损失
  3. 不支持复杂的功能,如菜单管理、路由排序等
  • 真实动态路由

通过路由api addRoute实现

首先定义扁平化的数据格式

interface ReqRouterListItem {
  id: number; //路由的id
  pid: number; //父级id  顶级菜单的pid 0
  index: number; //层级
  component: string; //组件的文件地址,views下
  path: string; //浏览器的地址
  title: string; //标题
  icon: string; //图标
  menu: boolean; //是否为目录
  redirect: string; //重定向
}
interface Menu {
  path: string; //地址
  icon: string; //图标
  title: string; //标题
  children: Array<Menu>; //子项
}


先设置模拟数据


     export const RoleRouter = [
      {
        id: 1, //路由的id
        pid: 0, //父级id  顶级菜单的pid 0
        index: 1, //层级
        component: "", //组件的文件地址,views下
        path: "/Setting", //浏览器的地址
        title: "系统设置", //标题
        icon: "setting", //图标
        menu: true, //是否为目录
        redirect: "/Setting/router" //重定向
      },
      {
        id: 2, //路由的id
        pid: 1, //父级id  顶级菜单的pid 0
        index: 1, //层级
        component: "/Setting/router", //组件的文件地址,views下
        path: "/Setting/router", //浏览器的地址
        title: "路由配置", //标题
        icon: "", //图标
        menu: true, //是否为目录
        redirect: "" //重定向
      },
      {
        id: 3, //路由的id
        pid: 1, //父级id  顶级菜单的pid 0
        index: 2, //层级
        component: "/Setting/system", //组件的文件地址,views下
        path: "/Setting/system", //浏览器的地址
        title: "系统配置", //标题
        icon: "", //图标
        menu: true, //是否为目录
        redirect: "" //重定向
      },
      {
        id: 4, //路由的id
        pid: 0, //父级id  顶级菜单的pid 0
        index: 1, //层级
        component: "/Detail/product", //组件的文件地址,views下
        path: "/Detail/product/:id", //浏览器的地址
        title: "产品详情", //标题
        icon: "", //图标
        menu: false, //是否为目录
        redirect: "" //重定向
      },
      {
        id: 5, //路由的id
        pid: 0, //父级id  顶级菜单的pid 0
        index: 1, //层级
        component: "/Detail/userInfo", //组件的文件地址,views下
        path: "/Detail/userInfo", //浏览器的地址
        title: "个人信息", //标题
        icon: "", //图标
        menu: false, //是否为目录
        redirect: "" //重定向
      }
    ];

路由算法

  • 生成动态路由


import router from "@/router";
const AllRouter = import.meta.glob("@/views/**/*.vue");
export const TransformObjectToRoute = (RouterList: Array<ReqRouterListItem>) => {
  RouterList.forEach((item) => {
    const { path, components, title, menu, redirect } = item;
    if (item.menu && item.component === "") {
      router.addRoute("layout", {
        name: path,
        path,
        redirect,
        meta: {
          title,
          menu
        }
      });
    }
    if (item.menu && item.component !== "") {
      router.addRoute("layout", {
        name: path,
        path,
        component: AllRouter[`/src/views${component}/index.vue`],
        meta: {
          title,
          menu
        }
      });
    }
    if (!item.menu && item.component === "") {
      router.addRoute({
        name: path,
        redirect,
        path,
        meta: {
          title,
          menu
        }
      });
    }
    if (!item.menu && item.component !== "") {
      router.addRoute({
        name: path,
        component: AllRouter[`/src/views${component}/index.vue`],
        path,
        meta: {
          title,
          menu
        }
      });
    }
  });
};



  • 生成菜单

AsideVue.vue

<template>
  <t-aside>
    <t-menu v-model="$route.path" :theme="theme" expand-mutex :collapsed="collapsed">
      <template #logo> logo </template>
      <MenuVue :menu="useGlobalStore().GetMenu()"></MenuVue>
      <template #operations>
        <t-button @click="useGlobalStore().collapsed = !collapsed" variant="text" shape="square">
          <template #icon><t-icon name="view-list" /></template>
        </t-button>
      </template>
    </t-menu>
  </t-aside>
</template>

<script setup lang="ts">
import MenuVue from "./MenuVue.vue";
import { storeToRefs } from "pinia";
import { useGlobalStore } from "@/stores";
const { collapsed, theme } = storeToRefs(useGlobalStore());
</script>

MenuVue.vue


<template>
  <template v-for="item in props.menu" :key="item.path">
    <t-submenu v-if="!!item.children.length" :value="item.path">
      <template #icon>
        <t-icon v-if="!!item.icon" :name="item.icon" />
      </template>
      <template #title>
        <span>{{ item.title }}</span>
      </template>
      <MenuVue :menu="item.children" />
    </t-submenu>
    <t-menu-item v-else :value="item.path" :to="item.path">
      <template #icon>
        <t-icon v-if="!!item.icon" :name="item.icon" />
      </template>
      {{ item.title }}
    </t-menu-item>
  </template>
</template>

<script setup lang="ts">
import MenuVue from "./MenuVue.vue";
const props = defineProps<{
  menu: Array<Menu>;
}>();
</script>


global.ts


GetMenu() {
      const menu = this.ReqRouterList.filter((item) => !!item.menu);
      const TempMenu: any = (pid: number | string) => {
        return menu
          .filter((item) => item.pid == pid)
          .sort((a, b) => Number(a.index) - Number(b.index))
          .map((item) => {
            const children = TempMenu(item.id);

            return {
              path: item.path,
              icon: item.icon,
              title: item.title,
              children
            };
          });
      };

      return TempMenu(0);
}


路由文件
router/index.ts

import { createRouter, createWebHistory } from "vue-router";
import Layout from "@/layout/index.vue";
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: "/",
      name: "layout",
      component: Layout
    },
    {
      path: "/Login",
      name: "Login",
      component: () => import("@/views/Login/index.vue"),
      meta: {
        title: "登录"
      }
    },
    {
      path: "/404",
      name: "/404",
      component: () => import("@/views/Error/404.vue")
    },
    {
      path: "/:catchAll(.*)",
      redirect: "/404"
    }
  ]
});
router.beforeEach((to: any, form, next) => {
  window.document.title = to.meta.title ? to.meta.title : import.meta.env.VITE_APP_TITLE;
  return next();
});

export default router;



这样就可以实现动态路由了。 但是有一个问题,如果我们刷新会导致404.因为你刷新了。动态路由是不存在的。因为addRoute是异步的。刷新完成了。但是路由还没有添加完成。

解决方案

本质是刷新的时候加载新的路由,然后跳转。

<template>
  <t-loading class="loading" :loading="loading" show-overlay>
    <RouterView />
  </t-loading>
</template>

<script setup lang="ts">
import { useGlobalStore } from "./stores/global";
import router from "./router";
const { theme, loading } = storeToRefs(useGlobalStore());
const { updateTheme, updateAuth } = useGlobalStore();
const path = router.options.history.location;
onBeforeMount(async () => {
  updateTheme(theme.value);
  if (useGlobalStore().token !== "") {
    await updateAuth();
    router.replace(path);
  }
});
</script>

router.options.history.location是否放在外边跟你数据有关,如果是同步的,在哪里无所谓。但是要通过请求,一定要放在外边。否则不是最新的地址,因为vue-router是同步的,而请求是异步的。在请求完成之前,路由已经完成了跳转。