vue-router动态路由的使用

这是一个vue动态路由的使用,主要是记录我遇到的一个问题,就是在使用vue Router的动态路由的时候,npm run dev能正常运行,访问到对应的路由。但是npm run build打包之后,就无法访问动态路由了,报如下所示的错误

Failed to load resource: the server responded with a status of 404 (Not Found)
TypeError: Failed to fetch dynamically imported module: http://localhost:8848/views/front/CaiDan1.vue
Uncaught (in promise) TypeError: Failed to fetch dynamically imported module: http://localhost:8848/views/front/CaiDan1.vue
Failed to load resource: the server responded with a status of 404 (Not Found)
TypeError: Failed to fetch dynamically imported module: http://localhost:8848/views/front/CaiDan2.vue
Uncaught (in promise) TypeError: Failed to fetch dynamically imported module: http://localhost:8848/views/front/CaiDan2.vue

在这里插入图片描述
解决办法,在后面。顺便把我遇到的问题记录下来了,由于我这里写了如何动态添加路由,如果是想直看我是怎么,解决上述问题的,请直接看目录打包之后动态路由失效问题,以及解决办法,就可以了
在这里插入图片描述
完整的代码在这里:vue3动态路由配置打包无法访问: vue3动态路由配置打包无法访问,这个是记录一下,方便后面用到这个代码的时候直接拉下来,免得到时候再建立一个新的 (gitee.com)

创建项目

代码在后面,这是先截图说明一下
创建vue3+vite+ts 的项目在这里有,Home | Vite中文网 (vitejs.cn),创建项目的时候,记得修改一下这里。
在这里插入图片描述
改成如下图所示
在这里插入图片描述
这样改的原因是因为,如果不改成Node,element plus的import 引入会爆红,虽然npm run dev可以正常运行,但是npm run build就会有问题,会检测ts语法问题。导致打包不成功的。具体看这里有说明:vite+ts+elementplus运行正常打包报错_vite 打包失败_m0_62317155的博客-CSDN博客 。还有一种解决办法,就是打包的时候跳过ts检查,上面的不用改。如下图所示,进行配置,就可以在npm run build的时候跳过ts语法检查了。从而可以像正常运行环境一样,有语法爆红也可以正常访问网站,这个看自己的选择,我这里还是保持语法检查吧。
在这里插入图片描述
通过vite创建的vue3项目,都会有一个公共CSS样式文件,我们需要修改一下,不使用创建项目时默认的公共CSS样式,以符合我们需要的样式,如下图所示:这里说明一下,使用vite创建项目自带的公共样式文件,在main.ts中引用了,否则会不生效的。这个在创建的时候就已经配置好了。具体可以去看一下main.ts就知道了
在这里插入图片描述

使用vue Router,安装 | Vue Router (vuejs.org)动态路由 | Vue Router (vuejs.org),路由配置,可以看官网,我这里先截图说明一下。在src目录下创建一个router目录和views目录,router目录进行路由配置,views目录存放视图组件,也就是路由跳转展示的vue组件。具体如下图所示,路由配置和views的视图组件的代码在后面,这里先截图说明一下,先了解过程再看代码。
在这里插入图片描述
然后子在main.ts中引入路由配置,图中element plus的配置,看element plus的官网,里面有很详细的配置教程==》这里我使用到了element plus的这个组件库,安装 | Element Plus (element-plus.org)
在这里插入图片描述
到这里,整个项目过程算是搭建起来了,关于以上内容可以自行去官网看,这里就不赘述了。接下来的代码就是能够直接根据后端返回的动态菜单和动态路由列表进行实现对路由的动态权限设置了。这个权限是根据RBAC来进行控制的。对于一些地方,代码中都有详细的说明。建议先看一下这个vue-router个人的记录_m0_62317155的博客-CSDN博客

直接上代码

在这里插入图片描述

动态路由 | Vue Router (vuejs.org)
目录结构如下图所示:
在这里插入图片描述
根据上图目录,看一下下图所示的路由关系图,便于理解
在这里插入图片描述

App.vue代码

<template>
  <div style="width: 100%; height: 100%;">
    <!-- 路由占位,跟路由展示的位置 -->
    <router-view></router-view>
  </div>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>

CaiDan1.vue代码

注意代码组件所在的位置,这里在views/front目录下

