vue-router 深刻学习

概述

使用 vue 构建 单页面应用 时,离不开 vue-router 的配合。经过 vue-router, 咱们能够创建 路由和组件(页面)之间的映射关系。当 切换路由 时,将 匹配的组件(页面) 渲染到对应的位置。html

虽然在工做中能够很熟练的使用 vue-router,但在使用过程当中经常会出现一些疑问。好比:vue

  1. vue-router怎么安装? 安装过程当中作了什么?node

  2. vue-router 是怎么工做的? 原理是什么?webpack

  3. hash模式和history模式有什么区别?web

  4. router-view 是怎样渲染成当前路由对应的组件(页面)的?vue-router

  5. 嵌套路由是怎么工做的?数组

  6. 路由懒加载是怎么工做的?promise

  7. 各个导航守卫(钩子函数) 分别在什么状况下会触发?浏览器

针对这些问题,本文会结合一个小例子,一一解答。缓存

示例

咱们先经过一个简单的小例子,来回顾一下 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-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);
}
复制代码

安装 过程当中,有一些 关键步骤

  1. 全局 注册 beforeCreatedestroyed 钩子函数。

    在建立 vue实例(根vue实例/组件vue实例) 时触发 beforeCreate 钩子函数。

    若是是 根vue实例,根据传入的 router实例 定义 _router(路由实例对象) 属性, 而后对 router实例 进行初始化。在 router实例 初始化过程当中,会为 根vue实例 定义 _route(路由信息对象) 属性。 接下来,会将 根vue实例_route 属性设置为 响应式属性切换路由 会致使 _route 属性 更新, 而后 触发界面从新渲染

    若是是 组件vue实例,为 组件vue实例 定义 _routerRoot 属性, 指向 根vue实例

    切换路由 致使 原路由页面对应的vue实例 须要 销毁 时, 触发 destroyed 钩子函数。

  2. Vue.prototype 定义 $router$route 属性, 设置对应的 getter

    vue实例 经过 $router$route 属性访问 路由实例对象路由信息对象 时, 实际访问的是 根vue实例_router_route 属性。

    全部的 vue实例 访问的 $router($route) 都是 同一个

  3. 注册全局组件: RouterViewRouterLink

    渲染页面 的过程当中, 若是遇到 router-linkrouter-view, 会使用 全局注册 生成的 构造函数

vue-router工做原理

HTML5 中引入了 window.history.pushStatewindow.history.replaceState 方法, 它们能够分别 添加和修改浏览器的历史记录不须要从新加载页面

pushStatereplaceState 方法需配合 window.onpopstate 使用。咱们能够经过 浏览器的前进、回退按钮 或者 window.history.backwindow.history.gowindow.history.forward 方法, 激活浏览器的某个历史记录。 若是 激活的历史记录 是经过 pushState方法添加 或者 被replaceState方法修改,会触发注册的 popstate 事件。

pushStatereplaceState 方法不会触发 popstate 事件。

若是浏览器不支持 pushStatereplaceState 方法, 咱们也能够经过 window.location.hash = 'xxx' 或者 window.location.replace 的方式 添加和修改浏览器的历史记录, 而 不须要从新加载页面

window.location.hashwindow.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.backwindow.history.gowindow.history.forwardthis.$router.backthis.$router.gothis.$router.forward 方法 激活某个历史记录时, 触发注册的 popstate(hashchange) 事件, 而后 从新渲染页面

使用 vue-router单页面应用 进行 页面跳转控制 的流程以下:

  1. 安装 vue-router

  2. 使用 VueRouter构造函数 构建 router 实例;

    在这个过程当中,主要操做以下:

    • 遍历 routeConfig.routes, 创建 路由path(name)组件(component) 之间的 映射关系

    • 根据 routeConfig.mode,为 router实例 构建相应的 history属性(mode: hash => HashHistory, mode: history => Html5History)

  3. 使用 router实例 构建 根vue实例, 触发 beforeCreate 钩子函数,对 router实例 进行 初始化;

    初始化 过程当中, 主要操做以下:

    • 根据当前 url,为 根vue实例 添加 _route(路由信息对象)属性

    • window 对象注册 popstate(hashchange) 事件;

    初始化 完成之后, 将 根vue实例_route(路由信息对象)属性 设置为 响应式属性。只要 更新_route属性,就会 触发界面更新

    首次界面渲染, 若是遇到 router-view 标签,会使用 当前路由对应的组件 进行渲染。

  4. 经过 vm.$router.push 或者 vm.$router.replace 进行 页面跳转

    在此过程当中, 会在浏览器中 新增一个历史记录 或者 修改当前历史记录, 而后 更新根vue实例的_route(路由信息对象)属性, 触发 界面更新

    界面从新渲染 的时候, 遇到 router-view 标签, 会使用 新路由对应的组件 进行渲染。

  5. 当使用 浏览器的前进、回退按钮 或者 vm.$router.govm.$router.backvm.$router.forward 方法 激活某个浏览器历史记录时, 触发注册的 popstatue(hashchange) 事件, 更新根vue实例的_route(路由信息对象)属性, 触发 界面更新

    界面从新渲染 的时候, 遇到 router-view 标签, 会使用 新路由对应的组件 进行渲染。

