vue-router原理及其核心功能实现

9,283 阅读10分钟

路由是什么?

自从网站,web等诞生开始,路由就一直存在;在前后端分离之前,一般提到的路由都是后端路由;路由通过一个请求,然后分发到指定的路径,匹配对应的处理程序;它的作用就是分发请求,把对应的请求分发到对应的位置

前端路由与后端路由

后端路由

后端路由可以理解为服务器将浏览器请求的url解析之后映射成对应的函数,这个函数会根据资源类型的不同进行不同的操作,如果是静态资源,那么就进行文件读取,如果是动态数据,那么就会通过数据库进行一些增删查改的操作

后端路由的优点是利于SEO且安全性较高;缺点就是代码耦合度高,加大了服务器压力,且http请求受限于网络环境,影响用户体验

前端路由

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

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

前端路由的优点就是组件切换不需要发送http请求,切换跳转快,用户体验好;缺点就是没有合理的利用缓存且不利于SEO

前端路由模式

hash模式

hash模式是vue-router的默认路由模式,它的标志是在域名之后带有一个#

http://localhost:8888/#/home

通过window.location.hash获取到当前url的hash;hash模式下通过hashchange方法可以监听url中hash的变化

window.addEventListener("hashchange", function(){}, false)

hash模式的特点是兼容性更好,并且hash的变化会在浏览器的history中增加一条记录,可以实现浏览器的前进和后退功能;

缺点由于多了一个#,所以url整体上不够美观

history模式

history模式是另一种前端路由模式,它基于HTML5的history对象

通过location.pathname获取到当前url的路由地址;history模式下,通过pushStatereplaceState方法可以修改url地址,结合popstate方法监听url中路由的变化

history模式的特点是实现更加方便,可读性更强,同时因为没有了#,url也更加美观;

它的劣势也比较明显,当用户刷新或直接输入地址时会向服务器发送一个请求,所以history模式需要服务端同学进行支持,将路由都重定向到根路由

vue-router工作流程

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

Vue插件基础知识

Vue.use()

Vue.use()方法用于插件安装,通过它可以将一些功能或API入侵到Vue内部;

它接收一个参数,如果这个参数有install方法,那么Vue.use()会执行这个install方法,如果接收到的参数是一个函数,那么这个函数会作为install方法被执行

install方法在执行的时候也会接收到一个参数,这个参数就是当前Vue的实例

通过接收到的Vue实例,可以定义一些全局方法或属性,也可以通过prototype对Vue的实例方法进行扩展

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

通过前面的前置知识,已经对路由有了一些了解,接下来就开始实现一个routerJs

先来看一下vue-router的使用方法,然后再基于此进行一步一步的拆解分析

<!-- index.js -->
import vueRouter from './router'
import App from 'app.vue'
Vue.use(vueRouter)

const router = new vueRouter({
    routes: []
})

new Vue({
    router,
    render: h => h(App)
})

在上面的使用示例中可以看出,通过Vue.use()方法将vueRouter安装为插件;通过插件的安装即可以在全局使用vueRouter的方法及相关组件;

install方法

首先需要先实现install方法,通过install向全局注入vueRouter

<!-- router.js -->
class vueRouter {
    constructor(){}
}
vueRouter.install = function(Vue) {
    Vue.mixin({
        beforeCreate(){
            // $options.router存在则表示是根组件
            if (this.$options && this.$options.router) {
                this._root = this
                this._router = this.$options.router
                Vue.util.defineReactive(this, 'current', this._router.history)
            } else {
                // 不是根组件则从父组件中获取
                this._root = this.$parent._root
            }
            
            // 使用$router代理对this._root._router的访问
            Object.defineProperty(this, '$router', {
                get() {
                    return this._root._router
                }
             })
        }
    })
}

install方法接收一个Vue实例作为参数,通过Vue.mixin()全局混入beforeCreated生命周期钩子;通过Vue实例暴露的工具方法defineReactivecurrent属性变成一个监视者

为了避免在使用过程中对_router的修改,所以通过Object.defineProperty设置一个只读属性$router,并使用它代理对this._root._router的访问

router初始化

vue-router在初始化的时候需要通过new操作符,所以需要提供一个vueRouter类并暴露给外部使用;同时还需要一个history类来保存当前的路由路径

class HistoryRoute {
    constructor() {
        this.current = null
    }
}
class vueRouter {
    // options 为初始化时的参数
    constructor(options) {
        this.mode = options.mode || 'hash'
        this.routes = options.routes || []
        this.history = new HistoryRoute
        this.init()
    }
    init() {
        if (this.mode === 'hash') {
            // 初始化一个#
            location.hash ? '' : location.hash = '/'
            // 页面加载完成获取当前路由
            window.addEventListener('load', () => {
                this.history.current = location.hash.slice(1)
            })
            window.addEventListener('hashchange', () => {
                this.history.current = location.hash.slice(1)
            })
        } else {
            window.addEventListener('load', () => {
                this.history.current = location.pathname
            })
            window.addEventListener('popstate', () => {
                this.history.current = location.pathname
            })
        }
    }
}
export default vueRouter

在上面的代码中,创建一个HistoryRoute类,HistoryRoutecurrent属性储存当前路由,在install方法会让这个值实现可响应并监视它的变化,并根据它的变化渲染不同的组件

vueRouter在实例化时接收到一个options对象作为初始化的参数,options中指定了路由模式(mode)和路由表(routes);如果options中没有指定moderoutes,则mode默认为hash模式,routes默认为[]

