手把手教你打造属于自己的VueRouter

155 阅读4分钟

前言

在上一篇文章中,我简单的带大家了解了一下前端路由的原理,这次,我将带大家手写一个属于自己的VueRouter。

创建项目

我们首先利用Vue-cli创建一个Vue项目,记得勾选安装Router选项。

~D4SZ2Z@4F1QDB9ZPSH4@6S.png

删除一些没用的文件后项目结构如下图所示:

1.png

具体文件的代码如下: App.vue:

<template>
  <div id="app">
    <nav>
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </nav>
    <router-view/>
  </div>
</template>

route/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'home',
    component: HomeView
  },
  {
    path: '/about',
    name: 'about',
    component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router

Home.vue

<template>
  <div class="home">
    <h1>this is home page</h1>
  </div>
</template>

About.vue

<template>
  <div class="about">
    <h1>This is an about page</h1>
  </div>
</template>

至此,一切准备工作完成,现在我们将VueRouter的引入改成我们自己的myVueRouter.js

import VueRouter from './myVueRouter.js'

分析

通过观察原始项目,我们可以发现VueRouter({...})就是一个构造函数,因为他通过new创建了一个实例router,所以,我们可以很容易得出结论:VueRouter的本质是一个类

所以我们可以写出如下代码:

class VueRouter{}

我们还用使用了Vue.use()方法,使每个组件都可以拥有store的实例,而use方法的原则就是执行对象的install方法。

所以,补充代码如下:

class VueRouter {}

VueRouter.install = function(){}

最后将VueRouter导出即可

class VueRouter {}

VueRouter.install = function(){}
export default VueRouter

对于install方法,我们需要保证install方法被执行的时候第一个参数是Vue,其余参数是注册插件时传入的参数,所以我们将Vue保存起来。

// myVueRouter.js
let Vue = null;
class VueRouter{}
VueRouter.install = function(v) {
  Vue = v
}
export default VueRouter

我们知道,在使用VueRouter的过程中,有两个组件经常用得到,他们是router-link和router-view,我们该怎么实现他们呢,Vue中为我们提供了一个创建组件的方法Vue.component(),添加到install方法中。

    Vue.component('router-link',{
        render(h){
            return h('a',{},'首页')
        }
    })
    Vue.component('router-view',{
        render(h){
            return h('h1',{},'首页视图')
        }
    })
};

对install方法进行补充:

