手写一个简单的 vue-router

435 阅读2分钟

需求分析

话不多说,直接上代码:

<div id="demo">
  <div>
    <router-link to="/">Home</router-link> |
    <router-link to="/about">About</router-link>
  </div>
  <router-view />
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.11/vue.js"></script>
<script src="./vue-router.js"></script>
<script>
  const Home = Vue.component('home', {
    template: '<div>Home</div>',
  })
  const About = Vue.component('about', {
    template: '<div>About</div>',
  })
</script>
<script>
  Vue.use(VueRouter)

  const routes = [
    {
      path: '/',
      name: 'Home',
      component: Home,
    },
    {
      path: '/about',
      name: 'About',
      component: About,
    },
  ]

  const router = new VueRouter({
    routes,
  })

  const app = new Vue({
    el: '#demo',
    data: {
      msg: 'hello',
    },
    router,
  })
</script>

插件基本结构

根据我们 vue-router 的使用方式,我们写出插件的基本结构如下:

class VueRouter {
  constructor(options) {}
}

VueRouter.install = function (Vue) {}

初次渲染路由

我们先不考虑路由变化的情况,仅实现第一次渲染。首先我们实现下我们的 VueRouter

class VueRouter {
  constructor(options) {
    this.routes = options.routes

    this.routeMap = {}
    this.routes.forEach((route) => {
      this.routeMap[route.path] = route
    })

    // 当前路由
    this.current = window.location.hash.slice(1)
  }
}

我们通过一个 routeMap 来存储路由和组件的对应关系以方便后续进行索引,同时用一个 current 变量来记录当前地址栏中的路由。

然后,我们需要在插件安装的时候定义下我们的 router-view 组件:

VueRouter.install = function (Vue) {
  Vue.component('router-view', {
    render(h) {
      const {routeMap, current} = ???
      const component = routeMap[current] ? routeMap[current].component : null
      return h(component)
    },
  })
}

我们需要拿到路由表以及当前的路由,但是从哪去获取呢?很明显,这个信息存在于 VueRouter 实例之上,但是我们执行 VueRouter.install 的时候还没有该实例,是不是就没有办法了呢?注意到 VueRouter 实例是传递给了 Vue 的根实例的,所以我们可以在根组件的生命周期中将 router 这个对象共享给所有组件,这样 router-view 在渲染的时候就可以拿到所需要的信息了:

VueRouter.install = function (Vue) {
  Vue.mixin({
    beforeCreate() {
      // 只有根组件上会有这个
      if (this.$options.router) {
        Vue.prototype.$router = this.$options.router
      }
    }
  })
  Vue.component('router-view', {
    render(h) {
      const {routeMap, current} = this.$router
      const component = routeMap[current] ? routeMap[current].component : null
      return h(component)
    },
  })
}

监听路由变化

我们使用 hashchange 来监听路由的变化,当路由变化的时候将当前 hash 赋值给 this.current。当然,为了使得当 this.current 发生变化的时候能够触发视图重新渲染,我们需要将 current 属性定义为响应式的:

class VueRouter {
  constructor(options) {
    ...

    Vue.util.defineReactive(this, 'current', window.location.hash.slice(1))

    window.addEventListener('hashchange', this.onHashChange.bind(this))
  }

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

实现 router-link

该组件接受一个名为 to 的属性,并渲染出一个 a 标签,例如:<a href='#/about'>About</a>

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

至此,一个简单的 vue-router 就实现了。