init方法会根据不同的路由模式在页面加载完成后设置current,同时还会为路由的变化添加事件监听,确保及时更新current属性然后渲染组件

router-view组件与路由渲染

router-view组件的实现依赖于Vue.component()方法,通过这个方法向全局注册一个组件,需要注意的是Vue的全局组件注册需要在Vue实例化之前进行;

Vue.component方法接收两个参数,第一个是组件的名称,另一个是组件的选项对象;

router-view组件的作用是根据路由的变化渲染出路由所对应的组件,所以在注册时候主要是使用到选项对象中的render函数

Vue.component('router-view', {
    render(h) {
          
    }
})

接下来就需要实现router-view组件最重要的功能,如何找到需要渲染的组件?

可以知道的是,当路由变化的时候可以获取到最新的路由地址,同时也可以访问到routes(路由表)的数据

所以只需要根据路由地址从路由表中拿到相应的组件然后交给render函数执行就可以了

根据路由从路由表中获取组件有两种方式:

一种是路由每次变化的时候都是用find方法从路由表中查询一次,获取到路由对象,这种方式虽然可行,但是每次路有变化都去查询一次性能消耗太大;

另一种方式则是将路由与它所对应的组件以键值对的方式进行储存,路由变化的时候只需要根据路由地址进行查询即可;这种方式只需要遍历一次,路由变化时直接使用键值对的方式获取组件,能够非常有效的提高渲染速度

class vueRouter {
    constructor(options) {
        // 省略其他代码
        this.routes = options.routes || []
        this.routeMap = this.createMap(this.routes)
    }
    // 省略其他代码
    createMap(routes) {
        return routes.reduce((memo, current) => {
            memo[current.path] = current.component
            return memo
        }, {})
    }
}

至此,所有的路由都已经使用键值对的方式存入routeMap中,接下来就可以使用render函数进行组件渲染了

vueRouter.install = function(_Vue) {
    // 省略其他代码
    Vue.component('router-view', {
        render(h) {
            let current = this._self._root._router.history.current  // 当前路由
            let routerMap = this._self._root._router.routeMap
            return h(routerMap[current])
        }
    })
}  

到这里,router-view组件就封装完成了,它可以在任何一个组件中使用,并根据路由的变化而渲染不同的组件

push方法和replace方法

vue-router中,无论是声明式的路由跳转还是编程式的路由跳转,都需要通过这两个方法参与来完成;

history模式下, 路由切换通过window.history.pushState方法完成;在hash模式下,路由的切换是直接通过hash值的变化来实现

pushState

pushState是H5引入的新方法,主要用于添加历史记录条目;

它接收三个参数,分别是状态对象标题URL;由于第二个参数(标题)在部分浏览器上会被忽略,所以在这里主要了解一下状态对象和URL

stateObj 状态对象,它会与历史记录条目相关联;popstate事件触发时,状态对象会传入回调函数;浏览器会将这个状态对象序列化以后保存在本地,重新载入这个页面的时候可以拿到这个对象

URL 新历史记录的URL,必须与当前页面处在同一个域;浏览器的地址栏会显示这个地址

class vueRouter {
    constructor(options) {}
    push(url) {
        if (this.mode === 'hash') {
            location.hash = url
        } else {
            pushState(url)
        }
    }
  replace(url) {
      if (this.mode === 'hash') {
          location.hash = url
      } else {
          pushState(url, true)
      }
  }
}

function pushState(url, replace) {
    const history = window.history
    if (replace) {
        history.replaceState({key: history.state.key}, '', url)
    } else {
        history.pushState({key: Date.now()}, '', url)
    }
}

在上面的代码中,hash模式下直接通过location修改hash值,通过hash值的变化去改变视图组件,另外还封装了一个pushState方法统一负责history模式下的页面跳转,并通过一个replace参数判断使用哪种方式进行跳转;

router-link的实现

router-link也是通过Vue.component()方法注册的一个全局组件

Vue.component('router-link', {
    render(h) {
          
    }
})

router-link接收一些props参数,这里列举几个常用的,全部参数可以查看官方文档

to: 目标路由地址
tag: 渲染的标签
replace: 使用replace方式进行路由跳转,不留下history记录

接下来开始实现一个简单router-link组件

Vue.component('router-link', {
    props: {
        to: {
            type: [Object, String],
            required: true
        },
        tag: {
            type: String,
            default: 'a'
        },
        replace: Boolean
    },
    render(h) {
          let data = {}
          if (this.tag === 'a') {
              data.attrs = {href: this.to}
          } else {
              data.on = {click: () => {
                  if (this.replace) {
                      this._self._root._router.replace(this.to)
                  } else {
                      this._self._root._router.push(this.to)
                  }
              }}
          }
          return h(this.tag, data, this.$slots.default)
    }
})

router-link组件通过参数to设置目标路由,tag参数负责组件在页面上渲染的标签,默认为a标签,replace参数则负责控制在路由跳转时是否使用replace方法

render函数中根据不同的tag进行不同的数据拼接,在改变路由时,默认的a标签可以直接设置href属性,而其他标签则需要监听事件,然后使用router的路由跳转方法进行路由切换

到此就已经实现了vue-router的核心的路由监听,跳转,切换,组件渲染等功能,先到浏览器中看一下效果

routerTest.gif

写在最后

本文从路由的起源开始说起,到前后端路由的区别及优缺点,然后介绍了vue-router的工作流程并实现了vue-router中一些核心的方法及其原理

文章中涉及到的源码已提交到我的Github

End