基本流程图以下

hash / history

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.assignwindow.location.replace 方式来跳转页面。 每次跳转,都会 从新加载页面

使用 history 模式须要 服务端支持,要在服务端增长一个 覆盖全部状况的候选资源:若是 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是应用依赖的页面。 不然会出现 404 异常。

http请求 中不会包含 hash 值。

router-view

vue应用 中, 一个 vue组件(component)template标签最后渲染为实际dom树结构,经历的主要过程以下:

  1. vue组件 对应的 template标签 被解析成一个 vnode节点对象

    在解析过程当中,会根据 vue组件 对应的 options配置项({data, props, methods...}), 经过 Vue.extend 方法生成 vue组件 对应的构造方法 VueComponent

  2. 执行 vue组件 对应的构造方法 VueComponent,生成 vue组件实例,并进行 初始化

  3. 执行 vue组件实例render 方法,将 vue组件template模板 解析为 vnode节点树

  4. vue组件 对应的 vnode节点树 渲染为 实际的dom节点树

router-viewtemplate标签实际的页面,经历的过程和 普通vue组件 基本相同, 稍微的区别是在第一步 - 标签转化为vnode节点

遇到 router-view标签时,经过执行 全局注册的RouterViewrender 方法,可转化为 vnode节点, 主要过程以下:

  1. 获取 当前路由 所匹配的 组件对应的options配置项({data, props, methods...})

  2. 根据 组件options配置项, 经过 Vue.extend方法生成 vue组件 对应的构造方法 VueComponent

  3. 生成组件对应的 vnode节点

综上, router-viewtemplate标签最后的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 pathroute recordroute nameroute record 的映射关系, 即 pathMapnameMap。 其中, route record 是一个 路由记录对象, 会包含 pathnamecomponentsparamsquery 等信息。

遍历 routes配置项 的时候, 若是遇到了 嵌套路由, 会继续遍历 route.children, 将 子路由record子路由path、name映射关系 分别添加到 pathMapnameMap 中。 同时,在 子路由record 中添加 parent 属性,指向 父路由record, 创建 父子路由record 之间的 关联关系

当咱们切换到 某个路由 时, 会根据 path(或name)pathMap(或nameMap) 中寻找 匹配的路由record,将 匹配的路由record 添加到一个 数组 中。 若是 路由recordparent 属性不为 undefined, 那么将 parent record 经过 unshift 的方式添加到 数组 中。 递归 处理 路由recordparent 属性,直到属性值为 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, 渲染页面 pageApageA 页面中没有 router-view,路由切换处理完毕。

路由切换到 '/pathB', 匹配到的 路由record 只有 一个。先处理外层 router-view, 渲染页面 pageBpageB 中有 router-view 须要处理,可是 record列表 中已经 没有匹配的record,只能作 空处理

路由切换到 '/pathB/pathC', 匹配到的 路由record两个。 先处理外层 router-view, 使用 record列表 中的 第一个record 渲染页面 pageBpageB 中有 router-view 须要处理,使用 record列表 中的 第二个record 渲染页面 pageCpageC 页面中没有 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 配置项构建新路由对应的组件实例渲染页面

路由懒加载 具体的工做流程以下:

  1. 定义 路由组件, 每个组件都是一个 函数, 用于 动态异步加载组件

    const Foo = () => import(/* webpackChunkName: 'Foo'*/'./Foo')
    复制代码
  2. 构建 router实例, 使用 pathMap(nameMap) 收集 路由path(路由name)路由record 之间的映射关系。 路由record 中的 组件 是一个 函数,用于 异步获取构建组件须要的配置项(data、methods、render 等)

  3. 初次加载页面或路由切换激活某个路由 时, 获取 激活路由匹配的路由record

  4. 因为 路由record 中的 组件 是一个函数, 执行函数, 经过 动态添加script的方式加载组件, 返回一个 promise,状态为 pending

  5. 激活新路由动做中止,等待组件加载完成。 组件加载完成之后, 执行 js代码, 获取 组件对应的配置项(data、methods、render等), 将 步骤4 中的 promise 的状态置为 resolve。 触发 promise 经过 then 方法注册的 onResolve, 将 路由record中的组件更新为配置项对象激活新路由动做继续

  6. 经过 pushState(replaceState、go、back、forward) 激活新路由,触发 页面更新使用获取到的 options 配置项构建新路由对应的组件实例渲染页面

