Vue3动态路由(Vite+Vue3+TS+Mock)

一、动态路由简介

Vue通过路由进行页面管理,不同的路由绑定到不同的页面。一般来说,前端直接写好的路由为静态路由,在不修改代码的情况下,路由表是不会改变的。对于不需要动态改变路由表的网站,静态路由就已经足够了,但是当页面需要与权限进行绑定时,不同用户允许浏览的页面是不一样的,在这种情况下,静态路由就难以满足需求了。动态路由就是由后端根据场景生成的合适路由数据,前端获取此数据并进行解析,最后与固定不变的静态路由组合而成的路由。

本文将基于Vite+Vue3+TypeScript构建的Vue项目与mock模拟的后端接口,简单介绍Vue的动态路由配置。

此项目代码已上传至GitHub,链接如下:

https://github.com/XMNHCAS/VueDynamicRoute


二、创建Vite+Vue3+TS基础项目

首先使用以下命令创建项目

npm create vite 项目名 -- --template vue-ts

在刚刚创建的项目的目录下执行以下命令,安装vue初始的node依赖包 

npm i

参考示例如下:

项目创建完成后,在项目目录下执行以下命令,使用VSCode打开此项目。也可以直接在此目录下右键点击使用VSCode打开。

Code .

注意:使用VSCode编辑Vue+TS的项目,建议禁用Vetur,使用Volar插件。


三、项目初始化及静态路由配置 

3.1、安装需要的Node Module 

接下来我们需要安装几个node模块:vue-router、axios和mock。其中vue-router是Vue的路由配置模块,axios是请求包,mock则是前端模拟api接口的包。

可依次执行以下命令,或修改package.json文件之后直接npm i。

安装vue-router和axios:

npm i vue-router axios -S

安装mockjs和vite-plugin-mock。由于本项目的mock主要用途仅为模拟后端数据接口,所以安装为开发依赖,若打包为生产环境则会失效。

npm i mockjs vite-plugin-mock@2.9.8 -D

ps. 目前vite-plugin-mock最新版本修改了配置选项,而且直接运行可能会报错,暂时建议使用2.9.8或2.9.8以下的版本,等待作者修复后再使用新版本。若需要使用最新版本,请移步至该项目的Github仓库(https://github.com/vbenjs/vite-plugin-mock),Issues中有解决报错的办法。

3.2、创建需要的文件夹以及文件

首先我们打开src文件夹下的components文件,删除HelloWorld.vue。打开App.vue,将文件内容修改为以下代码:

<template>
  <router-view></router-view>
</template>

然后在src文件夹下分别创建router、utils、apis、views和mock文件夹,并在文件夹中创建如图所示的文件:

 

3.3、配置vue-router

/src/router/index.ts:此文件为路由配置文件,我们先在此创建初始的静态路由。

import { RouteRecordRaw, createRouter, createWebHistory } from 'vue-router'

// 静态路由表
const routes: Array<RouteRecordRaw> = [
    {
        // 路由重定向配置
        path: '/',
        redirect: '/Home'
    }, {
        path: '/Home',
        component: () => import('../views/HomePage.vue')
    }
]

// 路由对象
const router = createRouter({
    history: createWebHistory(),
    routes
})

export default router

3.4、配置mock

/src/mock/index.ts:此文件为mock模拟接口配置,我们先创建一个测试接口,返回一个Hello World

import { MockMethod } from "vite-plugin-mock"

const mock: Array<MockMethod> = [
    {
        // 接口路径
        url: '/api/test',
        
        // 接口方法
        method: 'get',

        // 返回数据
        response: () => {
            return {
                status: 200,
                message: 'success',
                data: 'Hello World'
            }
        }
    }
]

export default mock

3.5、配置vite.config.ts

要使用mock,我们还需要在vite.config.ts文件下对mock进行配置,让vite启动的同时启动mock服务。

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

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    // mock服务
    viteMockServe({
      supportTs: false,
      logger: false,
      mockPath: "./src/mock/",
    }),
  ]
})

3.6、配置axios

/src/utils/request.ts:此文件为axios配置文件,它将创建一个axios全局单例,由于本项目仅做最简单的演示,所以仅配置baseUrl,实际使用时可根据实际情况添加拦截器等功能。

