手动实现一个vue-router

741 阅读2分钟

前言

手动实现一个vue-router,了解基本的原理。

插件注册

vue-router是使用Vue.use的方式注册

Vue.use就是执行你传入的对象或者函数上是否含有instal方法,有则直接执行,将Vue作为第一个参数传入

Vue.use = function (plugin: Function | Object) {
    // ...
    // 处理参数,第一个参数不要,再往前面放一个Vue构造函数
    const args = toArray(arguments, 1)
    args.unshift(this)
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    return this
  }

vue-router的基本使用

下面是使用的核心代码

// 1.注册一下vueRouter的插件,实际上执行是vueRouter.install方法 
Vue.use(vueRouter)
// 2.一个路由的配置表
const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About,
  }
]
// 3.实例化routes
const router = new VueRouter({
  routes
})
new Vue({
    router
})

// 4.在组件中使用
<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </div>
    <router-view/>
  </div>
</template>

实现思路

vue-router在使用之后,个vue实例上都多了一个$store属性,可以使用router-link和router-view组件,同时每,这些都是在Vue.use时,执行install方法作用的。

1.install方法如下

  • 给每个vue实例添加一个$store, install方法,Vue.mixin混入beforeCreate生命周期
  • install方法中,注册两个全局组件router-link和router-view
  • router-link组件,渲染出一个a标签,将传入的to属性,设置到a标签的href属性上
  • router-view组件,根据路由表通过render函数,找出对应的组件渲染出来 2.组件的动态切换,监听popState/hashchange方法,路由改变时,实现组件更新,这个可以利用vue的响应式来实现

基础版的代码实现

let Vue
export default class VueRouter {
    constructor(options) {
        this.$options = options
        // 将current的属性变成响应式,router-view组件有使用到current,所以当current变化时,outer-view组件会自动更新,这也是vue本身的特性。
        Vue.util.defineReactive(this, 'current', window.location.hash.slice(1) || '/')
        // 使用hashchange事件,在谷歌浏览器上赋值不成功了,改成popstate事件
        window.addEventListener('popstate', ()=>{
          this.current =  window.location.hash.slice(1)
        })
        this.map = {}
        options.routes.forEach(route=>{
            this.map[route.path] = route.component
        })
    }
}

VueRouter.install = function(_Vue) {
    Vue = _Vue
    Vue.mixin({
        beforeCreate() {
           // router传入vue的根实例,被赋值给原型,所以的vue实例都拥有$router
          if(this.$options.router) {
            Vue.prototype.$router = this.$options.router
          }
        }
    })
    Vue.component('router-link', {
        props: {
            to: String,
            required: true
        },
        render(h) {
            // 通过render函数,渲染a标签,a标签的内容是在默认插槽中,通过this.$slots.default取出
            return h('a', {attrs: {href: '#'+this.to}}, this.$slots.default)
        }
    })
    Vue.component('router-view',{
        render(h) {
            let component = null
            component = this.$router.map[this.$router.current] || null
            return h(component)
        }
    })
}

路由嵌套版的实现

假如在About组件中,还有一个router-view,再去请求/about,就会一直渲染About组件,栈溢出。所以实现起来稍微复杂一些

使用

// 路由表
const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About,
    children: [
      {
        path: '/about/info',
        component: {
          render(h) {
            return h('div', '我是children组件')
          }
        }
      }
    ]
  }
]

// about组件
<template>
  <div class="about">
    <h1>This is an about page</h1>
    <router-view/>
  </div>
</template>

实现

思路:当访问/about/info时,将/about/info自身组件和它所有父级的组件都匹配到一个数组里。每次渲染router-view组件时,做一个标记,最后计算路由嵌套的层数,取出对应的组件渲染。

let Vue
export default class VueRouter {
    constructor(options) {
        this.$options = options
        this.current =  window.location.hash.slice(1) || '/'
        Vue.util.defineReactive(this, 'matched', [])
        this.match()
        window.addEventListener('popstate', ()=>{
          this.current =  window.location.hash.slice(1)
          this.matched = []
        })
        this.map = {}
        options.routes.forEach(route=>{
            this.map[route.path] = route.component
        })
    }
    
    match(routes) {
        routes = routes || []
        for(const route of routes) {
            // 首页,假设没有路由嵌套
            if(route.path === '/' && this.current === '/') {
                this.matched.push(route)
                return 
            }
            
            // 匹配到路由所有的父级路由,push到一个数组里
            if(route.path !== '/' && this.current.indexOf(route.path) != -1) {
              this.matched.push(route)
              if(route.children) {
                  this.match(route.children)
              }
              return
            }
        }
    }
}

VueRouter.install = function install(_Vue) {
  Vue = _Vue
  // 全局混入
  // ...
  // 
  Vue.component('router-view', {
    render(h){
      // 标记当前组件是路由组件,方便下次计算深度
      this.$vnode.data.routerView = true
      let depth = 0
      let parent = this.$parent
      while(parent) {
        const vnodeData = parent.$vnode && parent.$vnode.data
        if(vnodeData && vnodeData.routerView) {
            depth++
        }
        parent = parent.$parent
      }

      // 获取当前路由对应的组件
      let component = null
      onst route = this.$router.matched[depth]
      if(route) component = route.component
      return h(component)
    }
  })
}

最后

大家对于vue-router的实现,是否有一个初步的了解呢,当然要深入了解还是要自己去看源码实现,欢迎点赞~