Vue 框架的路由模块之路由配置文件深入分析
一、引言
在现代前端开发中,单页面应用(SPA)已经成为主流,而 Vue.js 作为一款流行的 JavaScript 框架,其路由模块 Vue Router 在实现 SPA 的页面导航和路由切换方面起着至关重要的作用。路由配置文件是 Vue Router 的核心组成部分,它定义了应用的路由规则、路由守卫、路由懒加载等功能。深入理解路由配置文件的源码和工作原理,有助于开发者更好地使用 Vue Router,优化应用的性能和用户体验。
本文将从源码级别深入分析 Vue Router 的路由配置文件,详细介绍路由配置的各个方面,包括基本路由配置、路由守卫、路由懒加载、路由元信息等。通过对源码的解读,我们将了解每个配置项的作用和实现原理,以及它们之间的相互关系。
二、Vue Router 基础概述
2.1 Vue Router 简介
Vue Router 是 Vue.js 官方的路由管理器,它实现了单页面应用的路由功能,允许开发者通过定义路由规则来实现页面的导航和切换。Vue Router 提供了丰富的功能,如路由参数、路由守卫、路由懒加载等,使得开发者可以方便地构建复杂的单页面应用。
2.2 安装和使用 Vue Router
在使用 Vue Router 之前,需要先安装它。可以使用 npm 或 yarn 进行安装:
bash
# 使用 npm 安装
npm install vue-router
# 使用 yarn 安装
yarn add vue-router
安装完成后,在 Vue 项目中引入并使用 Vue Router:
javascript
// 引入 Vue 和 Vue Router
import Vue from 'vue';
import VueRouter from 'vue-router';
// 使用 Vue Router 插件
Vue.use(VueRouter);
// 定义路由配置
const routes = [
{
path: '/',
name: 'Home',
// 这里可以是一个组件,用于显示在匹配的路由上
component: () => import('@/views/Home.vue')
},
{
path: '/about',
name: 'About',
// 这里可以是一个组件,用于显示在匹配的路由上
component: () => import('@/views/About.vue')
}
];
// 创建路由实例
const router = new VueRouter({
// 路由模式,这里使用历史模式
mode: 'history',
// 路由配置
routes
});
// 创建 Vue 实例并挂载路由
new Vue({
// 挂载路由实例
router,
render: h => h(App)
}).$mount('#app');
上述代码展示了 Vue Router 的基本使用步骤:引入 Vue Router 插件、定义路由配置、创建路由实例并挂载到 Vue 实例上。
三、路由配置文件的基本结构
3.1 路由配置数组
路由配置文件的核心是一个路由配置数组,数组中的每个元素都是一个路由对象,定义了一个路由规则。路由对象包含了路由的路径、名称、组件等信息。以下是一个简单的路由配置数组示例:
javascript
const routes = [
{
// 路由路径
path: '/',
// 路由名称
name: 'Home',
// 路由对应的组件
component: () => import('@/views/Home.vue')
},
{
path: '/about',
name: 'About',
component: () => import('@/views/About.vue')
}
];
3.2 路由对象的基本属性
3.2.1 path
path
属性定义了路由的路径,它是一个字符串,可以包含动态参数。例如:
javascript
{
// 包含动态参数 :id 的路径
path: '/user/:id',
name: 'User',
component: () => import('@/views/User.vue')
}
在上述示例中,:id
是一个动态参数,表示该路径可以匹配 /user/1
、/user/2
等不同的路径。
3.2.2 name
name
属性为路由指定一个唯一的名称,方便在代码中通过名称来引用路由。例如:
javascript
{
path: '/about',
// 路由名称
name: 'About',
component: () => import('@/views/About.vue')
}
在代码中可以通过名称来导航到该路由:
javascript
// 通过路由名称导航到 About 路由
this.$router.push({ name: 'About' });
3.2.3 component
component
属性指定了路由匹配时要渲染的组件。可以是一个组件对象,也可以是一个返回组件的函数(用于路由懒加载)。例如:
javascript
// 直接引入组件
import Home from '@/views/Home.vue';
const routes = [
{
path: '/',
name: 'Home',
// 直接使用组件对象
component: Home
},
{
path: '/about',
name: 'About',
// 路由懒加载,返回一个 Promise
component: () => import('@/views/About.vue')
}
];
3.3 路由配置文件的源码分析
3.3.1 路由配置的初始化
在创建路由实例时,会对路由配置进行初始化。以下是 VueRouter
构造函数的部分源码:
javascript
export default class VueRouter {
constructor(options = {}) {
// 初始化路由配置
this.options = options;
// 初始化路由记录
this.routeMap = {};
// 初始化路由匹配器
this.matcher = createMatcher(options.routes || [], this);
// 其他初始化操作...
}
}
在上述源码中,options.routes
就是我们定义的路由配置数组。createMatcher
函数会根据路由配置数组创建一个路由匹配器,用于后续的路由匹配。
3.3.2 createMatcher 函数
createMatcher
函数的主要作用是将路由配置数组转换为路由记录,并创建一个路由匹配器。以下是 createMatcher
函数的部分源码:
javascript
export function createMatcher(routes, router) {
// 存储路由记录的数组
const routeMap = Object.create(null);
// 扁平化路由配置
const pathList = [];
// 注册路由配置
routes.forEach(route => {
addRouteRecord(pathList, routeMap, route);
});
// 返回路由匹配器对象
return {
// 匹配路由的方法
match,
// 添加路由的方法
addRoutes
};
}
在上述源码中,addRouteRecord
函数会将每个路由对象转换为路由记录,并存储在 routeMap
中。pathList
存储了所有的路由路径,方便后续的匹配。
3.3.3 addRouteRecord 函数
addRouteRecord
函数用于将路由对象转换为路由记录。以下是 addRouteRecord
函数的部分源码:
javascript
function addRouteRecord(pathList, routeMap, route, parent) {
// 获取路由的路径
const path = parent ? normalizePath(route.path, parent) : route.path;
// 创建路由记录对象
const record = {
// 路由路径
path,
// 路由名称
name: route.name,
// 路由对应的组件
component: route.component,
// 父路由记录
parent,
// 其他属性...
};
// 如果路由有子路由,递归处理子路由
if (route.children) {
route.children.forEach(child => {
addRouteRecord(pathList, routeMap, child, record);
});
}
// 将路由记录添加到路由映射表中
if (!routeMap[path]) {
pathList.push(path);
routeMap[path] = record;
}
}
在上述源码中,normalizePath
函数用于处理路由路径,确保路径的正确性。如果路由有子路由,会递归调用 addRouteRecord
函数处理子路由。
四、路由参数和路由查询
4.1 路由参数
路由参数是在路由路径中定义的动态部分,用于传递数据。在路由配置中,可以使用 :
来定义路由参数。例如:
javascript
const routes = [
{
// 定义路由参数 :id
path: '/user/:id',
name: 'User',
component: () => import('@/views/User.vue')
}
];
在组件中可以通过 $route.params
来获取路由参数。例如:
vue
<template>
<div>
<!-- 显示路由参数 id -->
<p>用户 ID: {{ $route.params.id }}</p>
</div>
</template>
<script>
export default {
name: 'User'
};
</script>
4.2 路由查询
路由查询是通过 URL 中的查询字符串传递数据。在路由配置中,不需要特别定义路由查询,只需要在导航时传递查询参数即可。例如:
javascript
// 通过路由名称导航,并传递查询参数
this.$router.push({ name: 'User', query: { name: 'John' } });
在组件中可以通过 $route.query
来获取路由查询参数。例如:
vue
<template>
<div>
<!-- 显示路由查询参数 name -->
<p>用户姓名: {{ $route.query.name }}</p>
</div>
</template>
<script>
export default {
name: 'User'
};
</script>
4.3 源码分析
4.3.1 路由参数的匹配
在路由匹配过程中,会处理路由参数。以下是 match
函数的部分源码:
javascript
function match(raw, currentRoute, redirectedFrom) {
// 解析路由路径和查询参数
const location = normalizeLocation(raw, currentRoute, false, redirectedFrom);
const { name, path, query, hash } = location;
// 查找匹配的路由记录
let record;
if (name) {
record = routeMapByName[name];
} else if (path) {
// 处理路由参数
record = matchRoute(path, pathList, routeMap);
}
// 其他处理...
return {
// 匹配的路由记录
route: record,
// 路由参数
params: location.params,
// 路由查询参数
query,
// 路由哈希值
hash
};
}
在上述源码中,matchRoute
函数会处理路由路径中的路由参数,并返回匹配的路由记录。
4.3.2 路由查询的处理
在 normalizeLocation
函数中,会处理路由查询参数。以下是 normalizeLocation
函数的部分源码:
javascript
function normalizeLocation(raw, current, append, redirectedFrom) {
let next = typeof raw === 'string' ? { path: raw } : raw;
// 处理路由查询参数
if (next.query && typeof next.query === 'string') {
next.query = parseQuery(next.query);
}
// 其他处理...
return next;
}
在上述源码中,parseQuery
函数会将查询字符串解析为对象。
五、路由守卫
5.1 全局路由守卫
全局路由守卫是在路由切换前或切换后执行的函数,用于进行全局的路由控制。Vue Router 提供了三种全局路由守卫:beforeEach
、beforeResolve
和 afterEach
。
5.1.1 beforeEach
beforeEach
是在路由切换前执行的守卫,它接收三个参数:to
、from
和 next
。to
表示即将进入的路由,from
表示当前离开的路由,next
是一个函数,用于控制路由的跳转。例如:
javascript
// 全局前置守卫
router.beforeEach((to, from, next) => {
// 检查用户是否登录
if (to.meta.requireAuth && !isAuthenticated()) {
// 如果未登录,跳转到登录页面
next({ name: 'Login' });
} else {
// 允许路由跳转
next();
}
});
5.1.2 beforeResolve
beforeResolve
是在路由解析完成后执行的守卫,它的使用方式和 beforeEach
类似。
5.1.3 afterEach
afterEach
是在路由切换后执行的守卫,它接收两个参数:to
和 from
,不接收 next
函数。例如:
javascript
// 全局后置守卫
router.afterEach((to, from) => {
// 记录路由切换日志
console.log(`从 ${from.name} 路由切换到 ${to.name} 路由`);
});
5.2 路由独享守卫
路由独享守卫是在单个路由配置中定义的守卫,只对该路由生效。可以使用 beforeEnter
来定义路由独享守卫。例如:
javascript
const routes = [
{
path: '/admin',
name: 'Admin',
component: () => import('@/views/Admin.vue'),
// 路由独享守卫
beforeEnter: (to, from, next) => {
// 检查用户是否有管理员权限
if (isAdmin()) {
// 允许路由跳转
next();
} else {
// 跳转到首页
next({ name: 'Home' });
}
}
}
];
5.3 组件内守卫
组件内守卫是在组件中定义的守卫,用于控制组件的进入和离开。Vue Router 提供了三种组件内守卫:beforeRouteEnter
、beforeRouteUpdate
和 beforeRouteLeave
。
5.3.1 beforeRouteEnter
beforeRouteEnter
是在路由进入组件前执行的守卫,它接收三个参数:to
、from
和 next
。由于此时组件实例还未创建,不能直接访问 this
。可以通过 next
函数的回调来访问组件实例。例如:
vue
<template>
<div>
<!-- 组件内容 -->
</div>
</template>
<script>
export default {
name: 'User',
beforeRouteEnter(to, from, next) {
// 检查用户是否有访问权限
if (hasAccess(to.params.id)) {
// 允许路由跳转,并在组件实例创建后访问实例
next(vm => {
// 可以在这里访问组件实例 vm
vm.initData();
});
} else {
// 跳转到无权限页面
next({ name: 'NoAccess' });
}
}
};
</script>
5.3.2 beforeRouteUpdate
beforeRouteUpdate
是在路由更新时执行的守卫,它接收三个参数:to
、from
和 next
。此时组件实例已经存在,可以直接访问 this
。例如:
vue
<template>
<div>
<!-- 组件内容 -->
</div>
</template>
<script>
export default {
name: 'User',
beforeRouteUpdate(to, from, next) {
// 更新组件数据
this.updateData(to.params.id);
// 允许路由跳转
next();
}
};
</script>
5.3.3 beforeRouteLeave
beforeRouteLeave
是在路由离开组件前执行的守卫,它接收三个参数:to
、from
和 next
。常用于提示用户是否保存未保存的数据。例如:
vue
<template>
<div>
<!-- 组件内容 -->
</div>
</template>
<script>
export default {
name: 'User',
beforeRouteLeave(to, from, next) {
// 检查是否有未保存的数据
if (this.hasUnsavedData()) {
// 提示用户是否保存数据
if (confirm('有未保存的数据,是否保存?')) {
this.saveData();
}
}
// 允许路由跳转
next();
}
};
</script>
5.4 源码分析
5.4.1 全局路由守卫的注册和执行
在 VueRouter
构造函数中,会初始化全局路由守卫。以下是部分源码:
javascript
export default class VueRouter {
constructor(options = {}) {
// 初始化全局前置守卫数组
this.beforeHooks = [];
// 初始化全局解析守卫数组
this.resolveHooks = [];
// 初始化全局后置守卫数组
this.afterHooks = [];
// 注册全局前置守卫
options.beforeEach && this.beforeEach(options.beforeEach);
// 注册全局解析守卫
options.beforeResolve && this.beforeResolve(options.beforeResolve);
// 注册全局后置守卫
options.afterEach && this.afterEach(options.afterEach);
// 其他初始化操作...
}
// 注册全局前置守卫的方法
beforeEach(fn) {
this.beforeHooks.push(fn);
return () => {
const i = this.beforeHooks.indexOf(fn);
if (i > -1) this.beforeHooks.splice(i, 1);
};
}
// 注册全局解析守卫的方法
beforeResolve(fn) {
this.resolveHooks.push(fn);
return () => {
const i = this.resolveHooks.indexOf(fn);
if (i > -1) this.resolveHooks.splice(i, 1);
};
}
// 注册全局后置守卫的方法
afterEach(fn) {
this.afterHooks.push(fn);
return () => {
const i = this.afterHooks.indexOf(fn);
if (i > -1) this.afterHooks.splice(i, 1);
};
}
}
在路由切换时,会依次执行全局前置守卫、路由独享守卫、组件内守卫等。以下是路由切换时执行守卫的部分源码:
javascript
function runQueue(queue, from, to, iterator, cb) {
const step = index => {
if (index >= queue.length) {
// 所有守卫执行完毕,调用回调函数
cb();
} else {
if (queue[index]) {
// 执行当前守卫
iterator(queue[index], to, from, () => {
// 递归执行下一个守卫
step(index + 1);
});
} else {
step(index + 1);
}
}
};
step(0);
}
// 执行路由守卫
function ensureTransitionHooks(to, from) {
const guards = [];
// 添加全局前置守卫
guards.push(...this.beforeHooks);
// 添加路由独享守卫
to.matched.forEach(record => {
record.beforeEnter && guards.push(record.beforeEnter);
});
// 添加组件内守卫
to.matched.forEach((record, i) => {
const component = record.components.default;
if (component) {
const hooks = [];
if (i > 0) {
const prevRecord = from.matched[i - 1];
if (prevRecord) {
// 添加 beforeRouteUpdate 守卫
component.beforeRouteUpdate && hooks.push(component.beforeRouteUpdate);
}
}
if (i === to.matched.length - 1) {
// 添加 beforeRouteEnter 守卫
component.beforeRouteEnter && hooks.push(component.beforeRouteEnter);
}
guards.push(...hooks);
}
});
// 添加全局解析守卫
guards.push(...this.resolveHooks);
return guards;
}
在上述源码中,runQueue
函数用于依次执行守卫队列中的守卫。ensureTransitionHooks
函数用于收集所有需要执行的守卫。
六、路由懒加载
6.1 路由懒加载的概念
路由懒加载是指在需要时才加载路由对应的组件,而不是在应用初始化时就加载所有组件。这样可以减少应用的初始加载时间,提高应用的性能。
6.2 路由懒加载的实现方式
6.2.1 使用动态 import()
在 Vue Router 中,可以使用动态 import()
语法来实现路由懒加载。例如:
javascript
const routes = [
{
path: '/',
name: 'Home',
// 路由懒加载,使用动态 import()
component: () => import('@/views/Home.vue')
},
{
path: '/about',
name: 'About',
component: () => import('@/views/About.vue')
}
];
6.2.2 分组懒加载
可以将多个路由组件分组进行懒加载,这样可以将相关的组件打包在一起。例如:
javascript
// 定义一个分组懒加载的函数
const loadGroup = () => import('@/views/Group.vue');
const routes = [
{
path: '/group1',
name: 'Group1',
// 分组懒加载
component: loadGroup
},
{
path: '/group2',
name: 'Group2',
component: loadGroup
}
];
6.3 源码分析
6.3.1 路由组件的加载
在路由匹配时,如果发现路由组件是一个函数,会调用该函数来加载组件。以下是 createRoute
函数的部分源码:
javascript
function createRoute(record, location, redirectedFrom) {
const route = {
// 路由记录
name: location.name || (record && record.name),
// 路由路径
path: location.path || '/',
// 路由参数
params: location.params || {},
// 路由查询参数
query: location.query || {},
// 路由哈希值
hash: location.hash || '',
// 路由匹配的记录数组
matched: record ? formatMatch(record) : [],
// 重定向的路由
redirectedFrom
};
if (record && record.component) {
if (typeof record.component === 'function') {
// 如果组件是一个函数,调用该函数加载组件
record.component().then(component => {
record.component = component;
// 触发路由更新
router.app.$forceUpdate();
});
}
}
return Object.freeze(route);
}
在上述源码中,当发现路由组件是一个函数时,会调用该函数并在组件加载完成后更新路由记录的组件属性。
七、路由元信息
7.1 路由元信息的概念
路由元信息是在路由配置中定义的额外信息,用于存储与路由相关的自定义数据。可以在路由配置中使用 meta
属性来定义路由元信息。例如:
javascript
const routes = [
{
path: '/admin',
name: 'Admin',
component: () => import('@/views/Admin.vue'),
// 定义路由元信息
meta: {
// 表示该路由需要登录权限
requireAuth: true
}
}
];
7.2 路由元信息的使用
可以在路由守卫中使用路由元信息来进行权限控制。例如:
javascript
router.beforeEach((to, from, next) => {
// 检查路由是否需要登录权限
if (to.meta.requireAuth && !isAuthenticated()) {
// 如果未登录,跳转到登录页面
next({ name: 'Login' });
} else {
// 允许路由跳转
next();
}
});
7.3 源码分析
7.3.1 路由元信息的存储
在 addRouteRecord
函数中,会将路由元信息存储在路由记录中。以下是部分源码:
javascript
function addRouteRecord(pathList, routeMap, route, parent) {
const path = parent ? normalizePath(route.path, parent) : route.path;
const record = {
path,
name: route.name,
component: route.component,
parent,
// 存储路由元信息
meta: route.meta || {}
};
// 其他处理...
if (!routeMap[path]) {
pathList.push(path);
routeMap[path] = record;
}
}
7.3.2 路由元信息的获取
在路由匹配时,可以通过 to.matched
数组来获取匹配的路由记录,从而获取路由元信息。例如:
javascript
router.beforeEach((to, from, next) => {
// 遍历匹配的路由记录
to.matched.forEach(record => {
// 获取路由元信息
const meta = record.meta;
if (meta.requireAuth && !isAuthenticated()) {
next({ name: 'Login' });
return;
}
});
next();
});
八、嵌套路由
8.1 嵌套路由的概念
嵌套路由是指在一个路由组件中嵌套另一个路由组件。通过嵌套路由,可以实现复杂的页面布局和导航。
8.2 嵌套路由的配置
在路由配置中,可以使用 children
属性来定义子路由。例如:
javascript
const routes = [
{
path: '/user',
name: 'User',
component: () => import('@/views/User.vue'),
// 定义子路由
children: [
{
path: 'profile',
name: 'UserProfile',
component: () => import('@/views/UserProfile.vue')
},
{
path: 'settings',
name: 'UserSettings',
component: () => import('@/views/UserSettings.vue')
}
]
}
];
在上述示例中,/user/profile
和 /user/settings
是 /user
的子路由。
8.3 嵌套路由的实现原理
8.3.1 路由记录的生成
在 addRouteRecord
函数中,会递归处理子路由,生成嵌套的路由记录。以下是部分源码:
javascript
function addRouteRecord(pathList, routeMap, route, parent) {
const path = parent ? normalizePath(route.path, parent) : route.path;
const record = {
path,
name: route.name,
component: route.component,
parent,
meta: route.meta || {}
};
// 如果路由有子路由,递归处理子路由
if (route.children) {
route.children.forEach(child => {
addRouteRecord(pathList, routeMap, child, record);
});
}
if (!routeMap[path]) {
pathList.push(path);
routeMap[path] = record;
}
}
8.3.2 嵌套路由的渲染
在路由匹配时,会根据匹配的路由记录渲染对应的组件。对于嵌套路由,会在父组件中渲染子组件。例如,在 User.vue
组件中,可以使用 <router-view>
来渲染子组件:
vue
<template>
<div>
<h1>用户页面</h1>
<!-- 渲染子组件 -->
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'User'
};
</script>
九、命名视图
9.1 命名视图的概念
命名视图是指为 <router-view>
组件指定一个名称,以便在一个路由中渲染多个组件。通过命名视图,可以实现复杂的页面布局。
9.2 命名视图的配置
在路由配置中,可以使用 components
属性来定义命名视图。例如:
javascript
const routes = [
{
path: '/',
components: {
// 默认视图
default: () => import('@/views/Home.vue'),
// 命名视图 sidebar
sidebar: () => import('@/views/Sidebar.vue')
}
}
];
在模板中,可以使用 name
属性为 <router-view>
指定名称:
vue
<template>
<div>
<!-- 默认视图 -->
<router-view></router-view>
<!-- 命名视图 sidebar -->
<router-view name="sidebar"></router-view>
</div>
</template>
<script>
export default {
name: 'App'
};
</script>
9.3 源码分析
9.3.1 命名视图的处理
在 addRouteRecord
函数中,会处理命名视图。以下是部分源码:
javascript
function addRouteRecord(pathList, routeMap, route, parent) {
const path = parent ? normalizePath(route.path, parent) : route.path;
const record = {
path,
name: route.name,
// 处理命名视图
components: route.components || { default: route.component },
parent,
meta: route.meta || {}
};
// 其他处理...
if (!routeMap[path]) {
pathList.push(path);
routeMap[path] = record;
}
}
9.3.2 命名视图的渲染
在路由匹配时,会根据命名视图的配置渲染对应的组件。以下是 createRoute
函数的部分源码:
javascript
function createRoute(record, location, redirectedFrom) {
const route = {
name: location.name || (record && record.name),
path: location.path || '/',
params: location.params || {},
query: location.query || {},
hash: location.hash || '',
matched: record ? formatMatch(record) : [],
redirectedFrom
};
if (record) {
const { components } = record;
Object.keys(components).forEach(key => {
const component = components[key];
if (typeof component === 'function') {
component().then(comp => {
components[key] = comp;
router.app.$forceUpdate();
});
}
});
}
return Object.freeze(route);
}
在上述源码中,会遍历 components
对象,加载并渲染命名视图对应的组件。
十、路由导航钩子的执行顺序
10.1 导航钩子的执行顺序规则
在路由切换时,导航钩子的执行顺序如下:
- 调用全局前置守卫
beforeEach
。 - 调用路由独享守卫
beforeEnter
。 - 调用组件内守卫
beforeRouteEnter
。 - 调用全局解析守卫
beforeResolve
。 - 调用全局后置守卫
afterEach
。 - 调用组件内守卫
beforeRouteUpdate
(如果路由更新)。 - 调用组件内守卫
beforeRouteLeave
(如果路由离开)。
10.2 源码分析
10.2.1 导航钩子的执行流程
在路由切换时,会按照上述顺序依次执行导航钩子。以下是路由切换时执行导航钩子的部分源码:
javascript
function transitionTo(location, onComplete, onAbort) {
const route = this.match(location, this.current);
const prev = this.current;
const from = prev;
const to = route;
// 执行全局前置守卫
const beforeHooks = this.beforeHooks;
runQueue(beforeHooks, from, to, (hook, to, from, next) => {
hook(to, from, next);
}, () => {
// 执行路由独享守卫
const enterHooks = ensureEnterHooks(to);
runQueue(enterHooks, from, to, (hook, to, from, next) => {
hook(to, from, next);
}, () => {
// 执行组件内守卫 beforeRouteEnter
const enterCbs = [];
const enterGuards = ensureEnterGuards(to, from, enterCbs);
runQueue(enterGuards, from, to, (guard, to, from, next) => {
guard(to, from, next);
}, () => {
// 执行全局解析守卫
const resolveHooks = this.resolveHooks;
runQueue(resolveHooks, from, to, (hook, to, from, next) => {
hook(to, from, next);
}, () => {
// 更新当前路由
this.updateRoute(to);
// 执行全局后置守卫
const afterHooks = this.afterHooks;
afterHooks.forEach(hook => hook(to, from));
if (onComplete) onComplete(to);
});
});
});
});
}
在上述源码中,通过嵌套的 runQueue
函数依次执行各个导航钩子。
十一、路由模式
11.1 路由模式的类型
Vue Router 提供了两种路由模式:hash
模式和 history
模式。
11.1.1 hash 模式
hash
模式是 Vue Router 的默认模式,它使用 URL 的哈希值(#
)来实现路由切换。例如,http://example.com/#/home
。哈希值的变化不会向服务器发送请求,因此可以在不刷新页面的情况下实现路由切换。
11.1.2 history 模式
history
模式使用 HTML5 的 History API
来实现路由切换。它使用真实的 URL 路径,例如,http://example.com/home
。在 history
模式下,路由切换会向服务器发送请求,因此需要服务器进行相应的配置。
11.2 路由模式的配置
在创建路由实例时,可以通过 mode
属性来配置路由模式。例如:
javascript
const router = new VueRouter({
// 使用 history 模式
mode: 'history',
routes
});
11.3 源码分析
11.3.1 路由模式的初始化
在 VueRouter
构造函数中,会根据 mode
属性初始化路由模式。以下是部分源码:
javascript
export default class VueRouter {
constructor(options = {}) {
// 其他初始化操作...
// 初始化路由模式
this.mode = options.mode || 'hash';
switch (this.mode) {
case 'history':
this.history = new HTML5History(this, options.base);
break;
case 'hash':
this.history = new HashHistory(this, options.base);
javascript
switch (this.mode) {
case 'history':
// 创建 HTML5History 实例,用于处理 history 模式
this.history = new HTML5History(this, options.base);
break;
case 'hash':
// 创建 HashHistory 实例,用于处理 hash 模式
this.history = new HashHistory(this, options.base);
break;
case 'abstract':
// 创建 AbstractHistory 实例,用于抽象模式,通常用于非浏览器环境
this.history = new AbstractHistory(this, options.base);
break;
default:
if (process.env.NODE_ENV !== 'production') {
// 在非生产环境下,若模式配置错误给出警告
assert(false, `invalid mode: ${this.mode}`);
}
}
// 其他初始化操作...
}
}
在上述代码中,根据 mode
属性的值,创建不同的 History
实例。HTML5History
用于处理 history
模式,它利用 HTML5 的 History API
进行路由管理;HashHistory
用于处理 hash
模式,通过监听 URL 的哈希值变化来实现路由切换;AbstractHistory
主要用于非浏览器环境,例如服务器端渲染或原生应用中。
11.3.2 HashHistory 模式源码分析
HashHistory
模式的核心在于监听 URL 哈希值的变化,当哈希值改变时触发路由切换。以下是 HashHistory
类的部分关键源码:
javascript
export class HashHistory extends History {
constructor(router, base) {
super(router, base);
// 确保初始哈希值存在
ensureSlash();
// 监听哈希值变化
this.setupListeners();
}
// 确保 URL 以哈希和斜杠开头
ensureSlash() {
const path = this.getCurrentPath();
if (path.charAt(0) === '/') {
return true;
}
// 如果没有哈希,设置默认哈希
replaceHash('/' + path);
return false;
}
// 设置哈希值变化的监听器
setupListeners() {
const router = this.router;
window.addEventListener('hashchange', () => {
if (!ensureSlash()) {
return;
}
// 当哈希值变化时,进行路由跳转
this.transitionTo(getHash(), route => {
if (supportsScroll) {
handleScroll(router, route, this.current, true);
}
});
});
}
// 获取当前哈希值对应的路径
getCurrentPath() {
return getHash().replace(/#(.*)$/, '$1');
}
// 进行路由跳转
transitionTo(location, onComplete, onAbort) {
super.transitionTo(location, route => {
// 更新哈希值
this.updateHash(route.fullPath);
if (onComplete) {
onComplete(route);
}
}, onAbort);
}
// 更新 URL 的哈希值
updateHash(path) {
if (supportsPushState) {
pushState(getUrl(path));
} else {
window.location.hash = path;
}
}
}
ensureSlash
方法:确保 URL 以哈希和斜杠开头,如果没有则进行设置。setupListeners
方法:监听hashchange
事件,当哈希值变化时调用transitionTo
方法进行路由跳转。getCurrentPath
方法:获取当前哈希值对应的路径。transitionTo
方法:调用父类的transitionTo
方法进行路由跳转,并在跳转完成后更新哈希值。updateHash
方法:根据浏览器是否支持pushState
来更新 URL 的哈希值。
11.3.3 HTML5History 模式源码分析
HTML5History
模式使用 HTML5 的 History API
来管理路由,通过 pushState
和 replaceState
方法改变 URL 而不刷新页面。以下是 HTML5History
类的部分关键源码:
javascript
export class HTML5History extends History {
constructor(router, base) {
super(router, base);
const expectScroll = router.options.scrollBehavior;
const supportsScroll = supportsPushState && expectScroll;
if (supportsScroll) {
setupScroll();
}
const initPath = this.getCurrentLocation();
// 初始化路由
this.transitionTo(initPath, route => {
if (supportsScroll) {
handleScroll(router, route, null, true);
}
});
// 监听 popstate 事件
window.addEventListener('popstate', e => {
const current = this.current;
// 获取当前路径
const path = this.getCurrentLocation();
this.transitionTo(path, route => {
if (supportsScroll) {
handleScroll(router, route, current, true);
}
});
});
}
// 获取当前 URL 的路径
getCurrentLocation() {
const path = getLocation(this.base);
return path;
}
// 进行路由跳转
transitionTo(location, onComplete, onAbort) {
const current = this.current;
super.transitionTo(location, route => {
// 更新历史记录
this.updateRoute(route);
if (supportsPushState) {
this.push(route.fullPath);
} else {
window.location = route.fullPath;
}
if (onComplete) {
onComplete(route);
}
}, onAbort);
}
// 使用 pushState 方法添加历史记录
push(location) {
this.saveScrollPosition();
const state = this.getCurrentState();
pushState(cleanPath(this.base + location), state);
}
// 使用 replaceState 方法替换历史记录
replace(location) {
this.saveScrollPosition();
const state = this.getCurrentState();
replaceState(cleanPath(this.base + location), state);
}
}
constructor
方法:初始化路由,监听popstate
事件,当用户点击浏览器的前进或后退按钮时触发该事件,调用transitionTo
方法进行路由跳转。getCurrentLocation
方法:获取当前 URL 的路径。transitionTo
方法:调用父类的transitionTo
方法进行路由跳转,并在跳转完成后更新历史记录。push
方法:使用pushState
方法添加历史记录。replace
方法:使用replaceState
方法替换历史记录。
12. 路由导航的实现细节
12.1 路由导航的触发方式
在 Vue Router 中,路由导航可以通过多种方式触发,包括声明式导航和编程式导航。
12.1.1 声明式导航
声明式导航是通过 <router-link>
组件来实现的,它会渲染为一个 <a>
标签,点击该标签会触发路由跳转。例如:
vue
<template>
<div>
<!-- 声明式导航到 Home 路由 -->
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
</div>
</template>
<router-link>
组件的 to
属性可以是一个字符串路径,也可以是一个对象,用于传递路由参数和查询参数。例如:
vue
<template>
<div>
<!-- 传递路由参数 -->
<router-link :to="{ name: 'User', params: { id: 1 } }">User 1</router-link>
<!-- 传递查询参数 -->
<router-link :to="{ name: 'Search', query: { keyword: 'vue' } }">Search Vue</router-link>
</div>
</template>
12.1.2 编程式导航
编程式导航是通过 this.$router
实例的方法来实现的,常用的方法有 push
、replace
和 go
。
push
方法:向历史记录中添加一条新的路由记录,相当于点击浏览器的链接。例如:
javascript
// 编程式导航到 Home 路由
this.$router.push('/');
// 传递路由参数和查询参数
this.$router.push({ name: 'User', params: { id: 1 }, query: { keyword: 'vue' } });
replace
方法:替换当前的路由记录,不会向历史记录中添加新的记录,相当于使用window.location.replace
。例如:
javascript
// 替换当前路由为 About 路由
this.$router.replace('/about');
go
方法:在历史记录中前进或后退指定的步数。例如:
javascript
// 后退一步
this.$router.go(-1);
// 前进一步
this.$router.go(1);
12.2 路由导航的实现原理
12.2.1 <router-link>
组件的实现
<router-link>
组件是一个自定义组件,它会根据 to
属性生成一个 <a>
标签,并处理点击事件。以下是 <router-link>
组件的部分关键源码:
javascript
export default {
name: 'router-link',
props: {
to: {
type: [String, Object],
required: true
},
// 其他属性...
},
render(h) {
const { to, tag, exact, append, replace, event } = this;
const router = this.$router;
const current = this.$route;
const { location, route, href } = router.resolve(to, current, append);
const classes = {};
const globalActiveClass = router.options.linkActiveClass;
const globalExactActiveClass = router.options.linkExactActiveClass;
const activeClassFallback = globalActiveClass || 'router-link-active';
const exactActiveClassFallback = globalExactActiveClass || 'router-link-exact-active';
const activeClass = this.activeClass || activeClassFallback;
const exactActiveClass = this.exactActiveClass || exactActiveClassFallback;
const compareTarget = location.path
? createRoute(null, location, null, router)
: route;
classes[activeClass] = isActive(router, current, compareTarget, exact);
classes[exactActiveClass] = isExactActive(router, current, compareTarget);
const handler = e => {
if (guardEvent(e)) {
if (replace) {
router.replace(location);
} else {
router.push(location);
}
}
};
const on = { click: guardEventHandlers(handler, event) };
return h(tag, {
class: classes,
attrs: { href },
on
}, this.$slots.default);
}
};
render
方法:根据to
属性生成<a>
标签,处理点击事件。当点击<a>
标签时,会调用router.push
或router.replace
方法进行路由跳转。isActive
和isExactActive
方法:用于判断当前路由是否激活,根据激活状态添加相应的 CSS 类。
12.2.2 编程式导航的实现
编程式导航的方法(push
、replace
和 go
)是通过 VueRouter
实例的方法来实现的。以下是 push
方法的部分关键源码:
javascript
export default class VueRouter {
// 其他方法...
push(location, onComplete, onAbort) {
this.history.push(location, onComplete, onAbort);
}
}
class History {
// 其他方法...
push(location, onComplete, onAbort) {
const { current: fromRoute } = this;
this.transitionTo(location, route => {
// 更新历史记录
this.updateRoute(route);
if (this.router.options.mode === 'hash') {
// 如果是 hash 模式,更新哈希值
this.updateHash(route.fullPath);
} else if (supportsPushState) {
// 如果支持 pushState,使用 pushState 方法添加历史记录
pushState(cleanPath(this.base + route.fullPath));
} else {
window.location = route.fullPath;
}
if (onComplete) {
onComplete(route);
}
}, onAbort);
}
}
push
方法:调用history.push
方法进行路由跳转,根据路由模式更新历史记录。
13. 路由配置文件的高级应用
13.3 路由过渡效果
在 Vue Router 中,可以为路由切换添加过渡效果,增强用户体验。可以使用 Vue 的 <transition>
或 <transition-group>
组件来实现路由过渡效果。
13.3.1 使用 <transition>
组件
vue
<template>
<div id="app">
<!-- 使用 <transition> 组件添加过渡效果 -->
<transition name="fade">
<router-view></router-view>
</transition>
</div>
</template>
<style>
/* 定义淡入淡出过渡效果 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>
在上述代码中,使用 <transition>
组件包裹 <router-view>
,并为其指定 name
属性为 fade
。然后在 CSS 中定义 fade-enter-active
、fade-leave-active
、fade-enter
和 fade-leave-to
类,实现淡入淡出的过渡效果。
13.3.2 基于路由的过渡效果
可以根据不同的路由设置不同的过渡效果。例如:
vue
<template>
<div id="app">
<!-- 根据路由元信息设置过渡效果 -->
<transition :name="transitionName">
<router-view></router-view>
</transition>
</div>
</template>
<script>
export default {
computed: {
transitionName() {
// 根据路由元信息返回过渡效果名称
return this.$route.meta.transition || 'fade';
}
}
};
</script>
<style>
/* 淡入淡出过渡效果 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
/* 滑动过渡效果 */
.slide-enter-active,
.slide-leave-active {
transition: transform 0.5s;
}
.slide-enter {
transform: translateX(100%);
}
.slide-leave-to {
transform: translateX(-100%);
}
</style>
在路由配置中,可以为路由添加 meta
信息来指定过渡效果:
javascript
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue'),
meta: {
// 指定过渡效果为 fade
transition: 'fade'
}
},
{
path: '/about',
name: 'About',
component: () => import('@/views/About.vue'),
meta: {
// 指定过渡效果为 slide
transition: 'slide'
}
}
];
13.4 路由的滚动行为
在单页面应用中,路由切换时可能需要控制页面的滚动位置。Vue Router 提供了 scrollBehavior
选项来实现路由的滚动行为。
13.4.1 基本滚动行为
javascript
const router = new VueRouter({
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
// 如果有保存的滚动位置,恢复到该位置
return savedPosition;
} else {
// 否则滚动到页面顶部
return { x: 0, y: 0 };
}
}
});
在上述代码中,scrollBehavior
函数接收三个参数:to
表示即将进入的路由,from
表示当前离开的路由,savedPosition
表示保存的滚动位置。如果有保存的滚动位置,返回该位置;否则返回 { x: 0, y: 0 }
,即滚动到页面顶部。
13.4.2 滚动到锚点
可以通过路由的 hash
值来实现滚动到页面的锚点位置。例如:
javascript
const router = new VueRouter({
routes,
scrollBehavior(to, from, savedPosition) {
if (to.hash) {
// 滚动到锚点位置
return {
selector: to.hash
};
} else if (savedPosition) {
return savedPosition;
} else {
return { x: 0, y: 0 };
}
}
});
在上述代码中,如果路由的 hash
值存在,返回一个包含 selector
属性的对象,用于指定滚动到的锚点位置。