实现一个简化版的vue-router

本文旨在介绍vue-router的实现思路,并动手实现一个简化版的vue-router。咱们先来看一下通常项目中对vue-router最基本的一个使用,能够看到,这里定义了四个路由组件,咱们只要在根vue实例中注入该router对象就可使用了.javascript

import VueRouter from 'vue-router';
import Home from '@/components/Home';
import A from '@/components/A';
import B from '@/components/B'
import C from '@/components/C'

Vue.use(VueRouter)

export default new VueRouter.Router({
  // mode: 'history',
  routes: [
    {
      path: '/',
      component: Home
    },
    {
      path: '/a',
      component: A
    },
    {
      path: '/b',
      component: B
    },
    {
      path: '/c',
      component: C
    }
  ]
})
复制代码

vue-router提供两个全局组件,router-viewrouter-link,前者是用于路由组件的占位,后者用于点击时跳转到指定路由。此外组件内部能够经过this.$router.push,this.$rouer.replace等api实现路由跳转。本文将实现上述两个全局组件以及pushreplace两个api,调用的时候支持params传参,而且支持hashhistory两种模式,忽略其他api、嵌套路由、异步路由、abstract路由以及导航守卫等高级功能的实现,这样有助于理解vue-router的核心原理。本文的最终代码不建议在生产环境使用,只作一个学习用途,下面咱们就来一步步实现它。vue

install实现

任何一个vue插件都要实现一个install方法,经过Vue.use调用插件的时候就是在调用插件的install方法,那么路由的install要作哪些事情呢?首先咱们知道 咱们会用new关键字生成一个router实例,就像前面的代码实例同样,而后将其挂载到根vue实例上,那么做为一个全局路由,咱们固然须要在各个组件中均可以拿到这个router实例。另外咱们使用了全局组件router-viewrouter-link,因为install会接收到Vue构造函数做为实参,方便咱们调用Vue.component来注册全局组件。所以,在install中主要就作两件事,给各个组件都挂载router实例,以及实现router-viewrouter-link两个全局组件。下面是代码:java

const install = (Vue) => {

  if (this._Vue) {
    return;
  };
  Vue.mixin({
    beforeCreate() {
      if (this.$options && this.$options.router) {
        this._routerRoot = this;
        this._router = this.$options.router;
        Vue.util.defineReactive(this, '_routeHistory', this._router.history)
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }

      Object.defineProperty(this, '$router', {
        get() {
          return this._routerRoot._router;
        }
      })

      Object.defineProperty(this, '$route', {
        get() {
          return {
            current: this._routerRoot._routeHistory.current,
            ...this._routerRoot._router.route
          };
        }
      })
    }
  });

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

  Vue.component('router-link', {    
    props: {
      to: String,
      tag: String,
    },
    render(h) { ... }
  })
  this._Vue = Vue;
}
复制代码

这里的this表明的就是vue-router对象,它有两个属性暴露出来供外界调用,一个是install,一个是Router构造函数,这样能够保证插件的正确安装以及路由实例化。咱们先忽略Router构造函数,来看install,上面代码中的this._Vue是个开始没有定义的属性,他的目的是防止屡次安装。咱们使用Vue.mixin对每一个组件的beforeCreate钩子作全局混入,目的是让每一个组件实例共享router实例,即经过this.$router拿到路由实例,经过this.$route拿到路由状态。须要重点关注的是这行代码:git

Vue.util.defineReactive(this, '_routeHistory', this._router.history)
复制代码

这行代码利用vue的响应式原理,对根vue实例注册了一个_routeHistory属性,指向路由实例的history对象,这样history也变成了响应式的。所以一旦路由的history发生变化,用到这个值的组件就会触发render函数从新渲染,这里的组件就是router-view。从这里能够窥察到vue-router实现的一个基本思路。上述的代码中对于两个全局组件的render函数的实现,由于会依赖于router对象,咱们先放一放,稍后再来实现它们,下面咱们分析一下Router构造函数。github

Router构造函数

通过刚才的分析,咱们知道router实例须要有一个history对象,须要一个保存当前路由状态的对象route,另外很显然还须要接受路由配置表routes,根据routes须要一个路由映射表routerMap来实现组件搜索,还须要一个变量mode判断是什么模式下的路由,须要实现pushreplace两个api,代码以下:vue-router

const Router = function (options) {
  this.routes = options.routes; // 存放路由配置
  this.mode = options.mode || 'hash';
  this.route = Object.create(null), // 生成路由状态
  this.routerMap = createMap(this.routes) // 生成路由表
  this.history = new RouterHistory(); // 实例化路由历史对象
  this.init(); // 初始化
}

Router.prototype.push = (options) => { ... }

Router.prototype.replace = (options) => { ... }

Router.prototype.init = () => { ... }
复制代码

咱们看一下路由表routerMap的实现,因为不考虑嵌套等其余状况,实现很简单,以下:api

const createMap = (routes) => {
  let resMap = Object.create(null);
  routes.forEach(route => {
    resMap[route['path']] = route['component'];
  })
  return resMap;
}
复制代码

RouterHistory的实现也很简单,根据前面分析,咱们只须要一个current属性就能够,以下:浏览器

const RouterHistory = function (mode) {
  this.current = null; 
}
复制代码

有了路由表和historyrouter-view的实现就很容易了,以下:异步