导航守卫

导航守卫 的具体用法详见 官网

咱们仍是经过文章开始提供的 示例 来讲明 各个路由守卫 在何时触发。

当路由从 '/pageA' 切换到 '/pageB' 时,要经历以下流程:

  1. pathMap(nameMap) 中获取 新路由对应的 route record, 构建 新路由对应的路由信息对象 - route

    若是使用了 路由懒加载route recordcomponents 中的组件不是一个 普通对象,而是一个 函数,构建 组件实例 须要的 options配置项(data、methods、props等) 尚未获取到。

    此时,新路由尚未被激活,仍是 原路由页面

  2. 触发 pageA 注册的 组件级路由守卫 - beforeRouteLeave。 此时,新路由尚未被激活,仍是 原路由页面

  3. 触发 全局守卫 - beforeEach。 此时,新路由尚未被激活,仍是 原路由页面

  4. 触发 新路由('/pageB') 注册的 路由独享守卫 - beforeEnter。 此时,新路由尚未被激活,仍是 原路由页面

  5. 若是使用了 路由懒加载,会经过 动态添加script元素的方式服务端 加载 页面源文件,获取 新路由页面对应的组件配置项。 此时 新路由 对应的 route recordcomponents 中的 组件 更新为一个 普通对象

    此时, 新路由依旧没有被激活,仍是 原路由页面

  6. 触发 pageB 注册的 组件级路由守卫 - beforeRouteEnter。此时, 新路由依旧没有被激活,仍是 原路由页面

  7. 触发 全局守卫 - beforeResolve, 表明 构建异步路由组件所需的 options 已经获取。此时, 新路由依旧没有被激活,仍是 原路由页面

  8. 更新 根vue实例_route 属性(路由信息对象)触发页面异步更新(触发_route的setter)。此时, 新路由依旧没有被激活,仍是 原路由页面

  9. 触发 全局守卫 - afterEach。 此时, 新路由依旧没有被激活,仍是 原路由页面

  10. 激活新路由,即经过 pushState新路由 添加到 浏览器历史记录中。 此时, 仍是 原路由页面

  11. 页面更新开始渲染 pageB

    触发 组件pageBbeforeCreatecreatedbeforeMount

  12. 销毁页面pageA,触发 组件pageAbeforeDestorydestroyed

  13. 触发 组件pageBmountedpageB 渲染完成。

嵌套路由间的切换会有稍许不一样, 当 '/pageB''/pageB/pageC' 之间 来回切换时, 流程以下:

  1. 等同上面。

  2. 若是从 '/pageB/pageC' 切换到 '/pageB', 触发 pageC 注册的 组件级路由守卫 - beforeRouteLeave

    若是从 '/pageB' 切换到 '/pageB/pageC', 不会 触发 pageB 注册的 组件级路由守卫 - beforeRouteLeave

  3. 触发 beforeEach

  4. 触发 组件pageB 注册的 组件级路由守卫 - beforeRouteUpdate

  5. 若是从 '/pageB' 切换到 '/pageB/pageC', 触发 /pageB/pageC 注册的 路由级路由守卫 - beforeEnter

    若是从 '/pageB/pageC' 切换到 '/pageB'不会触发beforeEnter

  6. 若是使用了 路由懒加载,须要经过 动态添加script元素的方式服务端 加载 页面源文件,获取 新路由页面对应的组件配置项

  7. 若是从 '/pageB' 切换到 '/pageB/pageC', 触发 组件pageC 注册的 组件级路由守卫 - beforeRouterEnter

    若是从 '/pageB/pageC' 切换到 '/pageB'不会触发beforeRouterEnter

  8. 触发 beforeResolve

  9. 更新 根vue实例_route 属性(路由信息对象)触发页面异步更新(触发_route的setter)

  10. 激活新路由,即经过 pushState新路由 添加到 浏览器历史记录中

  11. 页面更新开始渲染新页面

    若是从 '/pageB' 切换到 '/pageB/pageC', 触发 组件pageBbeforeUpdate, 触发 组件pageCbeforeCreatecreatedbeforeMountmounted,再触发 组件pageBupdated

    若是从 '/pageB/pageC' 切换到 '/pageB', 销毁 页面pageC,触发 组件pageBbeforeUpdate, 触发 组件pageCbeforeDestorydestroyed,再触发 组件pageBupdated

其余

  1. vue-router 使用 histroy模式 时,若是 浏览器不支持history.pushState, vue-router 会自动变为 hash模式, 使用 window.location.hash = xxxhashchange 实现 单页面应用

    若是为 history模式, 且 router.fallback 设置为 false 时, 若是 浏览器不支持history.pushState, vue-router 不会变为 hash模式。此时,只能经过 location.assignlocation.replace 方法 从新加载页面

  未完待续...

相关文章
相关标签/搜索