<template>
  <div style="width: 100%; height: 100%; background-color: #c1ffa8;">
    <h1>这是菜单11111的界面</h1>
  </div>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>

CaiDan2.vue代码

注意代码组件所在的位置,这里在views/front目录下

<template>
  <div style="width: 100%; height: 100%; background-color: #c2b04c;">
    <h1>这是菜单222222的界面</h1>
  </div>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>

CaiDan3.vue代码

<template>
  <div style="width: 100%; height: 100%; background-color: #ff5b5b;">
    <h1>这是菜单3333333的界面</h1>
  </div>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>

CaiDan4.vue代码

<template>
  <div style="width: 100%; height: 100%; background-color: #8cbeff;">
    <h1>这是菜单444444444的界面</h1>
  </div>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>

CaiDan6.vue代码

<template>
  <div style="width: 100%; height: 100%; background-color: #a592c7;">
    <h1>这是菜单6666666的界面</h1>
  </div>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>

Home.vue代码

<template>
  <!-- 这是首页内容 -->
  <div class="home-vue">
    <!-- 以下就是element plus的一些布局组件了 -->
    <div class="common-layout">
      <el-container>
        <!-- 侧边栏 -->
        <el-aside width="200px">
          <!-- 下面这一点代码可以考虑封装为一个Component组件,然后引用 -->
          <div style="height: 64px; padding: 16px;">
            XXX系统
          </div>
          <div>
            <el-menu router default-active="shouye" class="el-menu-vertical-demo" @open="handleOpen" @close="handleClose">
              <template v-for="item in menuList">
                <el-sub-menu v-if="item.children && item.children.length > 0" :index="item.path">
                  <template #title>
                    <el-icon><icon-menu /></el-icon>
                    <span>{{ item.menuName }}</span>
                  </template>
                  <el-menu-item v-for="item2 in item.children" :key="item2.menuId" :index="item2.path">
                    <el-icon><icon-menu /></el-icon>
                    <span>{{ item2.menuName }}</span>
                  </el-menu-item>
                </el-sub-menu>
                <el-menu-item v-else :index="item.path">
                  <el-icon><icon-menu /></el-icon>
                  <span>{{ item.menuName }}</span>
                </el-menu-item>
              </template>
            </el-menu>
          </div>
        </el-aside>
        <el-container>
          <!-- 头部 -->
          <el-header style="width: 100%; height: 56px; line-height: 56px; background-color: pink;">
            <el-row justify="end" align="middle" style="width: 100%; height: 56px;">
              <!-- 下面这一点代码可以考虑封装为一个Component组件,然后引用  -->
              <el-button @click="logout">退出登录</el-button>
            </el-row>
          </el-header>
          <!-- 主要内容显示区 -->
          <el-main style="width: 100%; height: 100%;">
            <router-view></router-view>
          </el-main>
        </el-container>
      </el-container>
    </div>
  </div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import {
  Menu as IconMenu,
} from '@element-plus/icons-vue'
// 添加路由,和重置路由
import { resetRoutes } from '../router';
import { useRouter } from 'vue-router';
const router = useRouter()

interface Menu {
  menuId?: number,
  pid?: number,
  path?: string,
  menuName?: string,
  component?: string,
  children?: Menu[]
}

/**
 * 退出登录
 */
const logout = () => {
  router.push('/')
  // 重置路由
  resetRoutes()
  // 清楚缓存的菜单列表信息
  localStorage.removeItem("menuList")
}

const menuList = ref<Menu[]>([
  {
    path: 'shouye',
    menuName: "首页",
    component: 'views/ShouYe.vue'
  }
])

const handleOpen = (key: string, keyPath: string[]) => {
  console.log(key, keyPath)
}
const handleClose = (key: string, keyPath: string[]) => {
  console.log(key, keyPath)
}

onMounted(() => {
  const menuListString = localStorage.getItem("menuList")
  if (menuListString) {
    const menuListStorage = JSON.parse(menuListString)
    menuList.value = menuList.value.concat(menuListStorage)
  }
})

</script>
<style scoped>
.home-vue {
  width: 100%;
  height: 100%;
}

.home-vue .common-layout {
  width: 100%;
  height: 100%;
}

.el-container {
  width: 100%;
  height: 100%;
}

.el-aside {
  background-color: aquamarine;
}
</style>

Login.vue代码

