在现在单页应用这么火爆的年代,路由已经成为了我们开发应用必不可少的利器;而纵观各大框架,都会有对应的强大路由支持。Vue.js 因其性能、通用、易用、体积、学习成本低等特点已经成为了广大前端们的新宠,而其对应的路由 vue-router 也是设计的简单好用,功能强大。本文就从源码来分析下 Vue.js 官方路由 vue-router 的整体流程。
本文主要以 vue-router 的 2.0.3 版本来进行分析。
首先来张整体的图:
先对整体有个大概的印象,下边就以官方仓库下 examples/basic
基础例子来一点点具体分析整个流程。
先来看看整体的目录结构:
和流程相关的主要需要关注点的就是 components
、 history
目录以及 create-matcher.js
、 create-route-map.js
、 index.js
、 install.js
。下面就从 basic 应用入口开始来分析 vue-router 的整个流程。
首先看应用入口的代码部分:
上边代码中关键的第 1 步,利用 Vue.js 提供的插件机制 .use(plugin)
来安装 VueRouter
,而这个插件机制则会调用该 plugin
对象的 install
方法(当然如果该 plugin
没有该方法的话会把 plugin
自身作为函数来调用);下边来看下 vue-router 这个插件具体的实现部分。
VueRouter
对象是在 src/index.js
中暴露出来的,这个对象有一个静态的 install
方法:
可以看到这是一个 Vue.js 插件的经典写法,给插件对象增加 install
方法用来安装插件具体逻辑,同时在最后判断下如果是在浏览器环境且存在 window.Vue
的话就会自动使用插件。
install
在这里是一个单独的模块,继续来看同级下的 src/install.js
的主要逻辑:
这里就会有一些疑问了?
插件在打包的时候是肯定不希望把 vue 作为一个依赖包打进去的,但是呢又希望使用 Vue
对象本身的一些方法,此时就可以采用上边类似的做法,在 install
的时候把这个变量赋值 Vue
,这样就可以在其他地方使用 Vue
的一些方法而不必引入 vue 依赖包(前提是保证 install
后才会使用)。
Vue.prototype
定义 $router
、 $route
属性就可以把他们注入到所有组件中吗? 在 Vue.js 中所有的组件都是被扩展的 Vue 实例,也就意味着所有的组件都可以访问到这个实例原型上定义的属性。
beforeCreate mixin
这个在后边创建 Vue 实例的时候再细说。
VueRouter
在入口文件中,首先要实例化一个 VueRouter
,然后将其传入 Vue 实例的 options
中。现在继续来看在 src/index.js
中暴露出来的 VueRouter
类:
里边包含了重要的一步:创建 match
匹配函数。
match
匹配函数 匹配函数是由 src/create-matcher.js
中的 createMatcher
创建的:
具体逻辑后续再具体分析,现在只需要理解为根据传入的 routes
配置生成对应的路由 map,然后直接返回了 match
匹配函数。
继续来看 src/create-route-map.js
中的 createRouteMap
函数:
可以看出主要做的事情就是根据用户路由配置对象生成普通的根据 path
来对应的路由记录以及根据 name
来对应的路由记录的 map,方便后续匹配对应。
这也是很重要的一步,所有的 History
类都是在 src/history/
目录下,现在呢不需要关心具体的每种 History
的具体实现上差异,只需要知道他们都是继承自 src/history/base.js
中的 History
类的:
实例化完了 VueRouter
,下边就该看看 Vue
实例了。
Vue
实例化很简单:
options
中传入了 router
,以及模板;还记得上边没具体分析的 beforeCreate mixin
吗,此时创建一个 Vue 实例,对应的 beforeCreate
钩子就会被调用:
具体来说,首先判断实例化时 options
是否包含 router
,如果包含也就意味着是一个带有路由配置的实例被创建了,此时才有必要继续初始化路由相关逻辑。然后给当前实例赋值 _router
,这样在访问原型上的 $router
的时候就可以得到 router
了。
下边来看里边两个关键: router.init
和 定义响应式的 _route
对象。
然后来看 router
的 init
方法就干了哪些事情,依旧是在 src/index.js
中:
可以看到初始化主要就是给 app
赋值,针对于 HTML5History
和 HashHistory
特殊处理,因为在这两种模式下才有可能存在进入时候的不是默认页,需要根据当前浏览器地址栏里的 path
或者 hash
来激活对应的路由,此时就是通过调用 transitionTo
来达到目的;而且此时还有个注意点是针对于 HashHistory
有特殊处理,为什么不直接在初始化 HashHistory
的时候监听 hashchange
事件呢?这个是为了修复 https://github.com/vuejs/vue-router/issues/725 这个 bug 而这样做的,简要来说就是说如果在 beforeEnter
这样的钩子函数中是异步的话, beforeEnter
钩子就会被触发两次,原因是因为在初始化的时候如果此时的 hash
值不是以 /
开头的话就会补上 #/
,这个过程会触发 hashchange
事件,所以会再走一次生命周期钩子,也就意味着会再次调用 beforeEnter
钩子函数。
来看看这个具体的 transitionTo
方法的大概逻辑,在 src/history/base.js
中:
可以看到整个过程就是执行约定的各种钩子以及处理异步组件问题,这里有一些具体函数具体细节被忽略掉了(后续会具体分析)但是不影响具体理解这个流程。但是需要注意一个概念:路由记录,每一个路由 route
对象都对应有一个 matched
属性,它对应的就是路由记录,他的具体含义在调用 match()
中有处理;通过之前的分析可以知道这个 match
是在 src/create-matcher.js
中的:
路由记录在分析 match
匹配函数那里以及分析过了,这里还需要了解下创建路由对象的 createRoute
,存在于 src/util/route.js
中:
回到之前看的 init
,最后调用了 history.listen
方法:
listen
方法很简单就是设置下当前历史对象的 cb
的值, 在之前分析 transitionTo
的时候已经知道在 history
更新完毕的时候调用下这个 cb
。然后看这里设置的这个函数的作用就是更新下当前应用实例的 _route
的值,更新这个有什么用呢?请看下段落的分析。
继续回到 beforeCreate
钩子函数中,在最后通过 Vue
的工具方法给当前应用实例定义了一个响应式的 _route
属性,值就是获取的 this._router.history.current
,也就是当前 history
实例的当前活动路由对象。给应用实例定义了这么一个响应式的属性值也就意味着如果该属性值发生了变化,就会触发更新机制,继而调用应用实例的 render
重新渲染。还记得上一段结尾留下的疑问,也就是 history
每次更新成功后都会去更新应用实例的 _route
的值,也就意味着一旦 history
发生改变就会触发更新机制调用应用实例的 render
方法进行重新渲染。
回到实例化应用实例的地方:
可以看到这个实例的 template
中包含了两个自定义组件: router-link
和 router-view
。
router-view
组件比较简单,所以这里就先来分析它,他是在源码的 src/components/view.js
中定义的:
可以看到逻辑还是比较简单的,拿到匹配的组件进行渲染就可以了。
再来看看导航链接组件,他在源码的 src/components/link.js
中定义的:
可以看出 router-link
组件就是在其点击的时候根据设置的 to
的值去调用 router
的 push
或者 replace
来更新路由的,同时呢,会检查自身是否和当前路由匹配(严格匹配和包含匹配)来决定自身的 activeClass
是否添加。
整个流程的代码到这里已经分析的差不多了,再来回顾下:
相信整体看完后和最开始的时候看到这张图的感觉是不一样的,且对于 vue-router 的整体的流程了解的比较清楚了。当然由于篇幅有限,这里还有很多细节的地方没有细细分析,后续会根据模块来进行具体的分析。