Vue-Router原理实现

255 阅读5分钟

一、前端路由与后端路由

后端路由:

服务器将浏览器请求的url解析之后映射对应的函数,该函数根据资源类型的不同进行不同操作。,若是静态资源,则进行文件的读取,若是动态数据,就会通过数据库进行一些增删查改的操作。

前端路由:

随着前端单页应用(SPA)的兴起,前端页面完全变成了组件化,不同的页面就是不同的组件,页面的切换就是组件的切换;页面切换的时候不需要再通过http请求,直接通过JS解析url地址,然后找到对应的组件进行渲染。

两者区别:

前端路由与后端路由最大的不同就是不需要再经过服务器,直接在浏览器下通过JS解析页面之后就可以拿到相应的页面。

各自的优缺点:

后端路由的优点是利于SEO且安全性较高;缺点就是代码耦合度高,加大了服务器压力,且http请求受限于网络环境,影响用户体验。
前端路由的优点就是组件切换不需要发送http请求,切换跳转快,用户体验好;缺点就是没有合理的利用缓存且不利于SEO

二、单页面前端路由模式

vue-router 在实现单页面前端路由时,提供了三种方式:Hash模式History模式abstract模式,根据mode参数来决定采用哪一种方式.

hash模式

  • vue-router 的默认模式。
  • 使用 URL 的 hash 来模拟一个完整的 URL,当 URL 改变时,页面不会去重新加载。
  • hash(#)是URL 的锚点,代表的是网页中的一个位置,单单改变#后的部分(/#/..),浏览器只会加载相应位置的内容,不会重新加载网页,也就是说 #是用来指导浏览器动作的,对服务器端完全无用,HTTP请求中不包括#;同时每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置;所以说Hash模式通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据。
  • 通过window.location.hash获取到当前url的hash;hash模式下通过hashchange方法可以监听url中hash的变化
window.addEventListener("hashchange", function(){}, false)

hash模式优缺点

hash模式的特点是兼容性更好,并且hash的变化会在浏览器的history中增加一条记录,可以实现浏览器的前进和后退功能;缺点由于多了一个#,所以url整体上不够美观。

history模式

  • history模式是另一种前端路由模式,它基于HTML5history对象
  • 通过location.pathname获取到当前url的路由地址;history模式下,通过pushStatereplaceState方法可以修改url地址,结合popstate方法监听url中路由的变化

history模式优缺点

  • history模式的特点是实现更加方便,可读性更强,同时因为没有了#url也更加美观。
  • 这种模式要玩好,还需要后台配置支持。如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是app 依赖的页面(即将路由都重定向到根路由)。

abstract模式

  • abstract模式是使用一个不依赖于浏览器的浏览历史虚拟管理后端。
  • 根据平台差异可以看出,在 Weex 环境中只支持使用 abstract 模式。 不过,vue-router 自身会对环境做校验,如果发现没有浏览器的 APIvue-router 会自动强制进入 abstract 模式,所以 在使用 vue-router 时只要不写 mode 配置即可,默认会在浏览器环境中使用 hash 模式,在移动端原生环境中使用 abstract 模式。 (当然,你也可以明确指定在所有情况下都使用 abstract 模式)

三、vue-router工作流程

  1. url改变
  2. 触发事件监听
  3. 改变vue-router中的current变量
  4. 监视current变量的监视者
  5. 获取新的组件
  6. render

Vue.use()

  • Vue.use()方法用于插件安装,通过它可以将一些功能或API入侵到Vue内部;
  • 它接收一个参数,如果这个参数有install方法,那么Vue.use()会执行这个install方法,如果接收到的参数是一个函数,那么这个函数会作为install方法被执行
  • install方法在执行的时候也会接收到一个参数,这个参数就是当前Vue的实例
  • 通过接收到的Vue实例,可以定义一些全局方法或属性,也可以通过prototypeVue的实例方法进行扩展
class vueRouter {
    constructor(){
    }
}
vueRouter.install = function(Vue) {
    
}

Vue.mixin()

Vue.mixin()方法用于注册全局混入,它接收一个对象作为参数,我们将这个对象称为混入对象;混入对象可以包含组件的任意选项;通过混入对象定义的属性和方法在每一个组件中都可以访问到.

<!-- router.js -->
class vueRouter {
    constructor(){
    }
}
vueRouter.install = function(Vue) {
    Vue.mixin({
        data(){
            return {
                name: '阿白smile'
            }
        }
    })
}

<!-- home.vue -->
// 省略代码
<script>
    export default {
        created(){
            console.log(name)   // '阿白smile'
        }
    }
</script>

四、实现一个routerJs

  1. router.js
// 1. 注册路由插件
Vue.use(VueRouter)
// 2. 路由规则
...
// 3. 创建 router 对象
const router = new VueRouter({
  mode: 'hash',
  //   mode: 'history',
  routes
})
// 导出router 对象
export default router
  1. 编写vuerouter.js
let _Vue = null
export default class VueRouter {
  // 实现install方法,通过install向全局注入vueRouter
  static install (Vue) {
    // 1、判断当前插件是否已经被安装
    if (VueRouter.install.installed) {
      return
    }
    VueRouter.install.installed = true
    // 2、把 Vue 构造函数记录到全局变量
    _Vue = Vue
    // 3、把创建 Vue 实例的时候传入的 router 对象注入 Vue 实例上
    // 混入
    _Vue.mixin({
      beforeCreate () {
        if (this.$options.router) {
          _Vue.prototype.$router = this.$options.router
          this.$options.router.init()
        }
      }
    })
  }

  constructor (options) {
    this.options = options
    this.mode = options.mode || 'hash'
    this.routeMap = {}
    this.data = _Vue.observable({
      current: '/'
    })
  }

  init () {
    this.createRouteMap()
    this.initComponents(_Vue)
    this.initCurrent()
    this.initEvents()
  }

  createRouteMap () {
    // 遍历所有的路由规则,把路由规则解析成键值对的形式,储存到 routeMap 中
    this.options.routes.forEach(route => {
      this.routeMap[route.path] = route.component
    })
  }

  initComponents (Vue) {
    const Link = {
      name: 'RouterLink',
      props: {
        to: {
          type: String,
          required: true
        }
      },
      methods: {}
    }
    if (this.mode === 'history') {
      Link.render = function (h) {
        return h('a', {
          attrs: {
            href: this.to
          },
          on: {
            click: this.clickHandler
          }
        }, this.$slots.default)
      }
      Link.methods.clickHandler = function (e) {
        e.preventDefault()
        history.pushState({}, '', this.to)
        this.$router.data.current = this.to
      }
    } else {
      Link.render = function (h) {
        return h('a', {
          attrs: {
            href: '/#' + this.to
          }
        }, this.$slots.default)
      }
    }

    const View = {
      render: h => h(this.routeMap[this.data.current])
    }

    Vue.component('router-link', Link)
    Vue.component('router-view', View)
  }

  // 第一次加载页面初始化
  initCurrent () {
    // history模式
    if (this.mode === 'history') {
      this.data.current = location.pathname
      return
    }
    // hash模式
    if (location.hash === '') {
      window.location.hash = '#/'
    }
    this.data.current = location.hash.slice(1)
  }

  initEvents () {
    // 监听hash改变
    window.addEventListener('hashchange', () => {
      this.data.current = location.hash.slice(1)
    })
    // 监听浏览器的的历史记录  当历史记录条目更改时,将触发popstate事件
    window.addEventListener('popstate', () => {
      this.data.current = window.location.pathname
    })
  }
}

附录:vue-router 源码图解

image.png