这里我随便设置了两个,用户,信息。这两个用户的树形菜单信息和路由信息我直接写死在代码中了,这里就不发送axios请求获取了。直接先写死吧,它们具有的信息,具体看代码下,很长的那两个if里面就知道了,可以复制出来,然后到json格式化的网站(JSON在线 | JSON解析格式化—SO JSON在线工具JSON在线解析及格式化验证 - JSON.cn)格式化看一下数据,稍微对比一下。两个用户具有不同的权限的。

<template>
  <div class="login-vue">
    <div class="logn-vue-container">
      <el-row style="margin-bottom: 16px;">
        <h1>登录系统</h1>
      </el-row>
      <el-row justify="center" style="width: 100%;">
        <el-form :model="form" style="width: 100%;">
          <el-form-item>
            <el-input v-model="form.name" placeholder="请输入用户名" style="width: 100%;" />
          </el-form-item>
          <el-form-item>
            <el-input v-model="form.password" placeholder="请输入密码" style="width: 100%;" />
          </el-form-item>
          <el-form-item>
            <el-button type="primary" style="width: 100%;" @click="onSubmit">登录</el-button>
          </el-form-item>
        </el-form>
      </el-row>
    </div>
  </div>
</template>

<script lang="ts" setup>
import { reactive } from 'vue'
import { ElMessage } from 'element-plus'
// 添加路由,和重置路由
import { resetRoutes, addServerRoutes } from '../router';
import { useRouter } from 'vue-router';
const router = useRouter()

// do not use same name with ref
const form = reactive({
  name: 'admin',
  password: '123456'
})

/**
 * 登录方法
 */
