vue-router实现动态路由

1. 什么是动态路由

动态路由不同于常见的静态路由,静态路由 是前端配置好的一套路由列表,在项目中登录后即可访问配置好的路由页面,也不会因为账号的不同有所限制;而 动态路由 则相反,如果账号权限不同,我们将会设置不同的路由列表,从而限制账号访问的页面。 动态路由是是可变的,而不是写死的,常见的是通过接口返回路由数据来进行匹配,如果匹配到和权限一至的路由就动态增加到项目中。

2. 动态路由的数据存储

这里讲述下项目中的路由数据存储方式:

2.1 后端存储路由对应的 code 码
code 码映射
// 首页 => 1001
// 资源管理 => 1002
// 权限管理 => 1003
// 审批管理 => 1004

接口返回值会根据不同的账号权限返回 code 码集合
// 管理员: [1001, 1002, 1003, 1004]
// 小明: [1001, 1002]
...
2.2 前端创建数组,将所有的 code 映射成一个路由数据

路由组件的获取,这里使用的是 Map对象,并且给每一个路由提供一个 key;路由列表中的 compontent 字段的值,将通过 keyMap 对象中获取

// 2.1 设置 Map 对象存储懒加载组件
const asyncRouter = new Map();
asyncRouter.set("home", () => import("@/views/home/index.vue"));

// 2.2 设置 code 码映射的路由数组,将其和返回值 code 对比取出匹配到的数据
const asyncRouter = [
  {
    path: "/home",
    name: "/home",
    meta: {
      title: "首页",
      cn: "首页",
      en: "Home",
      icon: "home",
    },
    component: routerMapping.get("home"),
    code: "1001",
  },
  // ...
];

3. 使用到的 API

这里主要讲述 vue-router 中提供的可以进行动态添加路由的 API

3.1 Vue Router 全局前置守卫

router.beforeEach 会注册一个全局前置守卫,当我们在路由跳转时,会自动触发 路由守卫函数,主要用来做一些页面进入的限制,比如:没有登录就不能进入某些特定页面。官网解释

// 常规写法
router.beforeEach((to, from, next) => {
  // to: 跳转到哪个路由
  // from: 从哪个路由跳转过来
  // next: 跳转函数,可以跳转到具体的 url
});

// 含有异步操作的方法
router.beforeEach(async (to, from, next) => {
  const res = await fetch("****");
  // to: 跳转到哪个路由
  // from: 从哪个路由跳转过来
  // next: 跳转函数,可以跳转到具体的 url
});
3.2 router.addRoutes()

addRoutes() 可以在应用程序运行的时候添加路由,它表示注册一个新的路由到我们程序上;如果新增加的路由与当前位置相匹配,就需要手动使用 router.push()router.replace() 导航,才能正常展示新的路由页面。官网解释

// 添加路由
router.addRoute({ path: "/about", component: About });

// 将嵌套路由添加到现有的路由中
router.addRoute("home", { path: "settings", component: AdminSettings });

// 手动跳转
router.replace("/about")

4. 具体的实现思路

这里主要讲述实现的思路,以及过程。

4.1 路由守卫中发送请求获取路由数据
router.beforeEach(async (to, from, next) => {
  if (isLogin()) {
    let { data } = await axios.get("/getAuthorList");
  } else {
    /* 没有token值*/
    const isWihte = whiteList.some((v) => to.path.indexOf(v) !== -1);
    // 在免登录白名单,直接进入;否则全部重定向到登录页
    if (isWihte) {
      next();
    } else {
      next(`/login`);
    }
  }
});

由于路由守卫函数支持 async await 所以需要进行 async 改写,发送请求。这里主要是获取到该用户的权限菜单 code

4.2 数据处理

将获取到的数据和前端存储的映射对象进行 code 码对比,获取到符合的路由信息,并组成路由对象的格式

let { data } = await axios.get("/getAuthorList");
const routerList = handleRouter(data);

// 处理的数据格式为
let routerList = [
  {
    path: 'path',
    name: 'path',
    meta: {
      title: 'title',
      cn: 'name',
      en: 'enName',
      icon: 'icon',
    },
    component: VueCompontent,
    children: [***],
  },
];
4.3 addRoutes() 动态添加路由

这一步主要是添加路由到指定的路由中,或者直接添加即可;添加路由后需要动态添加一个 404,防止跳转到不存在的页面;最后需要手动执行下 next() 进行跳转。这样就能跳转到动态添加的页面了。

routerList.forEach((item) => {
  router.addRoute("home", item);
});

// 添加404路由:vur-router 4.x版本写法
router.addRoute({ path: "/:catchAll(.*)", redirect: "/404" });

