大家好!路由探索之旅,现在发车!
今天咱们不开车,聊聊Vue Router的两个重要组件:RouterView
和 RouterLink
。 这俩哥们儿,一个负责展示,一个负责跳转,在Vue单页面应用里扮演着举足轻重的角色。 咱们一起扒一扒它们的源码,看看它们是如何跟路由实例眉来眼去的。
一、RouterView:路由的“展示窗口”
想象一下,你家客厅的电视机,RouterView
就相当于这个电视机。 路由配置决定了播放哪个频道(组件),RouterView
就负责把这个频道(组件)的内容渲染出来。
1.1 RouterView 的核心逻辑
RouterView
的主要职责是:
- 响应路由变化: 监听路由实例的
currentRoute
对象,一旦发生变化,就重新渲染。 - 动态渲染组件: 根据
currentRoute
对象中的matched
数组,找到匹配的组件,并进行渲染。 - 处理嵌套路由: 支持多层嵌套的路由,每个
RouterView
负责渲染当前层级的组件。
1.2 RouterView 的源码剖析 (简化版)
为了便于理解,我们来看一个简化版的 RouterView
组件的实现:
// RouterView.js (简化版)
import { h, defineComponent, inject, computed } from 'vue';
import { RouterContext } from './injectionSymbols';
export default defineComponent({
name: 'RouterView',
props: {
name: {
type: String,
default: 'default',
},
},
setup(props, { attrs, slots }) {
const router = inject(RouterContext); // 从父组件注入 router 实例
const route = computed(() => router.currentRoute.value); // 获取响应式的 currentRoute
const depth = inject('routerViewDepth', 0); // 获取当前 RouterView 的嵌套深度
provide('routerViewDepth', depth + 1); // 向子组件提供嵌套深度
const matchedRouteRef = computed(() => {
const matched = route.value.matched;
return matched[depth]; // 获取当前深度的 matched 路由记录
});
return () => {
const matchedRoute = matchedRouteRef.value;
// 如果没有匹配的路由记录,则不渲染任何内容
if (!matchedRoute) {
return slots.default ? slots.default() : null;
}
const component = matchedRoute.components[props.name]; // 获取组件
// 如果没有组件,则不渲染任何内容
if (!component) {
return h('div', 'No component found for ' + props.name);
}
// 传递 props 和 attrs 给组件
const routeProps = matchedRoute.props[props.name];
const propsData = routeProps === true ? route.value.params : (typeof routeProps === 'function' ? routeProps(route.value) : routeProps);
const allProps = {
...propsData,
...attrs,
};
// 渲染组件
return h(component, allProps, slots.default ? slots.default() : null);
};
},
});
代码解释:
inject(RouterContext)
:RouterView
通过inject
从祖先组件 (通常是App
组件) 注入router
实例。 这样RouterView
才能访问路由信息,比如currentRoute
。RouterContext
是一个Symbol
,用于在组件之间传递router
实例。computed(() => router.currentRoute.value)
:currentRoute
是一个响应式对象,包含了当前路由的信息。RouterView
通过computed
创建一个响应式的route
计算属性,以便在路由变化时自动更新。inject('routerViewDepth', 0)
和provide('routerViewDepth', depth + 1)
: 这两个是用来处理嵌套路由的关键。routerViewDepth
记录了RouterView
的嵌套深度。 每个RouterView
都增加深度并传递给子组件。 这样,嵌套的RouterView
就能知道自己应该渲染哪个路由记录。matched[depth]
:currentRoute.matched
是一个数组,包含了从根路由到当前路由的所有匹配的路由记录。depth
决定了RouterView
应该使用哪个路由记录来渲染组件。matchedRoute.components[props.name]
: 每个路由记录都有一个components
对象,包含了该路由对应的组件。props.name
允许我们在一个路由记录中指定多个具名视图。 默认情况下,name
是'default'
。h(component, allProps, slots.default ? slots.default() : null)
: 使用h
函数创建一个 VNode,渲染组件。allProps
包含了从路由记录中获取的 props 和传递给RouterView
的 attributes。slots.default
允许我们向组件传递插槽。
流程图:
步骤 | 描述 | 涉及到的关键变量/函数 |
---|---|---|
1 | 从父组件注入 router 实例。 |
inject(RouterContext) |
2 | 获取响应式的 currentRoute 对象。 |
computed(() => router.currentRoute.value) |
3 | 获取当前 RouterView 的嵌套深度。 |
inject('routerViewDepth', 0) |
4 | 向子组件提供新的嵌套深度。 | provide('routerViewDepth', depth + 1) |
5 | 从 currentRoute.matched 数组中找到当前深度的匹配的路由记录。 |
matched[depth] |
6 | 如果没有匹配的路由记录,则不渲染任何内容。 | |
7 | 从匹配的路由记录中获取要渲染的组件。 | matchedRoute.components[props.name] |
8 | 如果没有组件,则不渲染任何内容。 | |
9 | 从路由记录中获取 props,并与传递给 RouterView 的 attributes 合并。 |
matchedRoute.props[props.name] , attrs |
10 | 使用 h 函数创建一个 VNode,渲染组件。 |
h(component, allProps, slots.default ? slots.default() : null) |
总结:
RouterView
的核心在于响应路由变化,并根据当前路由信息动态渲染对应的组件。 通过 inject
和 provide
实现嵌套路由的支持。
二、RouterLink:路由的“传送门”
RouterLink
就像网页上的链接,点击它可以跳转到不同的页面。 在Vue Router 中,RouterLink
负责生成带有正确 href
属性的 <a>
标签,并阻止默认的链接跳转行为,改用 router.push
或 router.replace
来更新路由。
2.1 RouterLink 的核心逻辑
RouterLink
的主要职责是:
- 生成链接: 根据
to
属性生成正确的href
属性。 - 阻止默认行为: 阻止
<a>
标签的默认跳转行为。 - 触发路由跳转: 调用
router.push
或router.replace
来更新路由。 - 处理激活状态: 根据当前路由是否匹配
to
属性,添加或移除激活 class。
2.2 RouterLink 的源码剖析 (简化版)
// RouterLink.js (简化版)
import { h, defineComponent, inject, computed, onMounted, onBeforeUnmount, ref } from 'vue';
import { RouterContext } from './injectionSymbols';
import { useRoute } from './useRoute';
import { useRouter } from './useRouter';
export default defineComponent({
name: 'RouterLink',
props: {
to: {
type: [String, Object],
required: true,
},
tag: {
type: String,
default: 'a',
},
activeClass: {
type: String,
default: 'router-link-active',
},
exactActiveClass: {
type: String,
default: 'router-link-exact-active',
},
replace: {
type: Boolean,
default: false,
},
ariaCurrentValue: {
type: String,
default: 'page',
},
},
setup(props, { slots }) {
const router = useRouter();
const route = useRoute();
const isActive = ref(false);
const isExactActive = ref(false);
const linkRef = ref(null);
const normalizedTo = computed(() => router.resolve(props.to));
const updateActiveClass = () => {
const currentRoute = route;
isActive.value = normalizedTo.value.href === currentRoute.fullPath;
isExactActive.value = normalizedTo.value.path === currentRoute.path;
};
onMounted(() => {
updateActiveClass();
router.afterEach(updateActiveClass);
});
onBeforeUnmount(() => {
router.afterEach(updateActiveClass);
});
const onClick = (evt) => {
if (props.replace) {
router.replace(props.to);
} else {
router.push(props.to);
}
evt.preventDefault();
};
return () => {
const { tag, activeClass, exactActiveClass, ariaCurrentValue } = props;
const active = isActive.value;
const exactActive = isExactActive.value;
const linkClass = {
[activeClass]: active,
[exactActiveClass]: exactActive,
};
const attrs = {
href: normalizedTo.value.href,
onClick,
};
if (tag === 'a' && ariaCurrentValue && exactActive) {
attrs['aria-current'] = ariaCurrentValue;
}
return h(
tag,
{
...attrs,
class: linkClass,
ref: linkRef,
},
slots.default ? slots.default() : null
);
};
},
});
代码解释:
useRouter()
和useRoute()
:RouterLink
使用useRouter
和useRoute
hooks 来访问router
实例和route
对象。normalizedTo = computed(() => router.resolve(props.to))
:router.resolve(props.to)
将to
属性转换为一个标准化的路由对象,包含了href
、path
、query
等信息。normalizedTo
是一个计算属性,以便在to
属性变化时自动更新。updateActiveClass()
: 这个函数负责更新激活 class。 它比较normalizedTo.href
和currentRoute.fullPath
来判断是否激活,并设置isActive.value
和isExactActive.value
。onMounted(() => { ... })
: 在组件挂载后,调用updateActiveClass()
更新激活 class,并使用router.afterEach(updateActiveClass)
注册一个全局后置钩子,在每次路由切换后都更新激活 class。onClick(evt)
: 这个函数处理点击事件。 它调用router.push
或router.replace
来更新路由,并使用evt.preventDefault()
阻止默认的链接跳转行为。h(tag, { ...attrs, class: linkClass, ref: linkRef }, slots.default ? slots.default() : null)
: 使用h
函数创建一个 VNode,渲染链接。attrs
包含了href
和onClick
等属性。linkClass
包含了激活 class。slots.default
允许我们向链接传递插槽。
流程图:
步骤 | 描述 | 涉及到的关键变量/函数 |
---|---|---|
1 | 使用 useRouter() 和 useRoute() 获取 router 实例和 route 对象。 |
useRouter() , useRoute() |
2 | 使用 router.resolve(props.to) 将 to 属性转换为标准化的路由对象。 |
router.resolve(props.to) |
3 | 创建 updateActiveClass() 函数,用于更新激活 class。 |
|
4 | 在组件挂载后,调用 updateActiveClass() 更新激活 class,并使用 router.afterEach(updateActiveClass) 注册一个全局后置钩子。 |
onMounted() , router.afterEach(updateActiveClass) |
5 | 创建 onClick(evt) 函数,处理点击事件,调用 router.push 或 router.replace 更新路由,并阻止默认的链接跳转行为。 |
onClick(evt) , router.push(props.to) , router.replace(props.to) , evt.preventDefault() |
6 | 使用 h 函数创建一个 VNode,渲染链接,包含 href 属性,onClick 事件,以及激活 class。 |
h(tag, { ...attrs, class: linkClass, ref: linkRef }, slots.default ? slots.default() : null) |
总结:
RouterLink
的核心在于生成链接,阻止默认行为,并触发路由跳转。 它还负责处理激活状态,以便在当前路由匹配 to
属性时添加激活 class。
三、RouterView 和 RouterLink 如何与路由实例交互
现在我们已经了解了 RouterView
和 RouterLink
的基本实现,让我们看看它们是如何与路由实例交互的。
-
RouterView
:- 依赖注入: 通过
inject(RouterContext)
从祖先组件注入router
实例。 - 响应式数据: 通过
computed(() => router.currentRoute.value)
获取响应式的currentRoute
对象。 - 嵌套路由: 通过
inject('routerViewDepth', 0)
和provide('routerViewDepth', depth + 1)
处理嵌套路由。
- 依赖注入: 通过
-
RouterLink
:- 访问路由实例: 使用
useRouter()
hook 来访问router
实例。 - 访问路由信息: 使用
useRoute()
hook 来访问route
对象。 - 生成链接: 使用
router.resolve(props.to)
将to
属性转换为标准化的路由对象。 - 触发跳转: 使用
router.push
或router.replace
来更新路由。 - 监听路由变化: 通过
router.afterEach
注册钩子,检测路由变化。
- 访问路由实例: 使用
交互图:
+-----------------+ inject(RouterContext) +-----------------+
| RouterView | <---------------------------- | Router |
+-----------------+ +-----------------+
| ^
| computed(() => router.currentRoute.value) |
| |
v |
+-----------------+ | router.push() |
| CurrentRoute | | router.replace() |
+-----------------+ v
+-----------------+
| RouterLink |
+-----------------+
^ |
| | useRouter(), useRoute()
| v
+-----------------+
| Route |
+-----------------+
表格对比:
特性 | RouterView | RouterLink |
---|---|---|
主要功能 | 展示组件 | 导航跳转 |
依赖的路由实例 | 通过 inject(RouterContext) 获取 |
通过 useRouter() 获取 |
响应式数据 | computed(() => router.currentRoute.value) |
useRoute() 返回的 route 对象 |
路由跳转 | 无 | router.push 或 router.replace |
嵌套路由 | 支持 | 无需关心嵌套路由,路由实例负责处理 |
激活状态 | 无 | 根据当前路由是否匹配 to 属性添加/移除激活 class |
四、总结与展望
今天我们一起探索了 Vue Router 中 RouterView
和 RouterLink
组件的实现,了解了它们如何与路由实例交互。 RouterView
负责展示组件,RouterLink
负责导航跳转,它们是构建单页面应用的重要基石。
当然,我们今天看的只是简化版的源码,实际的源码要复杂得多。 但是,理解了这些核心概念,就能更好地使用 Vue Router,并能更深入地理解其内部机制。
希望这次旅程能帮助大家更好地掌握 Vue Router,写出更优雅、更高效的 Vue 应用! 谢谢大家!