const onSubmit = () => {
  // 登录发送axios请求前,先重置一下路由,避免上一次登录的动态路由还留在路由表中,造成权限冲突
  // router目录的index.ts中的重置路由方法
  resetRoutes()
  // 同理,也要清除一下上次登录存储的菜单列表,这个localStorage已经处理过了,即使没有也不会报错的,所以放心清理
  // 保存这个数据的在登录那里的代码中有,就在这个文件后面
  localStorage.removeItem("menuList")

  // 发送axios请求,登录成功,跳转,返回用户信息和用户具有的菜单列表和路由列表,这里直接一些数据吧,实际情况是发送axios请求的,跟后端交互的
  // 这里只是模拟一下发送请求得到的结果信息
  if (form.name === 'admin' && form.password === '123456') {
    const userInfo = {
      userName: 'admin',
      // 这些字段应该是后端返回的数据,也可以设置一些icon的字段,这里就搞这么麻烦了,反正也是模拟一下的
      // 菜单栏树形数据信息
      menuList: [
        {
          "menuId": 1,
          "pid": 0,
          "menuName": "菜单1",
          "path": "caidan1",
          "component": "views/front/CaiDan1.vue",
          "children": []
        },
        {
          "menuId": 2,
          "pid": 0,
          "menuName": "菜单2",
          "path": "caidan2",
          "component": "views/front/CaiDan2.vue",
          "children": []
        },
        {
          "menuId": 3,
          "pid": 0,
          "menuName": "菜单3",
          "path": "caidan3",
          "component": "views/CaiDan3.vue",
          "children": []
        },
        {
          "menuId": 4,
          "pid": 0,
          "menuName": "菜单4",
          "path": "caidan4",
          "component": "views/CaiDan4.vue",
          "children": []
        },
        {
          "menuId": 5,
          "pid": 0,
          "menuName": "菜单5",
          "path": "",
          "component": "",
          "children": [
            {
              "menuId": 6,
              "pid": 5,
              "menuName": "菜单5的子菜单-1",
              "path": "zicaidan1",
              "component": "views/ZiCaiDan1.vue"
            },
            {
              "menuId": 7,
              "pid": 5,
              "menuName": "菜单5的子菜单-2",
              "path": "zicaidan2",
              "component": "views/ZiCaiDan2.vue"
            },
            {
              "menuId": 8,
              "pid": 5,
              "menuName": "菜单5的子菜单-3",
              "path": "zicaidan3",
              "component": "views/ZiCaiDan3.vue"
            }
          ]
        },
        {
          "menuId": 9,
          "pid": 0,
          "menuName": "菜单6",
          "path": "caidan6",
          "component": "views/CaiDan6.vue",
          "children": []
        }
      ],
      // 路由列表信息
      routerList: [
        {
          "menuId": 1,
          "pid": 0,
          "menuName": "菜单1",
          "path": "caidan1",
          "component": "views/front/CaiDan1.vue",
          "children": []
        },
        {
          "menuId": 2,
          "pid": 0,
          "menuName": "菜单2",
          "path": "caidan2",
          "component": "views/front/CaiDan2.vue",
          "children": []
        },
        {
          "menuId": 3,
          "pid": 0,
          "menuName": "菜单3",
          "path": "caidan3",
          "component": "views/CaiDan3.vue",
          "children": []
        },
        {
          "menuId": 4,
          "pid": 0,
          "menuName": "菜单4",
          "path": "caidan4",
          "component": "views/CaiDan4.vue",
          "children": []
        },
        {
          "menuId": 6,
          "pid": 5,
          "menuName": "菜单5的子菜单-1",
          "path": "zicaidan1",
          "component": "views/ZiCaiDan1.vue"
        },
        {
          "menuId": 7,
          "pid": 5,
          "menuName": "菜单5的子菜单-2",
          "path": "zicaidan2",
          "component": "views/ZiCaiDan2.vue"
        },
        {
          "menuId": 8,
          "pid": 5,
          "menuName": "菜单5的子菜单-3",
          "path": "zicaidan3",
          "component": "views/ZiCaiDan3.vue"
        },
        {
          "menuId": 9,
          "pid": 0,
          "menuName": "菜单6",
          "path": "caidan6",
          "component": "views/CaiDan6.vue",
          "children": []
        }
      ]
    }
    // 请求成功之后,动态添加路由
    addServerRoutes("home", userInfo.routerList)
    // 保存用户的树形带单数据到localStorage中,也可以保存到pinina中
    localStorage.setItem("menuList", JSON.stringify(userInfo.menuList))
   
    ElMessage.success('登录成功')
    router.push('/home')
  } 
  if (form.name === 'zhangsan' && form.password === '123456') {
    const userInfo = {
      userName: 'admin',
      menuList: [
        {
          "menuId": 3,
          "pid": 0,
          "menuName": "菜单3",
          "path": "caidan3",
          "component": "views/CaiDan3.vue",
          "children": []
        },
        {
          "menuId": 4,
          "pid": 0,
          "menuName": "菜单4",
          "path": "caidan4",
          "component": "views/CaiDan4.vue",
          "children": []
        },
        {
          "menuId": 5,
          "pid": 0,
          "menuName": "菜单5",
          "path": "",
          "component": "",
          "children": [
            {
              "menuId": 8,
              "pid": 5,
              "menuName": "菜单5的子菜单-3",
              "path": "zicaidan3",
              "component": "views/ZiCaiDan3.vue"
            }
          ]
        },
        {
          "menuId": 9,
          "pid": 0,
          "menuName": "菜单6",
          "path": "caidan6",
          "component": "views/CaiDan6.vue",
          "children": []
        }
      ],
      routerList: [
        {
          "menuId": 3,
          "pid": 0,
          "menuName": "菜单3",
          "path": "caidan3",
          "component": "views/CaiDan3.vue",
          "children": []
        },
        {
          "menuId": 4,
          "pid": 0,
          "menuName": "菜单4",
          "path": "caidan4",
          "component": "views/CaiDan4.vue",
          "children": []
        },
        {
          "menuId": 8,
          "pid": 5,
          "menuName": "菜单5的子菜单-3",
          "path": "zicaidan3",
          "component": "views/ZiCaiDan3.vue"
        },
        {
          "menuId": 9,
          "pid": 0,
          "menuName": "菜单6",
          "path": "caidan6",
          "component": "views/CaiDan6.vue",
          "children": []
        }
      ]
    }
    // 请求成功之后,动态添加路由
    addServerRoutes("home", userInfo.routerList)
    // 保存用户的树形带单数据到localStorage中,也可以保存到pinina中
    localStorage.setItem("menuList", JSON.stringify(userInfo.menuList))
   
    ElMessage.success('登录成功')
    router.push('/home')
  } 
  if(form.name !== "admin" && form.name !== 'zhangsan' && form.password !== '123456') {
    ElMessage.error('用户名或密码错误,登录失败!')
  }
}
</script>