Vue.mixin({ // 将mixin的内容混入到Vue的初始参数options中
    beforeCreate() { // 需要在$options初始化完成之前准备
      if (this.$options && this.$options.router) { // 是根组件
        this._root = this // 将_root挂载到根组件的实例上
        this._router = this.$options.router 
      } else { // 是子组件
        // 将_root根组件复制一份到子组件上
        this._root = this.$parent && this.$parent._root
      }

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

上述代码主要是为了让除了根组件以外的其他组件也能拥有router,最后通过defineProperty对this进行劫持,虽然我们获取的是$router,但其实返回的是根组件的_root._router。

然后我们对VueRouter传入的参数进行处理,第一个参数表示当前的路由模式,第二个参数是一个数组格式的路由表,但我们直接处理路由表很不方便,对其进行处理,转化成key:value的格式,代码如下:

constructor(options) {
    this.mode = options.mode || 'hash';
    this.routes = options.routes
    this.routesMap = this.createMap(this.routes)
  }
  createMap(routes) {
    return routes.reduce((pre, current) => {
      pre[current.path] = current.component // '/':Home, '/about':About
      return pre
    }, {})
  }

路由中需要存放当前的路径,为了表示当前的路径状态,我们可以用对象来管理

constructor() {
    this.current = null;
}

同时VueRouter中还需要添加一句

this.history = new HistoryRoute()

但目前的current也就是当前的路径还是null,所以我们需要进行初始化,同时还要先判断路由的模式,然后将路径保存在current中,实现原理可以参考上一篇文章。

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 {
      location.pathname ? '' : location.pathname = '/'
      window.addEventListener('load', () => {
        this.history.current = location.pathname
      })
      window.addEventListener('popstate', () => {
        this.history.current = location.pathname
      })
    }
  }

我们已经可以获取当前的路径,可以开始实现$route了

Object.defineProperty(this, '$route', {
    get() {
      return this._root._router.history.current
    }
})

跟router类似,也是通过数据劫持然后返回当前的路径。

现在我们保存了当前的路径,可以根据路径从路由表中获取对应的组件进行渲染,由此完善router-view组件

Vue.component('router-view', {
    render(h) {
      let current = this._self._root._router.history.current // 当前路由地址 '/about'
      let routerMap = this._self._root._router.routesMap // 所有路由对象
      return h(routerMap[current])
    }
})

render函数里的this指向一个Proxy代理对象,代理Vue组件,而我们前面说每个组件都有一个_root属性指向根组件,根组件上有_router这个路由实例,所以我们可以从router实例上获取路由表,也可以获得当前的路径,然后再把获得的组件放到h()里进行渲染。

但有一个问题,当我们改变路径的时候,视图是没有重新渲染的,所以,需要将_router.history进行响应化。

Vue.util.defineReactive(this, 'xxx', this._router.history)

我们可以利用Vue的一个api:defineReactive,使得this._router.history对象得到监听

下面对Vue-router进行完善

Vue.component('router-link', { // 声明全局组件
    props: {
      to: String
    },
    render(h) {
      let mode = this._self._root._router.mode
      let to = mode === 'hash' ? '#' + this.to : this.to
      return h('a', {attrs:{href: to}}, this.$slots.default)
    }
})

我们把router-link渲染成a标签,通过点击a标签可以实现url上的路径的切换,从而实现视图的重新渲染。

至此,我们已经完成全部的代码。完整代码如下:

let Vue = null

class HistoryRoute{
  constructor() {
    this.current = null;
  }
}

class VueRouter {
  constructor(options) {
    this.mode = options.mode || 'hash';
    this.routes = options.routes
    this.routesMap = this.createMap(this.routes)

    this.history = new HistoryRoute()
    this.init()
  }
  createMap(routes) {
    return routes.reduce((pre, current) => {
      pre[current.path] = current.component // '/':Home, '/about':About
      return pre
    }, {})
  }

  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 {
      location.pathname ? '' : location.pathname = '/'
      window.addEventListener('load', () => {
        this.history.current = location.pathname
      })
      window.addEventListener('popstate', () => {
        this.history.current = location.pathname
      })
    }
  }
}

VueRouter.install = function(v) { // 确保install方法只能被构造函数调用,实例对象无法使用
  Vue = v
  // console.log(Vue);

  Vue.mixin({ // 将mixin的内容混入到Vue的初始参数options中
    beforeCreate() { // 需要在$options初始化完成之前准备
      if (this.$options && this.$options.router) { // 是根组件
        this._root = this // 将_root挂载到根组件的实例上
        this._router = this.$options.router 
        Vue.util.defineReactive(this, 'xxx', this._router.history)
      } else { // 是子组件
        // 将_root根组件复制一份到子组件上
        this._root = this.$parent && this.$parent._root
      }

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

      Object.defineProperty(this, '$route', {
        get() {
          return this._root._router.history.current
        }
      })
    }
  })

  Vue.component('router-link', { // 声明全局组件
    props: {
      to: String
    },
    render(h) {
      let mode = this._self._root._router.mode
      let to = mode === 'hash' ? '#' + this.to : this.to
      return h('a', {attrs:{href: to}}, this.$slots.default)
    }
  })

  Vue.component('router-view', {
    render(h) {
      let current = this._self._root._router.history.current // 当前路由地址 '/about'
      let routerMap = this._self._root._router.routesMap // 所有路由对象
      return h(routerMap[current])
    }
  })
}

export default VueRouter;

实现效果如图:

2.png

3.png 以上就是手写一个VueRouter的全部过程了,欢迎大家指正。