import axios from 'axios'

// axios对象
const service = axios.create({
    // axios请求基础URL
    // 由于本项目使用mock + vite-plugin-mock启动的mock服务,默认使用的端口号与页面一致
    baseURL: "http://localhost:5173",
    timeout: 5000
})

export default service

/src/apis/index.ts:此文件为接口文件,接口统一放到此文件中。

import req from '../utils/request'

/**
 * 测试接口
 */

// 测试用Hello World
export const TestApi = () => req({ url: '/api/test', method: 'get' })

3.7、首页代码

/src/views/HomePage.vue

<template>
    <h1>Home</h1>
</template>

<script lang="ts" setup>
import { TestApi } from '../apis'

TestApi().then(res => console.log(res)).catch(err => console.log(err))
</script>

3.8、配置main.ts

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router'

const app = createApp(App)

// 启用路由
app.use(router)

app.mount('#app')

3.9、静态路由运行 

使用此命令运行,vite3默认的运行端口为5173。 

npm run dev

运行结果如下所示:

从右侧开发者工具中可以看出,我们模拟的test接口已经成功返回了数据,页面也正常根据路由跳转至Home页面。


四、配置动态路由

4.1、配置动态路由接口

首先我们先在刚刚创建的mock接口文件(/src/mock/index.ts)中添加一个返回路由信息的路由接口,如下所示:

import { MockMethod } from "vite-plugin-mock"

const mock: Array<MockMethod> = [
    /**
     * 测试接口
     */
    {
        // 接口路径
        url: '/api/test',

        // 接口方法
        method: 'get',

        // 返回数据
        response: () => {
            return {
                status: 200,
                message: 'success',
                data: 'Hello World'
            }
        }
    }, 
    /**
     * 路由数据接口
     */
    {
        url: '/api/routes',
        method: 'get',
        response: () => {
            // 路由
            const routes = [
                {
                    path: '/PageOne',
                    name: 'PageOne',
                    component: 'PageOne.vue'
                }, {
                    path: '/PageTwo',
                    name: 'PageTwo',
                    component: 'PageTwo.vue'
                }, {
                    path: '/PageThree',
                    name: 'PageThree',
                    component: 'PageThree.vue',
                }
            ]

            return {
                status: 200,
                message: 'success',
                data: routes
            }
        }
    }
]

export default mock

此接口返回三个页面的路由,根据这个接口的数据,我们在views文件夹中创建这三个页面。 

 

页面仅由一个h1标签组成,h1的内容为文件名。此外再加上一个按钮,用以跳转回Home页面。PageOne.vue代码如下:

<template>
    <h1>Page One</h1>
    <button @click="handleClick">Home</button>
</template>

<script lang="ts" setup>
import { useRouter } from 'vue-router';

const router = useRouter()
const handleClick = () => router.push({ path: '/Home' })
</script>

其余两个页面仅需修改h1标签的内容。

4.2、安装并配置pinia

我们的动态路由数据应由一个公共的地方进行管理,本文选用vue的状态管理器来实现这个功能。pinia是vue新一代的状态管理器,与vuex作用基本相同,但是功能比vuex更加强大。

pinia官方文档:https://pinia.web3doc.top

首先在项目目录下执行以下命令,安装pinia:

npm i pinia -S

安装完成后,创建store文件夹,并在此文件夹下创建index.ts文件。

 /src/store/index.ts

import { defineStore } from 'pinia'

// pinia状态管理器
export const useStore = defineStore('myStore', {
    state: () => {
        return {}
    },
    getters: {},
    actions: {}
})

 完成基础配置后,需要在mian.ts中引入pinia,需要注意的是,pinia必须在vue-router之后引入。

main.ts

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router'

import { createPinia } from 'pinia'
const pinia = createPinia()

const app = createApp(App)

// 启用路由
app.use(router)

// 启用pinia
app.use(pinia)

app.mount('#app')

4.3、添加路由数据接口

/src/apis/index.ts

import req from '../utils/request'

/**
 * 测试接口
 */

// 测试用Hello World
export const TestApi = () => req({ url: '/api/test', method: 'get' })