<style scoped>
.login-vue {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  /* public目录的图片,可以这样写,不过这里会报一个警告,说public是可以省略的 */
  /* background-image: url('/public/vite.svg'); */
  /* 这就是省略了public的写法 */
  /* background-image: url('/vite.svg'); */
  /* 上面的两种写法都可以在开发环境(npm run dev)和打包(npm run build)之后的部署环境访问到图片,展示图片 */
  /* 但是下面这种写法,npm run dev可以看到图片,但是npm run build打包之后无法访问图片,展示图片 */
  /* background-image: url('/vite.svg'); */
  /* 这里就暂时用背景颜色代替背景图片吧 */
  background-color: skyblue;
}

.login-vue .logn-vue-container {
  width: 480px;
  height: 400px;
  border-radius: 16px;
  background-color: #fff;
  /* flex-wrap: wrap; */
  flex-direction: column;
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 24px;
}
</style>

ShouYe.vue代码

这个是两个用户公共具有的菜单权限,具体到时候看一下运行的截图就知道了

<template>
  <div class="shou-ye-vue">
    <h1>这个是首页</h1>
  </div>
</template>
<script setup lang="ts">
</script>
<style scoped>
.shou-ye-vue {
  width: 100%;
  height: 100%;
  background-color: #c971f1;
}
</style>

ZiCaiDan1.vue代码

<template>
  <div style="width: 100%; height: 100%; background-color: #f9b2ff;">
    <h1>这是子子子菜单1111111的界面</h1>
  </div>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>

ZiCaiDan2.vue代码

<template>
  <div style="width: 100%; height: 100%; background-color: #ffb2b6;">
    <h1>这是子子子菜单22222222的界面</h1>
  </div>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>

ZiCaiDan2.vue代码

<template>
  <div style="width: 100%; height: 100%; background-color: #b2faff;">
    <h1>这是子子子菜单3333333的界面</h1>
  </div>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>

路由目录下的路由配置代码,index.ts

还有就是,路由重定向配置的路径需要格外注意,这个稍微少了一个/都会有可能造成问题,第二次刷线的时候无法展示路由,具体看这里。vue-router个人的记录_m0_62317155的博客-CSDN博客

import { createRouter, createWebHashHistory } from "vue-router";

/**
 * 这里注意了,保证路由的名字唯一,路由路径唯一
 */
const clientRoutes = [
  {
    path: "/",
    name: "根组件路",
    redirect: "/login",
  },
  {
    path: "/login",
    name: "登录界面",
    component: () => import("../views/Login.vue"),
  },
  {
    path: "/home",
    name: "home",
    component: () => import("../views/Home.vue"),
    redirect: "/home/shouye",
    // 公共组件放这里,也可以不放,也就是不写这个Children,使用addRoute动态路由添加的形式,这个addRoute就是在Children中添加路由的
    children: [
      {
        path: "shouye",
        name: "首页",
        component: () => import("../views/ShouYe.vue"),
      },
    ],
  },
];

const router = createRouter({
  history: createWebHashHistory(),
  routes: clientRoutes,
});

interface Route {
  path?: string;
  menuName?: string;
  component?: string;
}

// 动态路由官网:https://router.vuejs.org/zh/guide/advanced/dynamic-routing.html

/**
 * 防止刷新时,路由消失,因为刷新肯定会加载这个文件的,所以可以写在这里
 */
const serverRoutesObj = localStorage.getItem("serverRoutes");
if (serverRoutesObj) {
  const serverRoutesObjs = JSON.parse(serverRoutesObj);
  // 这个方法,在本文件的下面
  addServerRoutes(
    serverRoutesObjs.parentRouterName,
    serverRoutesObjs.routeList
  );
}

/**
 * 重置路由,让其处于未登录,未添加服务器返回的动态路由时的路由状态
 */
export function resetRoutes() {
  for(const r of clientRoutes) {
    // 这里就是没有传入父路由的名字,默认就是/的路径下动态添加路由
    router.addRoute(r)
  }
  // 如果是重置路由,那就清空对应的登录后服务器返回的路由信息,在下面的addServerRoutes方法中
  localStorage.removeItem("serverRoutes")
}

