前言
从这篇文章开始,我们来简单尝试读一下VueRouter的源码,并且尝试从源码中学习一些编码技巧来丰富自己的开发知识库。
本文不是初级入门文章,不会教你如何使用VueRouter,也不会对着VueRouter的文档当大自然的搬运工,如果你仅仅是想学习如何使用VueRouter的话,还请查阅相应的资料。
VueRouter的仓库地址:github.com/vuejs/route…
我以VueRouter4版本来进行阐述。
在本系列文章中,我们将会以简单的方式来探索,比如带有__DEV__这样的编译宏(其它编译宏类似),在构建之后会被编译成false,因为这种语句是dead code,最终就直接被TreeShaking掉了,它不会影响框架的主流程,所以我们在阅读源码的时候,就可以选择性忽视,从而降低我们的阅读难度。
好了,废话不多说,开始进入正题。
VueRouter的整体运行流程。
在没有阅读源码之前,我对VueRouter的运行流程其实很好奇的,为什么我们在Vue组件里面申明RouterView组件就可以把一个组件渲染出来呢,它又是如何工作的,它是怎么监听路由变化的?RouterView组件是如何跟route实例绑定的呢?正是因为我对这些问题比较好奇,所以驱动我尝试解读VueRouter的源代码。
首先,先看用法,我们先编写一个Demo组件,Demo组件内部一定要有RouterView的占位:
<template>
<div class="home">
<router-view />
</div>
</template>
然后,我们一定是要创建一个VueRouter的实例的:
import { createMemoryHistory, createRouter } from 'vue-router'
import HomeView from './HomeView.vue'
import AboutView from './AboutView.vue'
const routes = [
{ path: '/', component: HomeView },
{ path: '/about', component: AboutView },
]
const router = createRouter({
// 使用HTML5 history模式
history: createMemoryHistory(),
routes,
})
接着,我们需要让其跟Vue主App进行绑定:
const app = createApp(App)
app.use(router)
app.mount('#app')
在这儿,我们引入了一个createMemoryHistory的类,它是在告诉VueRouter用什么模式来解析路由,常见的有HTML5的History模式,也可以用简单原始的Hash模式(后文你可能会对这个模式吃惊的,先留一些悬念,不过本文不会阐述),还有一种Memory模式,是用于SSR模式下,所以一般不怎么用到。
这个History是一个重要的组成部分,主要用于处理浏览器地址栏的变化,在后文有可能我们会简单的带着大家读一下它的实现。
在History类的实现中,还有一个比较重要的组成部分——>Matcher,这是我们不打开源码感受不到的。VueRouter所支持的路由匹配语法都是在Matcher里面实现的。
最后,还有一个我们之前提到的RouterView组件,它其实是一个占位符,它渲染的时候持可以通过Vue的依赖注入(provide/inject)拿到当前Router匹配到的内容,并且把组件渲染到这个位置。
简单给大家总结一下VueRouter的处理流程,我们会根据自己的项目需求创建一个Router实例,然后把这个Router实例跟Vue进行绑定。Router内部可以根据当前浏览器的地址栏的内容匹配到路由元信息,Router在初始化的时候把几个关键的信息通过依赖注入的形式绑定到了Vue,然后RouterView组件在渲染时,会根据当前匹配到的内容将匹配到的路由信息上对应的组件渲染到当前的这个位置。
Router的初始化
在这篇文章,暂时还不带着大家解析VueRouter的生命周期是怎么实现的,我们的首要目标是要知道初始化的完成的工作。
其它的暂时先不管,首先是插件注册的方法,VueRouter做了很多事儿。
第一,注册RouterView组件和RouterLink组件。
第二,向Vue组件挂载全局的实例,$router对象,Vue3的写法和Vue2的写法有很大的不同,大家需要注意一下。
有的同学会问,VueRouter不是暴露有useRouter和useRouteAPI吗?那还挂载$router属性干嘛呢?因为这两个API是考虑到使用Composition API,还有不使用Composition API的时候,这个时候就需要$router属性,况且,肯定还是要跟Vue2的用法大体上要保持一致呀,所以需要挂载这个属性。
第三,然后注册了一个$route,用于表示当前的路由。
第四,使用依赖注入的方式,把router实例暴露出去,后续有重要的用途
第五,使用依赖注入的方式,把当前的路由信息暴露出去,后续用重要的用途
最后,重写Vue的unmount方法,加入自己的销毁逻辑,这是设计模式装饰模式的体现,大家可以体会一下。
在RouterView里面完成了什么事?
在上一小节中,我们已经了解到了VueRouter在注册的时候注册了RouterView这个组件,现在我们可以看看RouterView组件中,完成了一些什么逻辑。
还记得我们之前提到过的VueRouter在初始化的时候第五步完成的事儿吧,这个时候就用到了之前依赖注入的内容了。通过之前依赖注入的内容,我们可以拿到匹配到的路由信息。
因为RouterView组件可以支持嵌套调用,因此需要处理层级的问题,所有还要接着向更深的后代组件传递匹配到的路由信息。
紧接着,有一个比较冗长的watch,暂时下不看,因为它跟生命周期钩子有关系,后文我们会有一篇文章专门来讨论生命周期钩子。
然后,返回的是一个render函数:
先尝试读取当前匹配到的路由对应的组件,如果没有的话,就渲染它的slots上对应的内容,如果读取的到的话,则尝试创建。
这也是为什么RouterView可以支持插槽的原因,我们就可以跟KeepAlive组件,Transition组件配合,从而完成更炫酷的业务。
CompositionAPI->useRoute和useRouter的实现
不用惊讶这个方法的实现为什么这么简单,因为之前我们已经使用依赖注入将关键的路由实例和当前的路由信息挂载了,现在只需要取用即可。
总结
在这篇文章中,我们暂时是一个简单的对VueRouter源码入门,后面的内容会逐渐的变得复杂。
我们可以通过源码看到,在Vue的生态中开发插件时大量使用依赖注入,可以方便我们跨作用域进行的变量读取。
还有一个小技巧没有向大家展示:
首先是使用ES6提供的Symbol类型,防止魔术字符串,避免了潜在的字段冲突;
利用编译宏,控制了Symbol的元信息,不仅在生产模式下减少了代码的体积(积少成多,哈哈哈),还混淆了代码(比如在生产模式下只能拿到一堆的Symbol(''),保护了关键的变量信息)。
还有,比如TypeScript的函数重载:
// 函数签名1
function markAsReady<E = any>(err: E): E
// 函数签名2
function markAsReady<E = any>(): void
// 函数实现
function markAsReady<E = any>(err?: E): E | void {
if (!ready) {
// still not ready if an error happened
ready = !err
setupListeners()
readyHandlers
.list()
.forEach(([resolve, reject]) => (err ? reject(err) : resolve()))
readyHandlers.reset()
}
return err
}
通过函数签名的重载,我们在函数调用的时候,可以惬意的享受类型编程带来的快乐。
考虑到篇幅的关系,本文就仅仅阐述以上内容,在下一篇文章中,我们将开始阐述Router的核心实现,未完待续......