js
<template>
<div>
<router-link to="/">Go to Home</router-link>
<router-link to="/about">Go to About</router-link>
</div>
<router-view></router-view>
</template>
js
import { createRouter, createWebHistory } from 'vue-router'
import Main from './views/Main.vue'
import My from './views/My.vue'
export const routerHistory = createWebHistory()
export const router = createRouter({
history: routerHistory,
strict: true,
routes: [
{
path: '/',
component: Main,
},
{ path: '/about', component: My },
],
})
上面这段代码是 Vue Router 的最基本使用方式,上一篇介绍过,router-view
和router-link
组件能在全局使用,是因为在Vue Router 初始化过程中,全局根组件app,已经注册了:
js
app.component('RouterLink', RouterLink)
app.component('RouterView', RouterView)
每当用户切换路由,router-view
总是能知道自己渲染的是哪个组件。其实思路比较简单: 拿到当前的route信息,每个route信息都会包含component组件信息,渲染这个component组件就行。
具体逻辑:
方法一:
一:在源码中:createRouter
方法内部维护了一个 currentRoute
浅层响应式属性
js
const currentRoute = shallowRef<RouteLocationNormalizedLoaded>(
START_LOCATION_NORMALIZED
)
并给出初始值
js
export const START_LOCATION_NORMALIZED: RouteLocationNormalizedLoaded = {
path: '/',
name: undefined,
params: {},
query: {},
hash: '',
fullPath: '/',
matched: [],
meta: {},
redirectedFrom: undefined,
}
一般情况下,我们都会以 路径: "/" 来表示首页
二:在router 初始化(install)过程中,跟组件app会将 currentRoute 信息 透传到 子孙组件
js
app.provide(routerViewLocationKey, currentRoute)
接下来 来看router-view 组件
js
export const RouterViewImpl = defineComponent({
name: 'RouterView',
inheritAttrs: false,
props: {
/** 代码 省略*/
},
setup(props, { attrs, slots }) {
/** 代码 省略*/
},
})
这么一看,这不就是咱们在业务中的组件写法么。
三: 来到setup方法中,获取跟组件app提供的currentRoute 信息,将injectedRoute包装成计算属性,这样每次根组件app注入进来的injectedRoute发生变化时,router-view就能获取到最新值。
js
const injectedRoute = inject(routerViewLocationKey)!
const routeToDisplay = computed<RouteLocationNormalizedLoaded>(
() => props.route || injectedRoute.value
)
切换路由时,打印的相关路由信息
四:接下来要获取matched 数组内容,这里面才有route对应的要渲染的组件信息
js
const matchedRouteRef = computed<RouteLocationMatched | undefined>(
() => routeToDisplay.value.matched[depth.value]
)
这里有一个depth属性,这里先暂不介绍,放到后面再说,你可以默认为0。这样就能拿到匹配的组件信息,matchedRouteRef,打印下看看都有啥
可以看到有路由信息:path、meta、redirect等,也有一些组件专属的路由钩子函数,这些钩子函数只会负责该组件,全局的路由钩子跟这个负责的对象不一样,不能搞混。
这样就拿到components属性了,就可以渲染组件了。
js
const currentName = props.name
// 获取匹配到的路由信息
const matchedRoute = matchedRouteRef.value
// 获取要渲染的组件
const ViewComponent =
matchedRoute && matchedRoute.components![currentName]
// 如果没找到对应的name,构建一个默认插槽
if (!ViewComponent) {
return normalizeSlot(slots.default, { Component: ViewComponent, route })
}
对于每个router-view都有一个默认的name属性,默认值:default。
js
props: {
name: {
type: String as PropType<string>,
default: 'default',
},
route: Object as PropType<RouteLocationNormalizedLoaded>,
},
五:如果你在页面中,搞了多个router-view,这个属性的关键作用就是区分是哪个router-view。详见官网:(命名视图 | Vue Router (vuejs.org))
这里便于理解,默认就是default
当没有找到对应name 的 router-view, 就会给一个默认的插槽组件,插槽内容就是router-view 包裹的子组件,并将 当前的路由信息route 当作插槽属性抛出去。
js
<router-view v-slot="{ Component, route }">
<component :is="Component" ref="mainContent" />
</router-view>
六:正常情况下,都是有ViewComponent的。接下来解析路由参数,路由参数支持3种写法:
- 布尔模式
- 对象模式
- 函数模式
分别对应下面代码:
js
const routes = [{ path: '/user/:id', component: User, props: true }]
js
const routes = [ { path: '/user/:id', components: { default: User, sidebar: Sidebar }, props: { default: true, sidebar: false } } ]
js
const routes = [ { path: '/search', component: SearchUser, props: route => ({ query: route.query.q }) } ]
源码中处理路由参数:
js
const routePropsOption = matchedRoute.props[currentName]
const routeProps = routePropsOption
? routePropsOption === true
? route.params
: typeof routePropsOption === 'function'
? routePropsOption(route)
: routePropsOption
: null
- 如果是布尔模式,直接把params参数当作路由参数
- 如果是函数,调用函数
- 如果是对象,直接用对象
七:处理组件生命周期钩子函数:
js
const onVnodeUnmounted: VNodeProps['onVnodeUnmounted'] = vnode => {
// vnode中挂在的组件被销毁后,这里重置路由上的组件实例
if (vnode.component!.isUnmounted) {
matchedRoute.instances[currentName] = null
}
}
八:包装组件和属性:
js
const component = h(
ViewComponent,
assign({}, routeProps, attrs, {
onVnodeUnmounted,
ref: viewRef,
})
)
九:渲染组件
js
return (
normalizeSlot(slots.default, { Component: component, route }) ||
component
)
这里也是把component 和 route 抛出去
方法二
router-view上暴露出 route属性,你可以直接修改route属性
js
<template>
<div>
<router-link to="/">Go to Home</router-link>
<router-link to="/about">Go to About</router-link>
</div>
<router-view :route="currentRoute"></router-view>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
import { useRoute } from 'vue-router'
export default defineComponent({
name: 'App',
setup() {
const currentRoute = useRoute()
return {
currentRoute
}
},
})
</script>
回到源码中,当用户手动为router-view设置route,就以用户传入的为主:
js
const routeToDisplay = computed<RouteLocationNormalizedLoaded>(
() => props.route || injectedRoute.value
)
其他后续逻辑都是一样的。
关于depth属性
这里涉及到深度优先遍历,找到有components那一项配置,渲染这个组件。
业务中可能会有下面的代码:
js
export const router = createRouter({
history: routerHistory,
strict: true,
routes: [
{
path: '/',
redirect: '/children',
children: [
{
path: '/children',
redirect: '/v1',
children: [
{
path: '/v1',
component: Main,
},
],
},
],
},
{ path: '/about', component: My },
],
})
一级路由 甚至二三级路由都没有明确指定component,直到深层次的子组件才有component。拿这个例子来说, 此时depth 为2,matchedRouteRef匹配到的就是 path 为 "/v1" 这个对应的组件。
源码中匹配规则:
js
const depth = computed<number>(() => {
let initialDepth = unref(injectedDepth)
const { matched } = routeToDisplay.value
let matchedRoute: RouteLocationMatched | undefined
while (
(matchedRoute = matched[initialDepth]) &&
!matchedRoute.components
) {
initialDepth++
}
return initialDepth
})
总结
- router-view 会从props中获取当前匹配到的route,如果用户没有明确指定route,就会去根组件app中拿到匹配的路由
- 解析路由,获取对应的component,涉及到深度优先遍历。
- 解析路由参数和组件实例, 调用Vue内部h函数,包装成虚拟节点VNode,将其返回。
- 当路由变更时,重新获取对应的component组件,再次渲染。
扩展
说到 useRoute 和 useRouter这两个hooks,实现原理也很简单,由根组件app提供 provide 注入参数,hooks中inject到对应的数据。
js
const router: Router = {
install(app: App) {
const router = this
// router就是这个router大对象
app.provide(routerKey, router)
// currentRoute就是当前路由
app.provide(routerViewLocationKey, currentRoute)
}
}
js
export function useRouter(): Router {
return inject(routerKey)!
}
js
export function useRoute(): RouteLocationNormalizedLoaded {
return inject(routeLocationKey)!
}