/**
 * 添加服务器返回的动态路由
 * @param parentRouterName 父路由名称,用户确定动态路由添加所在的子路由位置,如这里为home时,则是上clientRoutes路由列表中
 * 名字为home的路,调用router.addRoute('home', {路由对象}),则为home的子路由,会跟上面那个公共路由合并起来的,也就是
 * 上面,那个home路由的Children,会自动拆分合并的。具体看官网https://router.vuejs.org/zh/guide/advanced/dynamic-routing.html
 * 就知道了,有详细的说明
 * @param routeList 路由列表,注意是路由列表,不是菜单列表,菜单列表可能是树形的,但是路由列表应该是非树形的,就是一个数组或者集合而已,
 * 当然这是在只有两级路由的情况下,如果是子路由中在嵌套子路由,这个就需要根据情况进行判断了
 */
export function addServerRoutes(parentRouterName: string, routeList: Route[]) {
  for (const r of routeList) {
    // 参数1,父路由的名字,如果没写,默认父路由就是根路径,也就是/这个
    // 参数2,路由对象
    router.addRoute(parentRouterName, {
      path: r.path!,
      name: r.menuName,
      component: () => import(/* @vite-ignore */ "../" + r.component),
    });
  }

  // 将路由信息保存到localStorage中缓存起来,方便后面用到。这里除了放到localStorage中,还可以使用pinina进行管理,这个看情况而定
  const routerObjectList = {
    parentRouterName: parentRouterName,
    routeList: routeList
  }
  localStorage.setItem("serverRoutes", JSON.stringify(routerObjectList))

}

export default router;

其实这里路由应该还配置一个404页面的,就是当用户输入不存在的路由时,跳转到404页面,这里就不写了。具体配置可以参靠官网。或者我的另一篇文章,这里就把截图贴出来一下,这里只是说明一下,顺嘴提一下而已。可以不配置的,都不带管这里也可以
在这里插入图片描述

main.ts配置如下所示

import { createApp } from 'vue'
import './style.css'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
// 引用配置的路由信息
import router from './router/index.ts'

import App from './App.vue'

createApp(App).use(ElementPlus).use(router).mount('#app')

运行结果如下所示

运行结果的GIF图如下所示:
在这里插入图片描述
通过上面的GIF图可以知道,不同的用户具有不同的权限是,登录得到的菜单和路由也是不一样的,具体可以看下面的截图对对比一下就知道了。这里有个bug,就是刷新的时候,element-plus的激活状态没有实时显示对应的菜单项。这里有一个解决办法,就是为菜单项的设置一个点击事件,将对应的点击菜单的时候将对应的path赋值给element plus的菜单激活active字段,然后保存到localStorage中或者pinina中,刷新的时候先判断localStorage、pinina有没有对应的激活记录,然后赋值,让其显示对应的菜单激活状态。这里后面再说吧!

当我没有登录的时候,运行起来,可以看到具有的默认路由截图如下所示
在这里插入图片描述

在这里插入图片描述
当我使用amdin登录之后,路由表的路由如下所示,发现了home下新增了很多的路由信息,
在这里插入图片描述
在这里插入图片描述
当我使用zhangsan的账号登录时,路由表中的路由信息如下所示,对比上面明显减少了不少路由
在这里插入图片描述
在这里插入图片描述

打包

运行npm run build打包,环境这里就不管了,这里只是打包一下而已
在这里插入图片描述
在这里插入图片描述
打开之后,发现一片空白
在这里插入图片描述
nginx的安装路经不能有中文!!!,将上面打包生成的dist文件复制到nginx对应的目录下,如下图所示
在这里插入图片描述
记住是整个dist复制,可以看一下它们的对比图
在这里插入图片描述
复制好之后,修改nginx的配置文件,找到nginx对应的安装目录下的conf/nginx.conf。打开,我这里使用的是Vscode打开的,进行了一些配置。
在这里插入图片描述
进行如下配置,记得配置好了要保存一下啊
在这里插入图片描述
配置好了之后,到nginx的安装目录下,双击nginx.exe
在这里插入图片描述
在这里插入图片描述
注意了,这个nginx会有一个黑色窗口闪一下而已,并不会有实际的图标出现。nginx的安装路经不能有中文!!!
然后访问http://localhost:8848/index.html或者http://localhost:8848都可以
在这里插入图片描述
在这里插入图片描述
如上图所示:路由点击左侧菜单并没有跳转到对应的路由,而且f12之后发现,报如下错误

