写路由相关的代码的时候,写完页面组件的相关的代码后,就可以直接一把梭哈 vue-router,但是 vue-router 是怎么实现它们间的切换的呢?
不知道大家有没有写过 JSP,我曾经短暂的写过一个星期 JSP, 过程让我有点煎熬,JSP 的工程化没有现在的我们使用的 webpack 那么好,就比如热更新这一开,JSP 的更新是需要重启整个 SpringBoot! 很难受,但可以说一下的是 JSP 实现页面切换的功能其实写起来和 vue-router 也是差不多的
<!-- 有点命名视图的感觉了 -->
<jsp:include page="head.jsp"></jsp:include>
<html></html>
<jsp:include page="bottom.jsp"></jsp:include>
<router-view name="head"></router-view>
<router-view></router-view>
<router-view name="bottom"></router-view>
JSP 的页面切换就像你看得一样,点击后返回目录中的 .jsp 文件,那么 vue-router 是怎么去做的呢?
<router-link>
<router-link>
其实没什么好讲的,源码小 300 行,让我们浅尝一下源码,router/src/RouterLink.ts 184
export const RouterLinkImpl = /*#__PURE__*/ defineComponent({
// ...
setup(props, { slots }) {
const link = reactive(useLink(props));
const { options } = inject(routerKey)!;
// 样式计算
// ...
return () => {
const children = slots.default && slots.default(link);
return props.custom
? children
: h(
"a",
{
"aria-current": link.isExactActive
? props.ariaCurrentValue
: null,
href: link.href,
// this would override user added attrs but Vue will still add
// the listener so we end up triggering both
onClick: link.navigate,
class: elClass.value,
},
children
);
};
},
});
简单来说就是返回一个 <a>
, 里面包含你要跳转的路由(即 URL),说过题外话,像这种库一般都是用 render
函数去渲染组件的(就是上面那个 h()
),而不是像我们平时用的模板语法 <template>
, 什么叫函数式组件?这就叫函数式组件
注意 <router-link>
有个小细节,看上面那个 onClick 就是拦截 <a>
原有的跳转事件,为什么这么做?因为 vue-router4 采用的是 history 模式,不同于 hash 模式 URL 前面挂个 #
就随便你怎么改都不会发送到服务器,history 模式就需要把跳转给拦截不然就跳到 404 了,下面有个例子
https://www.baidu.com/xxx // 这个访问会报错
https://www.baidu.com/#xxx // 这个就不会
再说一个小细节,vue-router4 是默认拦截的,因为 vue-router4 的 hash 模式不是正经 hash, 其底层也是用的 history 模式
<router-view>
注意注意 <router-view>
才是页面切换的敲门砖,但 <router-view>
的源码比 <router-link>
要少,有 231 行,不过核心都是一样的,router/src/RouterView.ts 43(我们还是挑最核心的部分来看)
export const RouterViewImpl = /*#__PURE__*/ defineComponent({
setup(props, { attrs, slots }) {
const injectedRoute = inject(routerViewLocationKey)!
const routeToDisplay = computed(() => props.route || injectedRoute.value)
const depth = inject(viewDepthKey, 0)
// 获得匹配路由
const matchedRouteRef = computed<RouteLocationMatched | undefined>(
() => routeToDisplay.value.matched[depth]
)
// 传递给子 router-view 它自己的路由层级
provide(viewDepthKey, depth + 1)
provide(matchedRouteKey, matchedRouteRef)
provide(routerViewLocationKey, routeToDisplay)
return () => {
const route = routeToDisplay.value
const matchedRoute = matchedRouteRef.value
const ViewComponent = matchedRoute && matchedRoute.components[props.name]
const currentName = props.name
if (!ViewComponent) {
return normalizeSlot(slots.default, { Component: ViewComponent, route })
}
const component = h(
ViewComponent,
assign({}, routeProps, attrs, {
onVnodeUnmounted,
ref: viewRef,
})
)
}
},
})
小细节小细节,<router-view>
深度迎合套娃性质,制定了一套 provide/inject 的父子 <router-view>
传递信息规则,通过父 <router-view>
传递下来的信息,使用 vue-router4 的 matcher 去找到正确的路由组件,没错,所谓的页面切换就是找组件的一个过程,对于一个个分散开来的 vue 组件,通过 matched 去找到组件,没错今天的主角就是 matcher!
matcher
matcher 其实是 vue-router 里面的核心,它有一个专门的文件夹 router/src/matcher,通过这个 matcher, vue-router 将从我们每一次路由导航的点击当中找到真正的路由组件
来到 router/src/router.ts 356
export function createRouter(options: RouterOptions): Router {
const matcher = createRouterMatcher(options.routes, options)
...
}
再配合一点官方文档的小知识
const Home = { template: "<div>Home</div>" };
const About = { template: "<div>About</div>" };
// options.routes 就是这里的 routes
const routes = [
{ path: "/", component: Home },
{ path: "/about", component: About },
];
const router = VueRouter.createRouter({
history: VueRouter.createWebHashHistory(),
routes,
});
createRouterMatcher() 的目的是为了创建我们定义的路由 routes
对应的映射表,而 <router-view>
就是通过这个映射表找到相应的组件并渲染
总结
vue-router4 页面切换
- Matcher 构建映射表
<router-link>
拦截<a>
跳转, 传递信息到 history 监听器<router-link>
根据 provide/inject 的信息, 找到映射表中的组件并渲染
这是写的关于 vue-router 的第三篇文章,基本上 vue-router4 的核心源码都看完了,但感觉家人们好像不是很喜欢看源码一类的文章,其实像 vue-router 这种我们写 vue 项目时几乎是必然要用的库,有机会还是可以去看一下的,源码并没有我一开想象的难,尤其 vue-router4 是用 typescript 写的,相较于传统的 JS 写的库,git clone
下来一堆报错,而 vue-router4 就不会,而且 vs-code 还提供了对 typescirpt
的分析功能,很方便就能够找到 vue-router4 中的某个方法的定义,某个变量的声明(当然 vs-code 做得还不够好,改天我会用 webstorm 去看一下),所以看源码难的是开始的决心