简单的VueRouter实现

247 阅读2分钟

1从VueRouter使用分析

首先,我们在项目中装好VueRouter之后,会自动生成一个router文件夹,然后我们从自带的index.js文件来看。

// router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

// 1.应用插件
Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'home',
    component: Home
  },
  {
    path: '/about',
    name: 'about',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

// 2.创建实例
const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router
这里引入了VueRouter之后使用了vue.use()所以我们之后手写的时候需求把我们手写的router变成插件的形式。

在我们进行配置之后进行实例的创建并导出。

**在main.js中使用。**
import router from './krouter'

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

这里挂载了router的实例,这里这么做的原因是想要能在全局进行使用,所以我们在插件中使用的时候应该要进行Vue.prototype.$router = router。这样我们在其他组件中才能正常的使用$router。


2 VueRouter工作原理分析

当我们在组件中使用的时候会用到<router-link>router-view,这两个组件是可以直接使用的,所以我们的插件里要有这两个组件的声明和注册。

接着,我们用一幅图来大致描述一下VueRouter的工作原理。

84cef92b483269b52bb8720c9e5b1501.png

基本思路我们已经有了,现在就可以开始着手实现了。


3 VueRouter实现

我们先看看如何创建的router实例

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

他通过new传入关键字来创建,所以我们创建一个VueRouter类

// crouter/cvue-router.js
let Vue;

class CVueRouter {
  constructor(option) {
    this.$option = option
  }
}

CVueRouter.install = function(_Vue) {
  Vue = _Vue
}

export default CVueRouter
这里创建一个全局的Vue是因为vue-router是一个组件,如果我们在这里面也把Vue包含了,这个包就会非常的大。而使用vue.use()时必须提供install方法,会默认调用Install方法并且传入Vue的构造函数,所以我们这里通过`_Vue`就能接受Vue了。这样就可以很好的避免上面的情况了。

我们的VueRouter创建完成之后,我们首先要做的就是让我们的router能在全局使用,我们的rouer配置只在根实例里面才能拿到,要在全局使用我们得拿到根实例里面的配置。于是,我们使用mixin在全局进行混入。这样我们就能拿到每个实例,然后找到根实例,将配置进行全局的挂载。
CVueRouter.install = function(_Vue) {
  Vue = _Vue

  Vue.mixin({
    beforeCreate() {
      console.log('this', this);
      if(this.$options.router) {
        Vue.prototype.$router = this.$options.router
      }
    },
  })
}

接下来我们就需要来实现一下router-linkrouter-view这两个全局组件了。

首先因为要创建组件,所以我们用Vue.component来创建组件。

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

router-link的本质其实就是a标签,a标签的使用一般都是<a href="#/about">abc</a>然后我们在使用router-link时都是<router-link to="/about">xxx</router-link>,于是createElement要做什么就很明确了。

h('a', { attrs: { href: '#' + to的参数 } }, 内容)

想要拿到to的参数,我们需要使用props,而内容这里就用上了插槽,我们通过默认插槽就可以直接拿到内容,所以。

Vue.component('router-link', {
  props: {
    to: {
      type: String,
      require: true
    }
  },
  render(h) {
    return h('a', { attrs: { href: '#' + this.to } }, this.$slots.default)
  },
})

接下来我们创建router-view组件——根据当前路径在路由表中获取到组件实例,直接进行渲染。

constructor(option) {
  this.$option = option;

  this.current = "/";
  window.addEventListener("hashchange", () => {
    console.log(window.location.hash.slice(1));
    this.current = window.location.hash.slice(1);
  });
}
Vue.component('router-view', {
  render(h) {
    let component = null
    this.$router.$option.routes.forEach(route => {
      if(route.path ===  this.$router.current) {
        component = route.component
      }
    })
    return h(component)
  },
})

首页显示

47745959b9286a7bb031b7087aecb276.png

点击about切换页面

image.png

现在页面已经能正常显示了。

但是现在又出现了一个问题,我们的render函数现在只会渲染一次。我们当前的current还不是响应式的,不会重复去渲染页面。所以我们就要让current变成响应式的。

这里我们就要借用vue来将current变成响应式数据:`Vue.util.defineReactive(this, 'current', '/')``,因为我们全局设置了Vue变量来存储vue的构造函数,所以我们可以在构造器中直接使用。

但是这里又有个小bug,就是当用户直接刷新的时候,因为我们的current设置了初始值,页面会回到初始值的页面,我们稍稍修改一下。

class CVueRouter {
  constructor(option) {
    this.$option = option;

    Vue.util.defineReactive(this, "current", "/");
    window.addEventListener("hashchange", this.onHashChange.bind(this));
    // 页面刷新将当前路由传入,更新cuttent触发render渲染对应组件
    window.addEventListener("load", this.onHashChange.bind(this));
  }

  onHashChange() {
    this.current = window.location.hash.slice(1);
  }
}

这样就完美解决了。


4 优化

这里我们有没有发现,当我们在router-view里面渲染的时候我们每次都是遍历路由表来获取组件,我们为什么不做一个path与commponent的映射表,这样就能快速获取到我们想要的实例了。

constructor(option) {
  ...
  this.routerMap = {};
  option.routes.forEach((route) => {
    this.routerMap[route.path] = route;
  });
}

Vue.component("router-view", {
  render(h) {
    const { routerMap, current } = this.$router;
    const component = routerMap[current].component || null;
    return h(component);
  },
});

这里我们还可以像vue的源码一样,把router-vew和router-link单独抽离成一个文件,然后导入进行使用。这里只是为了让代码结构更合理,就不做演示了。