Failed to load resource: the server responded with a status of 404 (Not Found)
TypeError: Failed to fetch dynamically imported module: http://localhost:8848/views/front/CaiDan1.vue
Uncaught (in promise) TypeError: Failed to fetch dynamically imported module: http://localhost:8848/views/front/CaiDan1.vue
Failed to load resource: the server responded with a status of 404 (Not Found)
TypeError: Failed to fetch dynamically imported module: http://localhost:8848/views/front/CaiDan2.vue
Uncaught (in promise) TypeError: Failed to fetch dynamically imported module: http://localhost:8848/views/front/CaiDan2.vue

在这里插入图片描述
这里大概意思是import对应的.vue文件没有导入成功,但是,硬编码在路由中的是可以访问的,如首页那个菜单的路由。然后是addRoute的没有导入成功对应的.vue,然后我就想到了是不是对应的懒加载有问题了,具体看后面的说明!

打包之后动态路由失效问题,以及解决办法

动态路由 | Vue Router (vuejs.org)
从上面的GIF图我们不难看出,在开发的时候通过,npm run dev运行的时候通过动态路由addRoute添加的路由,可以正常访问打开。但是npm run build打包之后,通过动态路由addRoute添加的路由无法正常访问。
在这里插入图片描述

export function addServerRoutes(parentRouterName: string, routeList: Route[]) {
  for (const r of routeList) {
    // 参数1,父路由的名字,如果没写,默认父路由就是根路径,也就是/这个
    // 参数2,路由对象
    router.addRoute(parentRouterName, {
      path: r.path!,
      name: r.menuName,
      component: () => import(/* @vite-ignore */ "../" + r.component),
    });
  }
  ......
}

这时,我们需要使用Vite 支持使用特殊的 import.meta.glob 函数从文件系统导入多个模块,具体看官网,实现自动导入对应的vue组件。功能 | Vite 官方中文文档 (vitejs.dev) ,我们可以进行如下配置,这里只是简单说明一下,完整的看后面的代码,这里是为了让更好理解,大概写了一下。

// 之里的意思是,导入views任意目录,任意以.vue结尾的文件
const modules = import.meta.glob("../views/**/**.vue")
export function addServerRoutes(parentRouterName: string, routeList: Route[]) {
  for (const r of routeList) {
    // 参数1,父路由的名字,如果没写,默认父路由就是根路径,也就是/这个
    // 参数2,路由对象
    router.addRoute(parentRouterName, {
      path: r.path!,
      name: r.menuName,
      // component: () => import(/* @vite-ignore */ "../" + r.component),
      // component: modules['../views/xxx.vue']
      component: modules[`../${r.component}`]
    });
  }
  ......
}

我们可以debugger跟踪一下,这个

const modules = import.meta.glob("../views/**/**.vue")

在这里插入图片描述

返回的是,什么数据
在这里插入图片描述
然后我们试着读取一下里面的数据
在这里插入图片描述

修改router目录下的路由配置文件index.ts

所以router目录下的index.ts,修改为如下所示的配置,代码后面有截图说明以及解释

import { createRouter, createWebHashHistory } from "vue-router";

const modules = import.meta.glob("../views/**/**.vue")

/**
 * 这里注意了,保证路由的名字唯一,路由路径唯一
 */
const clientRoutes = [
  {
    path: "/",
    name: "根组件路",
    redirect: "/login",
  },
  {
    path: "/login",
    name: "登录界面",
    component: () => import("../views/Login.vue"),
  },
  {
    path: "/home",
    name: "home",
    component: () => import("../views/Home.vue"),
    redirect: "/home/shouye",
    // 公共组件放这里,也可以不放,也就是不写这个Children,使用addRoute动态路由添加的形式,这个addRoute就是在Children中添加路由的
    children: [
      {
        path: "shouye",
        name: "首页",
        component: () => import("../views/ShouYe.vue"),
      },
    ],
  }
];

const router = createRouter({
  history: createWebHashHistory(),
  routes: clientRoutes,
});

interface Route {
  path?: string;
  menuName?: string;
  component?: string;
}

// 动态路由官网:https://router.vuejs.org/zh/guide/advanced/dynamic-routing.html

/**
 * 防止刷新时,路由消失,因为刷新肯定会加载这个文件的,所以可以写在这里
 */