/**
 * 动态路由接口
 */
export const GetDynamicRoutes = () => req({ url: '/api/routes', method: 'get' })

4.4、在pinia中添加配置项

由于我们使用的pinia存储我们的路由数据,所以我们需要在pinia的state中添加一个路由项(RouteRecordRaw类型的数组routes)。

我们在pinia的action中还需要添加一个根据路由数据加载动态路由的方法(addRoutes),路由数据和router对象由外部传入。外部传入router是为了避免循环调用router,毕竟需要进行加载动态路由的地方基本都有个router的示例对象,不过也可以直接在此方法里面直接调用router,并不影响结果,也不会报错。

加载路由的思路很简单,首先解析外部传入的路由数据,根据路由的数据类型生成对应的路由表,并存储到pinia中,然后直接遍历这个pinia中的路由表,使用router.addRoute()方法将路由加载进去。router.addRoute()方法还支持传如两个参数,这是为了在指定位置的路由中插入children,这种情况下第一个参数是父级路由的name,第二个参数就是要添加的children路由对象。本文的项目并没有做多层级的路由,所以使用一个参数即可。

vite使用动态路由,在动态导入组件的时候,需要注意不能将页面路径直接作为component导入,虽然开发环境一般是能正常加载,但是打包到生产环境的时候就会出错,所以我们需要添加以下代码:

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

然后使用这个modules来配置组件:

// 错误示例:components:()=>import(`../views/Pages/${m.component}`)
// 正确示例如下:
component: modules[`../views/Pages/${m.component}`],

最终的pinia代码如下: 

 /src/store/index.ts

import { defineStore } from 'pinia'
import { RouteRecordRaw } from 'vue-router'

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

// pinia状态管理器
export const useStore = defineStore('myStore', {
    state: () => {
        return {
            // 路由表
            routes: [] as Array<RouteRecordRaw>
        }
    },
    getters: {},
    actions: {
        // 添加动态路由,并同步到状态管理器中
        addRoutes(data: Array<any>, router: any) {
            data.forEach(m => {
                this.routes.push({
                    path: m.path,
                    name: m.name,
                    // 错误示例:components:()=>import(`../views/Pages/${m.component}`)
                    // 正确示例如下:
                    component: modules[`../views/Pages/${m.component}`],
                })
            })

            this.routes.forEach(m => router.addRoute(m))
        },
    }
})

4.5、加载动态路由

配置了路由接口和加载路由的方法,接下来我们就需要加载我们的动态路由了。

思路也很简单,在我们的Home页面中调用路由的数据接口,在获取到数据之后调用加载的方法即可。为了验证我们的路由是否被加载成功,我们还需要添加三个路由对应的按钮,以便我们进行路由跳转。

/src/views/HomePage.vue

<template>
    <h1>Home</h1>
    <div style="display: flex;gap:20px">
        <button v-for="item in routes" @click="handleClick(item.path)"> {{ item.name }}</button>
    </div>
</template>

<script lang="ts" setup>
import { useStore } from "../store";
import { TestApi, GetDynamicRoutes } from '../apis'
import { useRouter } from 'vue-router'
import { computed } from "@vue/reactivity";
import { onMounted } from "vue";

const router = useRouter()
const store = useStore()

// 动态路由表
const routes = computed(() => store.routes)

// 路由按钮点击事件
const handleClick = (path: string) => {
    router.push({ path })
}

onMounted(() => {
    if (store.routes.length < 1) {
        // 获取动态路由
        GetDynamicRoutes().then(res => {
            store.addRoutes(res.data.data, router)
        })      
    }

     // 测试接口
     TestApi().then(res => console.log(res.data)).catch(err => console.log(err))
})
</script>

效果如下: 

随便点击一个按钮,我们就能成功跳转到我们的动态路由了。

4.6、配置路由守卫

到这里,我们的动态路由已经加载成功了。不过其实我们还没有做完,这个动态路由还存在一个bug,假如我们刷新跳转后的页面,或者直接使用动态路由的路径进行跳转,就会出现以下情况:

可以看到,这种情况下我们的动态路由失效了,页面没了。这是因为我们的路由和pinia在刷新之后都会被重置,而我们加载路由的方法是在Home页面被调用的,当我们直接跳转到动态加载出来的路径或者直接在这个路径刷新的时候,Home页面并没有被加载,也就是说我们的动态路由并没有被加载上去,自然这个动态的页面也就丢失了。

解决方法有很多,举个例子,既然我们刷新丢了路由是因为Home页面没被加载,那我们把加载的方法直接写到必然会被调用的App.vue就可以了,这是个简单直接的方法。不过考虑到实际使用的时候,我们可能需要在路由跳转时进行鉴权操作,下面介绍另一种方法,使用路由守卫进行加载。

其实路由守卫加载的思路也很简单,假如我们的页面请求路径不是某个指定的路径的时候,我们就在跳转之前先去查询状态管理器中是否存在我们的动态路由,或者该动态路由是否满足我们的跳转要求,如果不满足就请求接口并加载我们的动态路由,并在加载完成后再继续跳转操作。在本项目中,我们的固定页面是Home页面,所以只要我们跳转的不是Home页面,就查询pinia是否存在路由表,如果没有则请求接口获取路由并加载。代码示例如下:

/src/router/index.ts

import { RouteRecordRaw, createRouter, createWebHistory } from 'vue-router'
import { useStore } from "../store";
import { GetDynamicRoutes } from '../apis'

// 静态路由表
const routes: Array<RouteRecordRaw> = [
    {
        // 路由重定向配置
        path: '/',
        redirect: '/Home'
    }, {
        path: '/Home',
        component: () => import('../views/HomePage.vue')
    }
]

// 路由对象
const router = createRouter({
    history: createWebHistory(),
    routes
})

// 路由守卫
router.beforeEach((to, from, next) => {
    if (to.path !== '/Home' && to.path !== '/') {
        const store = useStore()
        if (store.routes.length < 1) {

            GetDynamicRoutes().then(res => {
                store.addRoutes(res.data.data, router)
                next({ path: to.path, replace: true })

            }).catch(_ => {
                next()
            })

        } else {
            next()
        }
    } else {
        next()
    }
})

export default router

现在我们直接访问我们的动态页面也能成功加载了。

虽然能成功加载了,但是开发者工具那里还有一条报错:

[Vue Router warn]: No match found for location with path "/PageOne"

这是因为我们在路由跳转之前请求了一次接口进行加载,但是在完成路由加载之前,vue-router就直接先去找了这个路径,我们的路由都还没加载完成,当然也就找不到了,所以就会出这个警告。其实这个警告完全可以无视,不过我们最好还是处理一下,避免出现意外情况。

解决方法也很简单,添加一个404页面就可以了。在views文件夹中添加一个Error文件夹,并在此文件夹中添加404.vue文件。

<template>
    <h1>404</h1>
</template>

然后在静态路由中添加一个错误的路由:

 /src/router/index.ts

import { RouteRecordRaw, createRouter, createWebHistory } from 'vue-router'
import { useStore } from "../store";
import { GetDynamicRoutes } from '../apis'

// 静态路由表
const routes: Array<RouteRecordRaw> = [
    {
        // 路由重定向配置
        path: '/',
        redirect: '/Home'
    }, {
        path: '/Home',
        component: () => import('../views/HomePage.vue')
    }, {
        // 404页面配置
        path: '/:catchAll(.*)',
        component: () => import('../views/Errors/404.vue')
    }
]

// 路由对象
const router = createRouter({
    history: createWebHistory(),
    routes
})

// 路由守卫
router.beforeEach((to, from, next) => {
    if (to.path !== '/Home' && to.path !== '/') {
        const store = useStore()
        if (store.routes.length < 1) {

            GetDynamicRoutes().then(res => {
                store.addRoutes(res.data.data, router)
                next({ path: to.path, replace: true })

            }).catch(_ => {
                next()
            })

        } else {
            next()
        }
    } else {
        next()
    }
})

export default router

 效果如下:


五、结尾

至此我们的动态路由就完成了,动态路由的实现方法有很多,本文介绍的也只是其中一种,也有很大的优化空间,这个就看需要参照实际需求了。