使用 vue 构建 单页面应用 时,离不开 vue-router 的配合。经过 vue-router, 咱们能够创建 路由和组件(页面)之间的映射关系。当 切换路由 时,将 匹配的组件(页面) 渲染到对应的位置。html
虽然在工做中能够很熟练的使用 vue-router,但在使用过程当中经常会出现一些疑问。好比:vue
vue-router怎么安装? 安装过程当中作了什么?node
vue-router 是怎么工做的? 原理是什么?webpack
hash模式和history模式有什么区别?web
router-view 是怎样渲染成当前路由对应的组件(页面)的?vue-router
嵌套路由是怎么工做的?数组
路由懒加载是怎么工做的?promise
各个导航守卫(钩子函数) 分别在什么状况下会触发?浏览器
针对这些问题,本文会结合一个小例子,一一解答。缓存
咱们先经过一个简单的小例子,来回顾一下 vue-router 的使用。
// html
<div id="app">
<router-view></router-view>
</div>
复制代码
// js
// 安装 vue-router
Vue.use(VueRouter)
// router
var router = new VueRouter({
mode: 'history',
routes: [{
path: '/pageA',
name: 'pageA'
component: { template: '<div>pageA</div>'}
}, {
path: '/pageB',
name: 'pageB'
component: { template: '<div>pageB<router-view></router-view></div>'},
children: [{
path: '/pageC',
name: 'pageC'
component: { template: '<div>pageC</div>' }
}]
}]
})
// vue应用
var vm = new Vue({
el: '#app',
router
})
vm.$router.push('/pageA') // 跳转 pageA
vm.$router.repalce({name: 'pageB'}) // 跳转 pageB
vm.$router.push({path: '/pageB/pageC'}) // 跳转 pageC
复制代码
接下来,会结合上面 示例,对 概述中 提出的问题一一解答。
在使用 vue-router 开发 vue项目 的时候,须要先经过 Vue.use 方法来安装 vue-router插件。
Vue.use 方法的 内部操做 以下:
Vue.use = function (plugin) {
// 判断 plugin 是否已经安装。 若是已安装, 直接返回, 不需安装。
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// additional parameters
const args = toArray(arguments, 1);
args.unshift(this);
// 若是 plugin 是一个对象, 且提供 install 方法
// 执行 plugin 提供的 install 方法安装 plugin
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args);
} else if (typeof plugin === 'function') {
// 若是 plugin 是一个函数, 执行函数,安装 plugin
plugin.apply(null, args);
}
// 缓存 已经安装的 plugin
installedPlugins.push(plugin);
return this
};
复制代码
Vue.use 方法在 安装插件(plugin) 的时候, 会调用 插件(plugin) 的 install 方法。
待安装的插件(plugin), 若是是一个 对象,必须 显示提供install方法; 若是是一个 函数,则 自动做为install方法。
vue-router 提供了 install 方法供 Vue.use 方法使用。 方法详情 以下:
function install (Vue) {
...
Vue.mixin({
// beforeCreate 钩子函数, 每个vue实例建立的时候, 都会执行
beforeCreate () {
// 只有建立 根vue 实例的时候,配置项中才会有 router 选项
if (isDef(this.$options.router)) {
// this -> 通常为根vue实例
this._routerRoot = this;
// _router, 路由实例对象
this._router = this.$options.router;
// 初始化router实例
this._router.init(this);
Vue.util.defineReactive(this, '_route', this._router.history.current);
} else {
// this -> 通常为组件实例
// _routerRoot,含有router的vue实例, 通常为根vue实例
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
}
registerInstance(this, this);
},
// destroyed 钩子函数,每一个vue实例销毁时触发
destroyed () {
registerInstance(this);
}
});
// this.$router, 返回根vue实例的_router属性
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
});
// this.$route, 返回根vue实例的_route属性
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
});
// 全局注册 RouterView 组件
Vue.component('RouterView', View);
// 全局注册 RouterLink 组件
Vue.component('RouterLink', Link);
}
复制代码
安装 过程当中,有一些 关键步骤:
全局 注册 beforeCreate、destroyed 钩子函数。
在建立 vue实例(根vue实例/组件vue实例) 时触发 beforeCreate 钩子函数。
若是是 根vue实例,根据传入的 router实例 定义 _router(路由实例对象) 属性, 而后对 router实例 进行初始化。在 router实例 初始化过程当中,会为 根vue实例 定义 _route(路由信息对象) 属性。 接下来,会将 根vue实例 的 _route 属性设置为 响应式属性。 切换路由 会致使 _route 属性 更新, 而后 触发界面从新渲染。
若是是 组件vue实例,为 组件vue实例 定义 _routerRoot 属性, 指向 根vue实例。
当 切换路由 致使 原路由页面对应的vue实例 须要 销毁 时, 触发 destroyed 钩子函数。
给 Vue.prototype 定义 $router、$route 属性, 设置对应的 getter。
当 vue实例 经过 $router、$route 属性访问 路由实例对象 和 路由信息对象 时, 实际访问的是 根vue实例 的 _router、_route 属性。
全部的 vue实例 访问的 $router($route) 都是 同一个。
注册全局组件: RouterView 、 RouterLink。
在 渲染页面 的过程当中, 若是遇到 router-link、 router-view, 会使用 全局注册 生成的 构造函数。
HTML5 中引入了 window.history.pushState 和 window.history.replaceState 方法, 它们能够分别 添加和修改浏览器的历史记录 而 不须要从新加载页面。
pushState、replaceState 方法需配合 window.onpopstate 使用。咱们能够经过 浏览器的前进、回退按钮 或者 window.history.back、 window.history.go、 window.history.forward 方法, 激活浏览器的某个历史记录。 若是 激活的历史记录 是经过 pushState方法添加 或者 被replaceState方法修改,会触发注册的 popstate 事件。
pushState、replaceState 方法不会触发 popstate 事件。
若是浏览器不支持 pushState、replaceState 方法, 咱们也能够经过 window.location.hash = 'xxx' 或者 window.location.replace 的方式 添加和修改浏览器的历史记录, 而 不须要从新加载页面。
window.location.hash、 window.location.replace 需配合 window.onhashchange 使用。若是 激活的历史记录 是经过 window.location.hash方式添加 或者被 window.locaton.relace方法修改,会触发注册的 onhashchange 事件。
只要 url 中 # 后面的值发生变化, 就会触发 hashchange 事件。
使用 vue-router 进行 单页面应用页面跳转 是基于 上述原理 实现的。
当须要 跳转页面 时,经过 pushState(replaceState、window.location.hash、 window.location.replace) 方法 添加或修改历史记录, 而后 从新渲染页面。 当经过 浏览器的前进、回退按钮 或者 window.history.back、 window.history.go、 window.history.forward、this.$router.back、 this.$router.go、 this.$router.forward 方法 激活某个历史记录时, 触发注册的 popstate(hashchange) 事件, 而后 从新渲染页面。
使用 vue-router 对 单页面应用 进行 页面跳转控制 的流程以下:
安装 vue-router;
使用 VueRouter构造函数 构建 router 实例;
在这个过程当中,主要操做以下:
遍历 routeConfig.routes, 创建 路由path(name) 和 组件(component) 之间的 映射关系;
根据 routeConfig.mode,为 router实例 构建相应的 history属性(mode: hash => HashHistory, mode: history => Html5History)。
使用 router实例 构建 根vue实例, 触发 beforeCreate 钩子函数,对 router实例 进行 初始化;
在 初始化 过程当中, 主要操做以下:
根据当前 url,为 根vue实例 添加 _route(路由信息对象)属性;
为 window 对象注册 popstate(hashchange) 事件;
初始化 完成之后, 将 根vue实例 的 _route(路由信息对象)属性 设置为 响应式属性。只要 更新_route属性,就会 触发界面更新。
首次界面渲染, 若是遇到 router-view 标签,会使用 当前路由对应的组件 进行渲染。
经过 vm.$router.push 或者 vm.$router.replace 进行 页面跳转。
在此过程当中, 会在浏览器中 新增一个历史记录 或者 修改当前历史记录, 而后 更新根vue实例的_route(路由信息对象)属性, 触发 界面更新;
界面从新渲染 的时候, 遇到 router-view 标签, 会使用 新路由对应的组件 进行渲染。
当使用 浏览器的前进、回退按钮 或者 vm.$router.go、 vm.$router.back、 vm.$router.forward 方法 激活某个浏览器历史记录时, 触发注册的 popstatue(hashchange) 事件, 更新根vue实例的_route(路由信息对象)属性, 触发 界面更新;
界面从新渲染 的时候, 遇到 router-view 标签, 会使用 新路由对应的组件 进行渲染。
基本流程图以下:
vue-router 默认使用 hash 模式, 即 切换页面 只修改 location.hash。
hash 模式下, 页面 url 的格式为: protocol://hostname: port/xxx/xxx/#/xxx。
hash 模式, 也是经过 pushState(replaceState) 在浏览器中 新增(修改)历史记录。 当经过 前进或者后退 方式激活某个历史记录时, 触发 popstate 事件。 若是 浏览器不支持pushState,则经过 修改window.location.hash(window.location.replace) 在浏览器中 新增(修改)历史记录。 当经过 前进或者后退 方式激活某个历史记录时, 触发 hashChange 事件。
若是选择 history 模式, 切换页面会修改 location.pathname。
history模式下,页面url的格式为 protocol://hostname: port/xxx/xxx, 更加美观。
history 模式充分利用 pushState 来完成页面跳转。 若是 浏览器不支持pushState、replaceState,则经过 window.location.assign 、 window.location.replace 方式来跳转页面。 每次跳转,都会 从新加载页面。
使用 history 模式须要 服务端支持,要在服务端增长一个 覆盖全部状况的候选资源:若是 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是应用依赖的页面。 不然会出现 404 异常。
http请求 中不会包含 hash 值。
在 vue应用 中, 一个 vue组件(component) 从 template标签 到 最后渲染为实际dom树结构,经历的主要过程以下:
vue组件 对应的 template标签 被解析成一个 vnode节点对象。
在解析过程当中,会根据 vue组件 对应的 options配置项({data, props, methods...}), 经过 Vue.extend 方法生成 vue组件 对应的构造方法 VueComponent。
执行 vue组件 对应的构造方法 VueComponent,生成 vue组件实例,并进行 初始化。
执行 vue组件实例 的 render 方法,将 vue组件 的 template模板 解析为 vnode节点树。
将 vue组件 对应的 vnode节点树 渲染为 实际的dom节点树。
router-view 从 template标签 到 实际的页面,经历的过程和 普通vue组件 基本相同, 稍微的区别是在第一步 - 标签转化为vnode节点。
遇到 router-view标签时,经过执行 全局注册的RouterView 的 render 方法,可转化为 vnode节点, 主要过程以下:
获取 当前路由 所匹配的 组件对应的options配置项({data, props, methods...})。
根据 组件options配置项, 经过 Vue.extend方法生成 vue组件 对应的构造方法 VueComponent。
生成组件对应的 vnode节点。
综上, router-view 从 template标签 到 最后的dom节点树 的过程以下:
注意: 上述流程仅适用于 兄弟路由 中间的切换,不适用于 父子路由切换 和使用 keep-alive。 父子路由切换 和使用 keep-alive 以后的路由切换会稍微 不同。
嵌套路由 的具体使用详见 官网。
嵌套路由 会涉及到将 多个router-view 渲染为对应的 页面(或组件)。 在上面的例子中,路由切换到 /pageB 时,外层router-view 渲染为页面 pageB,内层router-view 渲染为 空;当路由切换到 /pageB/pageC 时, 外层router-view 渲染为页面 pageB,内层router-view 渲染为 pageC。
当咱们经过 new VueRouter(...) 建立 router实例 的时候, 会 遍历routes配置项, 分别创建 route path 和 route record、 route name 和 route record 的映射关系, 即 pathMap、nameMap。 其中, route record 是一个 路由记录对象, 会包含 path、name、components、params、query 等信息。
遍历 routes配置项 的时候, 若是遇到了 嵌套路由, 会继续遍历 route.children, 将 子路由record 和 子路由path、name 的 映射关系 分别添加到 pathMap、nameMap 中。 同时,在 子路由record 中添加 parent 属性,指向 父路由record, 创建 父子路由record 之间的 关联关系。
当咱们切换到 某个路由 时, 会根据 path(或name) 从 pathMap(或nameMap) 中寻找 匹配的路由record,将 匹配的路由record 添加到一个 数组 中。 若是 路由record 的 parent 属性不为 undefined, 那么将 parent record 经过 unshift 的方式添加到 数组 中。 递归 处理 路由record 的 parent 属性,直到属性值为 undefined 为止。 最后咱们会获得一个 路由record列表,列表中会包含 祖先路由record、父路由record、当前路由record。
在上面的示例中,各个路由匹配的 record列表 以下:
'/pageA': [{path: '/pageA', components: ...}]
'/pageB': [{path: '/pageB', components: ...}]
'/pageB/pageC': [
{path: '/pageB', component: ...},
{path: '/pageB/pageC', components: ...},
]
复制代码
路由切换 会触发 页面从新渲染。 页面从新渲染的时候会从 最外层的router-view 开始,而后 逐级处理 页面内部的 router-view。
处理 router-view标签 的时候, 会从当前路由的 record列表 中查找 匹配的record,而后将 record.components 渲染为 实际页面。 vue-router会保证每个 router-view 都能匹配到对应的 record。
当 路由切换到 '/pathA', 匹配到的 路由record 只有 一个。先处理外层 router-view, 渲染页面 pageA。 pageA 页面中没有 router-view,路由切换处理完毕。
当 路由切换到 '/pathB', 匹配到的 路由record 只有 一个。先处理外层 router-view, 渲染页面 pageB。 pageB 中有 router-view 须要处理,可是 record列表 中已经 没有匹配的record,只能作 空处理。
当 路由切换到 '/pathB/pathC', 匹配到的 路由record 有 两个。 先处理外层 router-view, 使用 record列表 中的 第一个record 渲染页面 pageB。 pageB 中有 router-view 须要处理,使用 record列表 中的 第二个record 渲染页面 pageC。pageC 页面中没有 router-view,路由切换处理完毕。
嵌套路由 之间 相互切换 时,父页面组件会更新。 更新时,先执行 父页面组件 对应的 render 方法生成 vnode节点树。 将 vnode节点树 渲染为 dom节点树 以前, 会将 新vnode节点树 和 原vnode节点树 作比较。 因为 新旧vnode节点树没有变化, 因此 父页面不会有任何dom操做。
父路由 切换到 子路由,父页面不变, 将 子页面对应的dom树 添加到 父页面的dom树 中。
子路由 切换到 父路由,父页面不变, 将 子页面对应的dom树 从 父页面的dom树 中删除。
子路由间相互切换,父页面不变, 将 上一个子页面对应的dom树 从 父页面的dom树 中删除,将 下一个子页面对应的dom树 添加到 父页面的dom树 中。
路由懒加载 的具体使用详见 官网。
上面的 示例 中没有用到 路由懒加载,在经过 new VueRouter 构建 router实例 的时候, routes 配置项中的每个 route 中的 component 都是一个 普通对象,构建组件所须要的各个配置项(data、props、methods等) 都已获取。
在实际的 vue单页面应用 中,路由懒加载 被普遍应用。 路由懒加载 的前提是使用 webpack打包源代码。使用 webpack打包源代码 之后,每一个页面 都会被分离成一个 单独的chunk,只有在 vue应用 须要时才会 动态加载(经过动态添加script元素的方法从服务端获取页面js文件,渲染页面)。
使用 路由懒加载 后, 在构建 router实例 的时候, routes 配置项中的每个 route 中的 component 都是一个 function, 用于 动态从服务端加载页面源代码,获取 构建组件所须要的各个配置项(data、props、methods等)。
在 切换路由 的时候, 会先 经过动态添加script元素的方式 从 服务端 加载 页面js文件, 获取 构建页面组件须要的options配置项(data、props、render、methods等)。 而后 激活新路由(pushState 将新路由添加到浏览器历史记录中), 触发 页面更新,使用获取到的 options 配置项构建新路由对应的组件实例,渲染页面。
路由懒加载 具体的工做流程以下:
定义 路由组件, 每个组件都是一个 函数, 用于 动态异步加载组件。
const Foo = () => import(/* webpackChunkName: 'Foo'*/'./Foo')
复制代码
构建 router实例, 使用 pathMap(nameMap) 收集 路由path(路由name) 和 路由record 之间的映射关系。 路由record 中的 组件 是一个 函数,用于 异步获取构建组件须要的配置项(data、methods、render 等)。
当 初次加载页面或路由切换激活某个路由 时, 获取 激活路由匹配的路由record。
因为 路由record 中的 组件 是一个函数, 执行函数, 经过 动态添加script的方式加载组件, 返回一个 promise,状态为 pending。
激活新路由动做中止,等待组件加载完成。 组件加载完成之后, 执行 js代码, 获取 组件对应的配置项(data、methods、render等), 将 步骤4 中的 promise 的状态置为 resolve。 触发 promise 经过 then 方法注册的 onResolve, 将 路由record中的组件更新为配置项对象。 激活新路由动做继续。
经过 pushState(replaceState、go、back、forward) 激活新路由,触发 页面更新, 使用获取到的 options 配置项构建新路由对应的组件实例,渲染页面。
导航守卫 的具体用法详见 官网。
咱们仍是经过文章开始提供的 示例 来讲明 各个路由守卫 在何时触发。
当路由从 '/pageA' 切换到 '/pageB' 时,要经历以下流程:
从 pathMap(nameMap) 中获取 新路由对应的 route record, 构建 新路由对应的路由信息对象 - route。
若是使用了 路由懒加载, route record 中 components 中的组件不是一个 普通对象,而是一个 函数,构建 组件实例 须要的 options配置项(data、methods、props等) 尚未获取到。
此时,新路由尚未被激活,仍是 原路由页面。
触发 pageA 注册的 组件级路由守卫 - beforeRouteLeave。 此时,新路由尚未被激活,仍是 原路由页面。
触发 全局守卫 - beforeEach。 此时,新路由尚未被激活,仍是 原路由页面。
触发 新路由('/pageB') 注册的 路由独享守卫 - beforeEnter。 此时,新路由尚未被激活,仍是 原路由页面。
若是使用了 路由懒加载,会经过 动态添加script元素的方式 从 服务端 加载 页面源文件,获取 新路由页面对应的组件配置项。 此时 新路由 对应的 route record 中 components 中的 组件 更新为一个 普通对象。
此时, 新路由依旧没有被激活,仍是 原路由页面。
触发 pageB 注册的 组件级路由守卫 - beforeRouteEnter。此时, 新路由依旧没有被激活,仍是 原路由页面。
触发 全局守卫 - beforeResolve, 表明 构建异步路由组件所需的 options 已经获取。此时, 新路由依旧没有被激活,仍是 原路由页面。
更新 根vue实例 的 _route 属性(路由信息对象),触发页面异步更新(触发_route的setter)。此时, 新路由依旧没有被激活,仍是 原路由页面。
触发 全局守卫 - afterEach。 此时, 新路由依旧没有被激活,仍是 原路由页面。
激活新路由,即经过 pushState 将 新路由 添加到 浏览器历史记录中。 此时, 仍是 原路由页面。
页面更新开始, 渲染 pageB。
触发 组件pageB 的 beforeCreate、created、beforeMount。
销毁页面pageA,触发 组件pageA 的 beforeDestory、destroyed。
触发 组件pageB的 mounted,pageB 渲染完成。
嵌套路由间的切换会有稍许不一样, 当 '/pageB' 和 '/pageB/pageC' 之间 来回切换时, 流程以下:
等同上面。
若是从 '/pageB/pageC' 切换到 '/pageB', 触发 pageC 注册的 组件级路由守卫 - beforeRouteLeave。
若是从 '/pageB' 切换到 '/pageB/pageC', 不会 触发 pageB 注册的 组件级路由守卫 - beforeRouteLeave。
触发 beforeEach。
触发 组件pageB 注册的 组件级路由守卫 - beforeRouteUpdate。
若是从 '/pageB' 切换到 '/pageB/pageC', 触发 /pageB/pageC 注册的 路由级路由守卫 - beforeEnter。
若是从 '/pageB/pageC' 切换到 '/pageB', 不会触发beforeEnter。
若是使用了 路由懒加载,须要经过 动态添加script元素的方式 从 服务端 加载 页面源文件,获取 新路由页面对应的组件配置项。
若是从 '/pageB' 切换到 '/pageB/pageC', 触发 组件pageC 注册的 组件级路由守卫 - beforeRouterEnter。
若是从 '/pageB/pageC' 切换到 '/pageB', 不会触发beforeRouterEnter。
触发 beforeResolve。
更新 根vue实例 的 _route 属性(路由信息对象),触发页面异步更新(触发_route的setter)。
激活新路由,即经过 pushState 将 新路由 添加到 浏览器历史记录中。
页面更新开始, 渲染新页面。
若是从 '/pageB' 切换到 '/pageB/pageC', 触发 组件pageB 的 beforeUpdate, 触发 组件pageC 的 beforeCreate、created、beforeMount、mounted,再触发 组件pageB 的 updated。
若是从 '/pageB/pageC' 切换到 '/pageB', 销毁 页面pageC,触发 组件pageB 的 beforeUpdate, 触发 组件pageC 的 beforeDestory、destroyed,再触发 组件pageB 的 updated。
当 vue-router 使用 histroy模式 时,若是 浏览器不支持history.pushState, vue-router 会自动变为 hash模式, 使用 window.location.hash = xxx 和 hashchange 实现 单页面应用。
若是为 history模式, 且 router.fallback 设置为 false 时, 若是 浏览器不支持history.pushState, vue-router 不会变为 hash模式。此时,只能经过 location.assign、 location.replace 方法 从新加载页面。
未完待续...