大家好!路由探索之旅,现在发车!
今天咱们不开车,聊聊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和useRoutehooks 来访问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 应用! 谢谢大家!