// 跳转
next({ path: to.path, replace: true });
4.4 pinia 仓库存储数据,增加判断

当我们进入到页面时,页面的菜单栏数据应该与我们获取到的权限数据保持一致,所以我们需要增加数据存储,来展示有效的菜单。

当我们在跳转时,由于请求时在守卫函数中处理的,所以,当我们跳转时,必定会重复被拦截,从而导致页面 死循环,所以我们需要增加一个判断,这时候就可以利用我们存储的值进行判断,来避免死循环。

// pinia
export const useMenuStore = defineStore("menu", () => {
  let routerList = ref([]);

  const setRouterList = (routes) => {
    routerList.value = routes;
  };
});

// router.beforEach
routerList.forEach((item) => {
  router.addRoute("home", item);
});
// ...
menuStore.$patch((state) => {
  state.routerList = routerList;
});

// 死循环判断
if (menuStore.routerList.length === 0) {
  // 在这里进行发送请求即可
  let { data } = await axios.get("/getAuthorList");
  const routerList = handleRouter(data);
} else {
  next();
}
4.4 菜单渲染

项目中使用的是 element-plus 的组件,所以直接从仓库中取出数据循环即可

<el-menu
  :default-active="currIndex"
  :unique-opened="true"
  :router="true"
  :collapse-transition="false"
>
  <template v-for="item in menuList" :key="item.path">
    <!-- 一级菜单 -->
    <el-sub-menu
      v-if="item.children && item.children.length > 0"
      :index="item.path"
      popper-class="menu-class"
    >
      <template #title>
        <i class="iconfont" :class="'icon-' + item.meta?.icon"></i>
        <span>{{ item.meta?.title }}</span>
      </template>
      <el-menu-item-group>
        <!-- 二级菜单 -->
        <div v-if="isCollapse" class="secMenuTitle">{{ item.meta?.cn }}</div>
        <el-menu-item
          v-for="itemChild in item.children"
          :index="itemChild.path"
          :key="itemChild.path"
        >
          <span>{{ itemChild.meta?.cn }}</span>
        </el-menu-item>
      </el-menu-item-group>
    </el-sub-menu>

    <!-- 一级菜单:无子菜单 -->
    <el-menu-item v-else :index="item.path">
      <i class="iconfont" :class="'icon-' + item.meta?.icon"></i>
      <template #title>
        <span style="font-size: 16px" class="menuTitle"
          >{{ item.meta?.cn }}</span
        >
      </template>
    </el-menu-item>
  </template>
</el-menu>

<script setup>
  import { storeToRefs } from "pinia";
  import { useMenuStore } from "@/store/menu";
  const {
    routerList: menuList,
    isCollapse,
    currIndex,
  } = storeToRefs(menuStore);
</script>
4.5 核心代码
// 路由守卫
router.beforeEach(async (to, from, next) => {
  const menuStore = useMenuStore();
  // 判断是否登录
  if (isLogin()) {
    if (menuStore.routerList.length === 0) {
      let { data } = await getAsyncRouter();
      const routerList = handleRouterItem(data);
      menuStore.$patch((state) => {
        state.routerList = routerList;
      });
      routerList.forEach((item) => {
        router.addRoute("home", item);
      });
      router.addRoute({ path: "/:catchAll(.*)", redirect: "/404" });
      next({ path: to.path, replace: true });
    } else {
      next();
    }
  } else {
    // 白名单可以跳转,否则全部重定向到登录页
    const isWihte = whiteList.some((v) => to.path.indexOf(v) !== -1);
    if (isWihte) {
      next();
    } else {
      next(`/login`); 
    }
  }
});

5. 遇到的问题

Q: 路由死循环?

A: 死循环是因为路由每次跳转时,都会被路由拦截器拦截,由于是动态添加的路由,所以需要手动 next({path: to.path}) 跳转,而每次手动 next 跳转时又会被拦截,所以此过程会不断重复导致死循环。

Q: router 文件中,使用 pinia 报错?

A: 在 Vue 3 中,无论 main.js 里的 app.use(pinia) 写在 app.use(router) 前面还是后面,vue-router,总是先初始化,所以会出现 pinia 使用报错。所以我们在使用 pinia 时需要在 router.beforeEach 函数中进行仓库初始化。

// router/index.ts
import { useMenuStore } from "@/store/menu";
// 写在这里会报错
const menuStore = useMenuStore();

router.beforeEach(async (to, from, next) => {
  // ***
});

// 正常获取
router.beforeEach(async (to, from, next) => {
  // 不报错
  const menuStore = useMenuStore();
  // ***
});
全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务