Vue3 集成 VueRouter
Vue Router 是 Vue.js 的官方路由管理器,用于构建单页面应用程序(SPA)。它与 Vue.js 核心深度集成,使得构建单页应用变得简单高效。
使用场景
- 构建单页面应用程序(SPA)
- 需要前端路由管理的项目
- 需要实现页面导航、路由守卫和懒加载等功能
- 需要基于路由的参数传递和状态管理
注意事项
- Vue Router 4.x 专为 Vue 3 设计,与 Vue 2 需要使用 Vue Router 3.x
- 路由配置应合理组织,避免过于复杂嵌套
- 注意路由守卫的执行顺序和时机
- 动态路由参数变化时组件不会重新创建,需要使用监听器处理
- 路由模式分为 hash 模式和 history 模式,后者需要服务器配置支持
基本用法
安装与配置
npm install vue-router@4
yarn add vue-router@4
pnpm add vue-router@42
3
新建 router/routes.ts
创建 src/router/routes.ts 文件
在 src 下创建 router 目录,然后在 router 目录里新建 routes.ts 文件:
└── src/
├── router/
├── routes.ts // 路由文件
├── index.ts // 路由配置文件2
3
4
方式1
// router/routes.ts
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';
// 定义路由配置
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
component: () => import('@/views/HomeView.vue')
},
{
path: '/about',
name: 'About',
component: () => import('@/views/AboutView.vue')
},
{
path: '/user/:id',
name: 'User',
component: () => import('@/views/UserView.vue'),
props: true // 将路由参数作为 props 传递给组件
}
];
// 创建路由实例
const router = createRouter({
history: createWebHistory(), // 使用 HTML5 History 模式
routes
});
export default router;2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
方式2
declare module 'vue-router' {
interface _RouteRecordBase {
hidden?: boolean | string | number;
}
}
const routes: RouteRecordRaw[] = [
{
path: '/index',
name: 'index',
component: () => {
return import('../components/Homepage.vue');
},
meta: {
keepAlive: true
},
children: [
// 添加子路由
{
path: 'article',
name: 'article',
component: () => {
return import('../views/page/article/index.vue');
}
},
{
path: 'TestTool',
name: 'TestTool',
component: () => {
return import('../views/page/testTool/TestTool.vue');
},
children: [
// 添加子路由
{
path: 'TestToolConvert',
name: 'TestToolConvert',
component: () => {
return import('@/views/page/testTool/TestToolConvert.vue');
}
}
]
}
]
},
{ path: '/', redirect: { name: 'home' } }
];
export default routes;2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
新建router/index.ts
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { createRouter, createWebHistory, _RouteRecordBase } from 'vue-router'
import routes from './routes'
import NProgress from 'nprogress'
const router = createRouter({
history: createWebHistory(), //历史模式会制造页面刷新
routes
})
// 页面切换之前取消上一个路由中未完成的请求
router.beforeEach((_to: any, _from: any, next: () => void) => {
NProgress.start()
next()
})
router.afterEach(() => {
// 进度条
NProgress.done()
})
export default router2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
挂载路由配置
main.ts 文件中挂载路由配置
import { createApp } from 'vue'
import App from './App.vue'
import router from './router/index'
// use
const app = createApp(App)
app.use(router)2
3
4
5
6
7
在组件中使用路由
<template>
<div>
<nav>
<router-link to="/">首页</router-link>
<router-link to="/about">关于</router-link>
<router-link :to="{ name: 'User', params: { id: 123 }}">用户页面</router-link>
</nav>
<!-- 路由出口 -->
<router-view />
</div>
</template>
<script lang="ts" setup>
import { useRouter, useRoute } from 'vue-router'
// 获取路由实例和当前路由
const router = useRouter()
const route = useRoute()
// 编程式导航
const goToAbout = () => {
router.push('/about')
// 或者使用命名路由
// router.push({ name: 'About' })
}
const goToUser = (id: number) => {
router.push({ name: 'User', params: { id } })
}
// 替换当前路由(不添加历史记录)
const replaceRoute = () => {
router.replace('/about')
}
// 前进后退
const goBack = () => {
router.go(-1) // 后退一步
}
const goForward = () => {
router.go(1) // 前进一步
}
</script>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
路由出口
<template>
<div id="app">
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" v-if="$route.meta.keepAlive" />
</keep-alive>
<component :is="Component" v-if="!$route.meta.keepAlive" />
</router-view>
</div>
</template>2
3
4
5
6
7
8
9
10
常用操作
路由参数和查询参数
<template>
<div>
<h1>用户详情</h1>
<p>用户ID: {{ userId }}</p>
<p>用户名: {{ username }}</p>
</div>
</template>
<script lang="ts" setup>
import { useRoute } from 'vue-router'
import { computed, watch } from 'vue'
const route = useRoute()
// 获取路由参数
const userId = computed(() => route.params.id as string)
// 获取查询参数
const username = computed(() => route.query.name as string || '未知用户')
// 监听路由参数变化
watch(
() => route.params.id,
(newId) => {
console.log('用户ID变化:', newId)
// 可以在这里重新获取用户数据
}
)
</script>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
嵌套路由
// router/index.ts
const routes: Array<RouteRecordRaw> = [
{
path: '/user/:id',
component: () => import('@/views/UserLayout.vue'),
children: [
{
path: '',
name: 'UserProfile',
component: () => import('@/views/UserProfile.vue')
},
{
path: 'posts',
name: 'UserPosts',
component: () => import('@/views/UserPosts.vue')
},
{
path: 'settings',
name: 'UserSettings',
component: () => import('@/views/UserSettings.vue')
}
]
}
]2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- UserLayout.vue -->
<template>
<div>
<h1>用户页面</h1>
<nav>
<router-link :to="{ name: 'UserProfile', params: { id: $route.params.id }}">资料</router-link>
<router-link :to="{ name: 'UserPosts', params: { id: $route.params.id }}">帖子</router-link>
<router-link :to="{ name: 'UserSettings', params: { id: $route.params.id }}">设置</router-link>
</nav>
<!-- 嵌套路由出口 -->
<router-view />
</div>
</template>2
3
4
5
6
7
8
9
10
11
12
13
14
路由守卫
// router/index.ts
import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router'
// 全局前置守卫
router.beforeEach((to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
// 可以在这里进行权限检查
const isAuthenticated = checkAuth() // 假设的认证检查函数
if (to.meta.requiresAuth && !isAuthenticated) {
next({ name: 'Login', query: { redirect: to.fullPath } })
} else {
next()
}
})
// 全局后置钩子
router.afterEach((to, from) => {
// 可以在这里进行页面跟踪等操作
document.title = to.meta.title as string || '默认标题'
})
// 路由独享的守卫
const routes: Array<RouteRecordRaw> = [
{
path: '/admin',
component: () => import('@/views/AdminView.vue'),
beforeEnter: (to, from, next) => {
// 检查管理员权限
if (isAdmin()) {
next()
} else {
next({ name: 'Home' })
}
}
}
]2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!-- 组件内守卫 -->
<script lang="ts" setup>
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
// 在组件卸载前调用
onBeforeRouteLeave((to, from, next) => {
// 可以在这里询问用户是否确认离开
const answer = window.confirm('确定要离开吗?未保存的更改将会丢失。')
if (answer) {
next()
} else {
next(false)
}
})
// 在当前路由更新但该组件被复用时调用
onBeforeRouteUpdate((to, from, next) => {
// 可以在这里获取新的数据
fetchUserData(to.params.id as string)
next()
})
</script>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
配置404页面
修改router/routes.ts
* 代表通配符,若放在任意路由前,会被先匹配,导致跳转到 404 页面,所以需将如下配置置于最后。
const routes = [
...//添加(放在最后)
{
path: "/:pathMatch(.*)*",
component: () => import("@/pages/notFound.vue"),
},
{
path: '*',
name: '404'
component: () => import('./404.vue')
}
];2
3
4
5
6
7
8
9
10
11
12
路由重定向
在嵌套路由中,当访问 /home 时想重定向到 /home/user
修改router/routes.ts
{
path: '/home',
component: () => import('@/pages/home.vue'),
redirect: '/home/user', //新增
children: [
{
path: '/home/user',
component: () => import('@/pages/user.vue'),
},
{
path: '/home/manage',
component: () => import('@/pages/manage.vue'),
},
],
},2
3
4
5
6
7
8
9
10
11
12
13
14
15
刷新当前路由
//+new Date()保证每次点击路由的query项都是不一样的,确保会重新刷新view
const routers = async (path: string) => {
await router.push({
path: path,
query: {
t: +new Date()
}
})
}2
3
4
5
6
7
8
9
跳转新窗口
/**
* @description: 跳转新页面
* @param {string} url
* @return {*}
*/
function winUrl(url: string): any {
window.open(url)
}
async function resolveId(path: string, id: number) {
const { href } = resolve(path, id)
await winUrl(href)
}2
3
4
5
6
7
8
9
10
11
12
13
高级用法
路由懒加载
使用 () => import() 方式导入的组件,只会在第一次进入页面时才会加载对应路由的组件
// 方式1
const UserDetails = () => import('./views/UserDetails.vue')
const router = createRouter({
// ...
routes: [{ path: '/users/:id', component: UserDetails }],
})
// 方式2
// 使用动态导入实现懒加载
const routes: Array<RouteRecordRaw> = [
{ path: '/admin',
name: 'Admin',
component: () => import(/* webpackChunkName: "admin" */ '@/views/Admin.vue')
}
]2
3
4
5
6
7
8
9
10
11
12
13
14
15
滚动行为
// router/index.ts
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
// 返回滚动位置
if (savedPosition) {
return savedPosition
} else if (to.hash) {
return {
el: to.hash,
behavior: 'smooth'
}
} else {
return { top: 0, left: 0 }
}
}
})2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
动态路由
// 添加路由
const newRoute: RouteRecordRaw = {
path: '/new-route',
component: () => import('@/views/NewView.vue')
}
router.addRoute(newRoute)
// 添加嵌套路由
router.addRoute('parent-route', {
path: 'child-route',
component: () => import('@/views/ChildView.vue')
})
// 删除路由
router.removeRoute('route-name')2
3
4
5
6
7
8
9
10
11
12
13
14
15
路由模式
Hash 模式
import { createRouter, createWebHashHistory } from 'vue-router'
const router = createRouter({
history: createWebHashHistory(), // 使用 URL hash
routes
})2
3
4
5
6
History 模式
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(), // 使用 HTML5 History API
routes
})2
3
4
5
6
路由导航
useRoute/useRouter
<script setup>
import { useRoute, useRouter } from 'vue-router'
const route = useRoute() // 路由信息
console.log(route.query)
const router = useRouter()// 路由跳转
router.push('/newPage')
</script>2
3
4
5
6
7
路由导航流程
- 导航被触发
- 在失活的组件里调用 beforeRouteLeave 守卫
- 调用全局 beforeEach 前置守卫
- 重用的组件调用 beforeRouteUpdate 守卫(2.2+)
- 路由配置调用 beforeEnter
- 解析异步路由组件
- 在被激活的组件里调用 beforeRouteEnter 守卫
- 调用全局的 beforeResolve 守卫(2.5+)
- 导航被确认
- 调用全局的 afterEach
- 触发 DOM 更新
- 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入
编程式导航
组合式API
<script setup lang="ts">
import { useRouter } from 'vue-router';
const router = useRouter();
const handleManage = () => {
router.push('/home/manage');
};
</script>2
3
4
5
6
7
8
9
路由传参
query传参
//页面传参
<script setup lang="ts">
import { useRouter } from 'vue-router';
const router = useRouter();
const handleManage = () => {
router.push({
path: '/home/manage',
query: {
plan: '123', // t: +new Date()
},
});
};
</script>2
3
4
5
6
7
8
9
10
11
12
13
14
15
//页面接参
<script setup lang="ts">
import { useRoute } from 'vue-router';
const route = useRoute();
console.log(route.query.plan); //query接参
</script>2
3
4
5
6
7
无参跳转
// 字符串
router.push('home')
// 对象
router.push({ path: 'home' })2
3
4
带参跳转
/**
* 传值跳转
* @param path 路径
* @param value 值
*/
const routerId = async (path: string, value: number | string) => {
await router.push({
path: path,
query: {
id: value,
t: +new Date()
}
})
}2
3
4
5
6
7
8
9
10
11
12
13
14
获取跳转过来的参数
import { useRoute } from 'vue-router'
const route = useRoute()
const state = reactive({
id: route.query.id,
})2
3
4
5
动态路由匹配
//定义路由
{
path: '/register/:plan', // 动态字段以冒号开始
component: () => import('@/pages/register.vue'),
},2
3
4
5
//页面传参
<script setup lang="ts">
import { useRouter } from 'vue-router';
const router = useRouter();
const handleManage = () => {
router.push('/register/123');
};
</script>2
3
4
5
6
7
8
9
//页面接参
<script setup lang="ts">
import { useRoute } from 'vue-router';
const route = useRoute();
console.log(route.params.plan); //params接参
</script>2
3
4
5
6
7
存储懒加载组件
// 用对象字面量来存储懒加载组件的路径和对应的组件函数
const asyncComponents = {
home: () => {
return import('@/components/MyHome.vue')
},
article: () => {
return import('@/views/page/article/Index.vue')
},
column: () => {
return import('@/views/page/article/components/column/ArticleColumn.vue')
}
}
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'homes',
meta: {
keepAlive: true
},
component: asyncComponents.home
},2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
导航守卫
vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。这里有很多方式植入路由导航中:全局的,单个路由独享的,或者组件级的。
路由导航守卫
<script setup>
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
// 添加一个导航守卫,在当前组件将要离开时触发。
onBeforeRouteLeave((to, from, next) => {
next()
})
// 添加一个导航守卫,在当前组件更新时触发。
// 在当前路由改变,但是该组件被复用时调用。
onBeforeRouteUpdate((to, from, next) => {
next()
})
</script>2
3
4
5
6
7
8
9
10
11
12
13
14
全局前置守卫
在路由跳转前触发,可在执行 next 方法前做登录判断,未登陆用户跳转到登录页
const router = new createRouter({})
//to: 即将要进入的目标 用一种标准化的方式
//from: 当前导航正要离开的路由 用一种标准化的方式
router.beforeEach((to, from, next) => {
if (to.path === '/login') {
//在登录页做清除操作,如清除token等
}
if (!localStorage.getItem('token') && to.path !== '/login') {
// 未登陆且访问的不是登录页,重定向到登录页面
return '/login';
}
...
// 必须执行 next 方法来触发路由跳转
next()
// 返回 false 以取消导航
return false
})
// 含有异步操作的方法
router.beforeEach(async (to, from, next) => {
const res = await fetch("****");
// to: 跳转到哪个路由
// from: 从哪个路由跳转过来
// next: 跳转函数,可以跳转到具体的 url
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
死循环解决
vue Router warn The “next“ callback was called more than once in one navigation guard
router.beforeEach(async (to, from, next) => {
const token = Cookies.get('token');
if (to.path === '/login' || to.path === '/') {
next();
}
else {
if (token) {
next();
} else {
console.log('pms out');
next('/login');
}
}
})2
3
4
5
6
7
8
9
10
11
12
13
14
全局解析守卫
与 beforeEach 类似,也是路由跳转前触发,区别是还需在所有组件内守卫和异步路由组件被解析之后,也就是在组件内 beforeRouteEnter 之后被调用。
router.beforeResolve((to, from, next) => {
...
// 必须执行 next 方法来触发路由跳转
next()
})2
3
4
5
全局后置钩子
和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身。它们对于分析、更改页面标题、声明页面等辅助功能以及许多其他事情都很有用。
router.afterEach((to, from) => {
// ...
})2
3
路由独享守卫
使用场景:部分页面不需要登录,部分页面需要登录才能访问
可在路由配置上直接定义 beforeEnter
const auth = () => {
if (!localStorage.getItem("token")) {
// 未登陆,重定向到登录页面
return "/login";
}
};
const routes = [
...{
path: "/home",
component: () => import("@/pages/home.vue"),
redirect: "/home/user",
children: [
{
path: "/home/user",
component: () => import("@/pages/user.vue"),
},
{
path: "/home/manage",
component: () => import("@/pages/manage.vue"),
beforeEnter: auth, //路由独享守卫
},
],
},
];2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
组件内的守卫
使用情景:预防用户在还未保存修改前突然离开。该导航可以通过返回 false 来取消
组件内可直接定义如下路由导航守卫
<script setup lang="ts">
import { onBeforeRouteLeave } from 'vue-router';
// 与 beforeRouteLeave 相同,无法访问 `this`
onBeforeRouteLeave((to, from) => {
const answer = window.confirm('确定离开吗');
// 取消导航并停留在同一页面上
if (!answer) return false;
});
</script>2
3
4
5
6
7
8
9
10
路由元信息
将自定义信息附加到路由上,例如页面标题,是否需要权限,是否开启页面缓存等
使用路由元信息+全局前置守卫实现部分页面不需要登录,部分页面需要登录才能访问
修改 router/index.ts
const routes = [
...{
path: "/home",
component: () => import("@/pages/home.vue"),
redirect: "/home/user",
children: [
{
path: "/home/user",
component: () => import("@/pages/user.vue"),
},
{
path: "/home/manage",
component: () => import("@/pages/manage.vue"),
meta: {
title: "管理页", // 页面标题
auth: true, //需要登录权限
},
},
],
},
];2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
修改 router/index.ts
router.beforeEach((to, from) => {
if (!localStorage.getItem("token") && to.meta.auth) {
// 此路由需要授权,请检查是否已登录
// 如果没有,则重定向到登录页面
return {
path: "/login",
// 保存我们所在的位置,以便以后再来
query: { redirect: to.fullPath },
};
}
});2
3
4
5
6
7
8
9
10
11
router-link
router-link 组件默认为a标签,在vue router 3.x中,可通过tag属性更改标签名,event属性更改事件名在vue router 4.x中,这两个属性已被删除,通过作用域插槽(子组件给父组件传值的插槽)实现自定义导航标签
示例:将导航标签改为div,且需双击触发
active-class
<router-link v-slot="{ href, navigate, isExactActive }" to="/home/user" custom>
<div :class="{ active: isExactActive }" :href="href" @dblclick="navigate">跳转user</div>
</router-link>2
3
<!-- 字符串 -->
<router-link to="home">Home</router-link>
<!-- 使用 v-bind 的 JS 表达式 -->
<router-link v-bind:to="'home'">Home</router-link>
<!-- 不写 v-bind 也可以,就像绑定别的属性一样 -->
<router-link :to="'home'">Home</router-link>
<!-- 同上 -->
<router-link :to="{ path: 'home' }">Home</router-link>
<!-- 命名的路由 -->
<router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link>
<!-- 带查询参数,下面的结果为 /register?plan=private -->
<router-link :to="{ path: 'register', query: { plan: 'private' }}">Register</router-link>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
设置 replace 属性,点击时,会调用 router.replace() 而不是 router.push(),导航后不会留下 history 记录。
<router-link :to="{ path: '/abc'}" replace></router-link>设置 append 属性后,则在当前 (相对) 路径前添加其路径。例如,我们从 /a 导航到一个相对路径 b,如果没有配置 append,则路径为 /b,如果配了,则为 /a/b
<router-link :to="{ path: 'relative/path'}" append></router-link>`exact-active-class
配置当链接被精确匹配的时候应该激活的 class。可以通过以下代码来替代。
<p>
<router-link v-bind:to = "{ path: '/route1'}" exact-active-class = "_active">Router Link 1</router-link>
<router-link v-bind:to = "{ path: '/route2'}" tag = "span">Router Link 2</router-link>
</p>2
3
4
router.go(n)
这个方法的参数是一个整数,意思是在 history 记录中向前或者后退多少步,类似 window.history.go(n)。
// 在浏览器记录中前进一步,等同于 history.forward()
router.go(1)
// 后退一步记录,等同于 history.back()
router.go(-1)
// 前进 3 步记录
router.go(3)
// 如果 history 记录不够用,那就默默地失败呗
router.go(-100)
router.go(100)2
3
4
5
6
7
8
9
子路由
<a-menu-item key="1" @click="Routers('/Admin-index/ArticleTable')">文章列表</a-menu-item>
<router-view></router-view>2
路由配置
{
path: '/Admin-index',
name: 'Admin-index',
component: () => import('@/views/admin/index/index.vue'),
children: [ // 添加子路由
{
path: 'ArticleTable',
name: 'ArticleTable',
component: () => import('@/views/admin/article/ArticleTable.vue'),
},
]
},2
3
4
5
6
7
8
9
10
11
12
路由守卫及页面权限控制
import router, { asyncRoutes } from '@/router'
import store from '@/store'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
// import getPageTitle from '@/utils/get-page-title'
// to:要去哪个页面
// from:从哪里来
// next:它是一个函数。
// 如果直接放行 next()
// to:要去哪个页面
// from:从哪里来
// next:它是一个函数。
// 如果直接放行 next()
// 如果要跳到其它页 next(其它页)
const whiteList = ['/login', '/404']
router.beforeEach(async(to, from, next) => {
document.title = 'hr人力项目--' + to.meta.title
// console.log(to, '跳转至', from)
// document.title = getPageTitle(to.meta.title)
NProgress.start() // 开启进度条
const token = store.state.user.token
if (token) {
// 已经登陆
if (to.path === '/login') {
// 如果当前在登录页,那么跳转首页
next('/') // next('/') 只要指定地址跳转,就不会经过router.afterEach(),因此需要手动关闭进度条
NProgress.done() // 关闭进度条
} else {
if (!store.getters.userId) {
// 即将进入登录页调用获取用户信息的函数 [需满足两个条件,1) 需拥有token 2)并未在登录页上 ]
const menuList = await store.dispatch('user/getUserInfo')
console.log(menuList, 'menuListdsadasdsdsa')
console.log(asyncRoutes, 'asyncRoutes')
const filterRoutes = asyncRoutes.filter(route => {
const routeName = route.children[0]
console.log(route.children[0], 'route.children[0].name')
return menuList.includes(routeName)
})
filterRoutes.push({ path: '*', redirect: '/404', hidden: true })
// console.log(filterRoutes, 'filterRoutesfilterRoutes')
router.addRoutes(filterRoutes)
store.commit('menus/setMenuList', filterRoutes)
// next({ ...to, replace: true })
next(to.path)
// 重新加载页面
} else {
next()
}
// 如果没有在登录页,那么放行,
}
} else {
// 没有登录
if (whiteList.includes(to.path)) {
// 如果此时在白名单页面上,那么放行
next()
} else {
// 如果此时不在白名单页面上,那么跳转至登录页
next('/login')
NProgress.done() // 关闭进度条
}
}
})
// 页面跳转之后执行钩子函数afterEach()
router.afterEach(() => {
NProgress.done() // 关闭进度条
})2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
常见问题与解决方案
处理重复导航错误
// 封装一个安全的导航函数
const safePush = (path: string) => {
if (route.path !== path) {
router.push(path)
}
}
// 或者在全局错误处理中捕获
router.onError((error) => {
if (error.message.includes('Avoided redundant navigation')) {
// 忽略重复导航错误
} else {
// 处理其他错误
console.error('路由错误:', error)
}
})2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
处理未知路由
// 添加一个捕获所有路由的规则
const routes: Array<RouteRecordRaw> = [
// ...其他路由
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFound.vue')
}
]2
3
4
5
6
7
8
9
路由对象/属性类型报错
引入 _RouteRecordBase 定义 hidden
import {
createRouter,
createWebHashHistory,
RouteRecordRaw,
_RouteRecordBase
} from 'vue-router'
declare module 'vue-router'{
interface _RouteRecordBase {
hidden?: boolean | string | number
}
}
const routes: Array<RouteRecordRaw> = [
{
path: '/',
redirect: '/login',
},
{
path: '/login',
name:'login',
hidden: false,
component: () => import('@/views/login.vue'), // 懒加载组件
}
]2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
vue3 TypeError: parentComponent.ctx.deactivate is not a function
只要为 component 动态组件添加一个唯一属性key即可
<component :is="Component" :key="route.name" v-if="route.meta.isKeepAlive"></component>router 文件中,使用 pinia 报错
在 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();
// ***
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
🚀 Vue Router 插件系统:让路由扩展变得简单优雅
背景
在 Vue 应用开发中,我们经常需要围绕 Vue Router 开发各种功能,比如页面导航方向、跨页面通信、滚动位置还原等。这些功能本可以作为 Vue Router 的扩展独立开发,但由于 Vue Router 官方并不支持插件机制,我们不得不将它们作为 Vue 插件来实现,这带来了以下问题:
插件的职责模糊不清
以页面缓存插件为例,它本应为 Vue Router 提供功能,却必须作为 Vue 插件开发,这让人感觉关注点有所偏离:
import type { ComputedRef, Plugin } from 'vue'
declare module 'vue-router' {
interface Router {
keepAlive: {
pages: ComputedRef<string[]>
add: (page: string) => void
remove: (page: string) => void
}
}
}
export const KeepAlivePlugin: Plugin = (app) => {
const router = app.config.globalProperties.$router
if (!router) {
throw new Error('[KeepAlivePlugin] 请先安装 Vue Router.')
}
const keepAlivePageSet = shallowReactive(new Set<string>())
const keepAlivePages = computed(() => Array.from(keepAlivePageSet))
router.keepAlive = {
pages: keepAlivePages,
add: (page: string) => keepAlivePageSet.add(page),
remove: (page: string) => keepAlivePageSet.delete(page),
}
// 在路由变化时自动更新缓存列表
router.afterEach((to, from) => {
if (to.meta.keepAlive) {
keepAlivePageSet.add(to.fullPath)
}
})
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
需要手动清理响应式副作用
仍以页面缓存插件为例,我们需要使用 effectScope 创建响应式副作用,并在应用卸载时手动停止:
import { effectScope } from 'vue'
// ...
export const KeepAlivePlugin: Plugin = (app) => {
// ...
const scope = effectScope(true)
const keepAlivePageSet = scope.run(() => shallowReactive(new Set<string>()))!
const keepAlivePages = scope.run(() =>
computed(() => Array.from(keepAlivePageSet)),
)!
// ...
app.onUnmount(() => {
scope.stop()
keepAlivePageSet.clear()
})
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
插件初始化时机问题
Vue Router 的 createRouter() 和 app.use(router) 是分离的,无法在创建 Router 时立即安装扩展插件,这可能导致插件功能在初始化之前就被调用:
// src/router/index.ts
export const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/home',
component: HomeView,
},
],
})
// KeepAlivePlugin 的类型扩展已生效,但插件可能尚未初始化
// 手动调用插件方法
router.keepAlive.add('/home')2
3
4
5
6
7
8
9
10
11
12
13
14
// main.ts
app.use(router).use(KeepAlivePlugin)2
解决方案
经过 Vue Router 4 插件集合与插件系统的设计实践,开发了 vue-router-plugin-system,旨在为 Vue Router 提供标准化的插件系统与统一的安装机制,让路由扩展功能的开发和集成变得简单、高效、可复用。
仍以页面缓存插件为例,使用 vue-router-plugin-system 后的完整代码如下:
// src/router/plugins/keep-alive.ts
import type { ComputedRef, Plugin } from 'vue'
import type { RouterPlugin } from 'vue-router-plugin-system'
import { withInstall } from 'vue-router-plugin-system'
declare module 'vue-router' {
interface Router {
keepAlive: {
pages: ComputedRef<string[]>
add: (page: string) => void
remove: (page: string) => void
}
}
}
// RouterPlugin 在插件安装时会通过 effectScope 自动收集响应式副作用,
// 在应用卸载时自动停止,且 router 实例会被显式地注入到插件上下文中,
// 无需再通过 app.config.globalProperties.$router 获取
export const KeepAlivePluginImpl: RouterPlugin = ({ router, onUninstall }) => {
const keepAlivePageSet = shallowReactive(new Set<string>())
const keepAlivePages = computed(() => Array.from(keepAlivePageSet))
router.keepAlive = {
pages: keepAlivePages,
add: (page: string) => keepAlivePageSet.add(page),
remove: (page: string) => keepAlivePageSet.delete(page),
}
// 在路由变化时自动更新缓存列表
router.afterEach((to, from) => {
if (to.meta.keepAlive) {
keepAlivePageSet.add(to.fullPath)
}
})
onUninstall(() => {
keepAlivePageSet.clear()
})
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// src/router/index.ts
import { createRouter } from 'vue-router-plugin-system'
import { KeepAlivePluginImpl } from './plugins/keep-alive'
// 使用库提供的 createRouter 函数创建 Router 实例,
// 支持直接注册插件,也支持其他集成方式(详见下文)
export const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/home',
component: HomeView,
},
],
// 通过 plugins 选项在创建 Router 时自动安装
plugins: [KeepAlivePluginImpl],
})
// 插件功能已就绪,可以安全调用
router.keepAlive.add('/home')2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
插件开发
一个完整的插件示例:
import type { RouterPlugin } from 'vue-router-plugin-system'
import { inject, watch } from 'vue'
const LoggerPlugin: RouterPlugin = ({
router,
runWithAppContext,
onUninstall,
}) => {
// 添加路由守卫
router.beforeEach((to, from, next) => {
console.log(`路由跳转: ${from.path} → ${to.path}`)
next()
})
// 需要 App 上下文时使用(如 inject、pinia store 等)
runWithAppContext(() => {
const theme = inject('theme', 'light')
watch(router.currentRoute, (route) => {
console.log('当前路由:', route.path, '主题:', theme)
})
})
// 注册清理逻辑
onUninstall(() => {
console.log('插件正在清理')
})
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
集成方式
方案一:插件库集成
插件库开发
// 将此包作为开发依赖,用 withInstall 包装插件并打包到 dist 中
import { withInstall } from 'vue-router-plugin-system'
const MyRouterPlugin = withInstall(
({ router, runWithAppContext, onUninstall }) => {
// 插件实现
},
)
export default MyRouterPlugin2
3
4
5
6
7
8
9
10
// package.json
{
"devDependencies": {
"vue-router-plugin-system": "latest"
}
}2
3
4
5
6
应用侧安装
import MyRouterPlugin from 'some-plugin-package'
// 选项 A:直接安装到路由实例,推荐紧跟在 createRouter 之后调用
MyRouterPlugin.install(router)
// 选项 B:作为 Vue 插件注册,必须在 Vue Router 之后,否则会抛出异常
app.use(router)
app.use(MyRouterPlugin)2
3
4
5
6
7
8
方案二:应用内部插件集成
对于应用内部开发的路由插件,可以在应用侧统一注册和管理。
内部插件开发
// 只需导出 RouterPlugin 实现
import type { RouterPlugin } from 'vue-router-plugin-system'
// src/router/plugins/auth.ts
export const AuthPlugin: RouterPlugin = ({
router,
runWithAppContext,
onUninstall,
}) => {
// 插件实现
router.beforeEach((to, from, next) => {
// 权限检查逻辑
next()
})
}
// src/router/plugins/cache.ts
export const CachePlugin: RouterPlugin = ({
router,
runWithAppContext,
onUninstall,
}) => {
// 缓存管理逻辑
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
应用侧安装
使用 batchInstall
// router.ts
import { batchInstall } from 'vue-router-plugin-system'
import { AuthPlugin, CachePlugin } from './plugins'
const router = createRouter({
history: createWebHistory(),
routes: [],
})
// 紧跟在 createRouter 之后调用
batchInstall(router, [AuthPlugin, CachePlugin])2
3
4
5
6
7
8
9
10
11
使用 createRouter
import { createWebHistory } from 'vue-router'
import { createRouter } from 'vue-router-plugin-system'
import { AuthPlugin, CachePlugin } from './plugins'
const router = createRouter({
history: createWebHistory(),
routes: [],
// 新增插件选项
plugins: [AuthPlugin, CachePlugin],
})2
3
4
5
6
7
8
9
10
核心特性
标准化插件接口
提供统一的 RouterPlugin 接口:
type RouterPlugin = (ctx: RouterPluginContext) => void
interface RouterPluginContext {
router: Router // Vue Router 实例
runWithAppContext: (handler: (app: App) => void) => void // 在 App 上下文中执行
onUninstall: (handler: () => void) => void // 注册清理回调
}2
3
4
5
6
7
自动清理响应式副作用
插件中创建的响应式副作用(watch、computed 等)会在卸载时自动清理,无需手动管理 effectScope。
API 参考
核心 API
createRouter(options)- 扩展版路由创建函数,支持plugins选项withInstall(plugin)- 包装插件,支持app.use()和Plugin.install(router)两种安装方式batchInstall(router, plugins)- 批量安装多个插件
插件上下文
interface RouterPluginContext {
router: Router // Vue Router 实例
runWithAppContext: (handler: (app: App) => void) => void // 在 App 上下文中执行
onUninstall: (handler: () => void) => void // 注册清理回调
}2
3
4
5
router- 用于添加路由守卫、访问路由信息、编程式导航runWithAppContext- 当需要使用inject()、pinia store等 App 上下文 API 时使用onUninstall- 注册清理回调,在应用卸载时按顺序执行
生命周期
- 所有插件在共享的
effectScope中运行,响应式副作用自动清理 - 插件按注册顺序初始化和清理
- 每个 Router 实例的
install只会被包装一次