const serverRoutesObj = localStorage.getItem("serverRoutes");
if (serverRoutesObj) {
  const serverRoutesObjs = JSON.parse(serverRoutesObj);
  // 这个方法,在本文件的下面
  addServerRoutes(
    serverRoutesObjs.parentRouterName,
    serverRoutesObjs.routeList
  );
}

/**
 * 重置路由,让其处于未登录,未添加服务器返回的动态路由时的路由状态
 */
export function resetRoutes() {
  for(const r of clientRoutes) {
    // 这里就是没有传入父路由的名字,默认就是/的路径下动态添加路由
    router.addRoute(r)
  }
  // 如果是重置路由,那就清空对应的登录后服务器返回的路由信息,在下面的addServerRoutes方法中
  localStorage.removeItem("serverRoutes")
}

/**
 * 添加服务器返回的动态路由
 * @param parentRouterName 父路由名称,用户确定动态路由添加所在的子路由位置,如这里为home时,则是上clientRoutes路由列表中
 * 名字为home的路,调用router.addRoute('home', {路由对象}),则为home的子路由,会跟上面那个公共路由合并起来的,也就是
 * 上面,那个home路由的Children,会自动拆分合并的。具体看官网https://router.vuejs.org/zh/guide/advanced/dynamic-routing.html
 * 就知道了,有详细的说明,这里可以根据需求调用这个函数,传入不同的父路由名字,然后子啊不同的父路由中添加子路由,实现多个路由有子路由
 * @param routeList 路由列表,注意是路由列表,不是菜单列表,菜单列表可能是树形的,但是路由列表应该是非树形的,就是一个数组或者集合而已,
 * 当然这是在只有两级路由的情况下,如果是子路由中在嵌套子路由,这个就需要根据情况进行判断了
 */
export function addServerRoutes(parentRouterName: string, routeList: Route[]) {
  for (const r of routeList) {
    // 参数1,父路由的名字,如果没写,默认父路由就是根路径,也就是/这个
    // 参数2,路由对象
    console.log(r.component)
    router.addRoute(parentRouterName, {
      path: r.path!,
      name: r.menuName,
      // component: () => import(/* @vite-ignore */ "../" + r.component),
      // component: modules['../views/xxx.vue']
      component: modules[`../${r.component}`]
    });
  }

  // 将路由信息保存到localStorage中缓存起来,方便后面用到。这里除了放到localStorage中,还可以使用pinina进行管理,这个看情况而定
  const routerObjectList = {
    parentRouterName: parentRouterName,
    routeList: routeList
  }
  localStorage.setItem("serverRoutes", JSON.stringify(routerObjectList))

}

export default router;

截图说明一下,完整的代码在,截图后面
在这里插入图片描述
修改保存之后,从新npm run dev运行的结果如下所示
在这里插入图片描述
从GIF图中,我们可以看到,能够正常运行,然后我们npm run build打包,并将生成的dist文件部署到之间配置的nginx中,因为之前已经配置好了,我们替换一下之前的nginx的dist文件就行了。
这里需要注意的是,先把之前运行的nginx停了,这里我简单粗暴一点,直接在任务管理器中把对应的nginx停了,右键结束。只要涉及的都结束它。
在这里插入图片描述
将新生成的dist放到对应的nginx目录下,然后启动nginx,访问http://localhost:8848/index.html或者http://localhost:8848
在这里插入图片描述
结果如下所示:通过GIF图我们可以知道,打包之后的路由也可以正常访问了,
打包之后的路由也可以正常访问了,打包之后的路由也可以正常访问了,打包之后的路由也可以正常访问了
在这里插入图片描述

##以下是无关紧要的问题,请忽略这一部分的内容
路由配置时,需要注意的一些地方
在这里插入图片描述
在这里插入图片描述
渲染菜单时
在这里插入图片描述
将?改成!就好了
在这里插入图片描述
或者这样改
在这里插入图片描述
添加冬天路由的时候,出现如下报错
不能将类型“string | undefined”分配给类型“string”。
不能将类型“undefined”分配给类型“string”。
在这里插入图片描述
加个!就好了
动态路由时会出现的问题,就是有一些警告,如下所示

The above dynamic import cannot be analyzed by Vite.
See https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars#limitations for supported dynamic import formats. If this is intended to be left as-is, you can use the /* @vite-ignore */ comment inside the import() call to suppress this warning.

在这里插入图片描述
在这里插入图片描述

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