Vue.component('router-view', {
    render(h) {
      let routerMap = this._self.$router.routerMap;
      return h(routerMap[this._self.$route.current])
    }
  })
复制代码

这里的this是一个renderProxy实例,他有一个属性_self能够拿到当前的组件实例,进而访问到routerMap,能够看到路由实例historycurrent本质上就是咱们配置的路由表中的path函数

接下来咱们看一下Router要作哪些初始化工做。对于hash路由而言,url上hash值的改变不会引发页面刷新,可是能够触发一个hashchange事件。因为路由history.current初始为null,所以匹配不到任何一个路由,因此会致使页面刷新加载不出任何路由组件。基于这两点,在init方法中,咱们须要实现对页面加载完成的监听,以及hash变化的监听。对于history路由,为了实现浏览器前进后退时准确渲染对应组件,还要监听一个popstate事件。代码以下:

Router.prototype.init = function () {

  if (this.mode === 'hash') {
    fixHash()
    window.addEventListener('hashchange', () => {
      this.history.current = getHash();
    })
    window.addEventListener('load', () => {
      this.history.current = getHash();
    })
  }

  if (this.mode === 'history') {
    removeHash(this);
    window.addEventListener('load', () => {
      this.history.current = location.pathname;
    })
    window.addEventListener('popstate', (e) => {
      if (e.state) {
        this.history.current = e.state.path;
      }
    })
  }

}
复制代码

当启用hash模式的时候,咱们要检测url上是否存在hash值,没有的话强制赋值一个默认pathhash路由时会根据hash值做为key来查找路由表。fixHashgetHash实现以下:

const fixHash = () => {
  if (!location.hash) {
    location.hash = '/';
  }
}
const getHash = () => {
  return location.hash.slice(1) || '/';
}
复制代码

这样在刷新页面和hash改变的时候,current能够获得赋值和更新,页面能根据hash值准确渲染路由。history模式也是同样的道理,只是它经过location.pathname做为key搜索路由组件,另外history模式须要去除url上可能存在的hash,removeHash实现以下:

const removeHash = (route) => {
  let url = location.href.split('#')[1]
  if (url) {
    route.current = url;
    history.replaceState({}, null, url)
  }
}
复制代码

咱们能够看到当浏览器后退的时候,history模式会触发popstate事件,这个时候是经过state状态去获取path的,那么state状态从哪里来呢,答案是从window.history对象的pushStatereplaceState而来,这两个方法正好能够用来实现routerpush方法和replace方法,咱们看一下这里它们的实现:

Router.prototype.push = function (options) {
  this.history.current = options.path;
  if (this.mode === 'history') {
    history.pushState({
      path: options.path
    }, null, options.path);
  } else if (this.mode === 'hash') {
    location.hash = options.path;
  }
  this.route.params = {
    ...options.params
  }
}

Router.prototype.replace = function (options) {
  this.history.current = options.path;
  if (this.mode === 'history') {
    history.replaceState({
      path: options.path
    }, null, options.path);
  } else if (this.mode === 'hash') {
    location.replace(`#${options.path}`)
  }
  this.route.params = {
    ...options.params
  }
}
复制代码

pushStatereplaceState可以实现改变url的值但不引发页面刷新,从而不会致使新请求发生,pushState会生成一条历史记录而replaceState不会,后者只是替换当前url。在这两个方法执行的时候将path存入state,这就使得popstate触发的时候能够拿到路径从而触发组件渲染了。咱们在组件内按照以下方式调用,会将params写入router实例的route属性中,从而在跳转后的组件B内经过this.$route.params能够访问到传参。

this.$router.push({
    path: '/b',
    params: {
      id: 55
    }
 });

复制代码

router-link实现

router-view的实现很简单,前面已经说过。最后,咱们来看一下router-link的实现,先放上代码:

Vue.component('router-link', {    
    props: {
      to: String,
      tag: String,
    },

    render(h) {
      let mode = this._self.$router.mode;
      let tag = this.tag || 'a';
      let routerHistory = this._self.$router.history;
      return h(tag, {
        attrs: tag === 'a' ? {
          href: mode === 'hash' ? '#' + this.to : this.to,

        } : {},
        on: {
          click: (e) => {
            if (this.to === routerHistory.current) {
              e.preventDefault();
              return;
            }
            routerHistory.current = this.to;
            switch (mode) {
              case 'hash':
                if (tag === 'a') return;
                location.hash = this.to;
                break;
              case 'history':
                history.pushState({
                  path: this.to
                }, null, this.to);
                break;
              default:
            }
            e.preventDefault();
          }
        },
        style: {
          cursor: 'pointer'
        }
      }, this.$slots.default)
    }
  })
复制代码

router-link能够接受两个属性,to表示要跳转的路由路径,tag表示router-link要渲染的标签名,默认a为标签。若是是a标签,咱们为其添加一个href属性。咱们给标签绑定click事件,若是检测到本次跳转为当前路由的话什么都不作直接返回,而且阻止默认行为,不然根据to更换路由。hash模式下而且是a标签时候能够直接利用浏览器的默认行为完成url上hash的替换,否者从新为location.hash赋值。history模式下则利用pushState去更新url。

以上实现就是一个简单的vue-router,完整代码参见vue-router-simple

相关文章
相关标签/搜索