「手写实现」Vue-Router

258 阅读3分钟

国庆快乐.jpeg

前言

Vue Router是 Vue.js官方的路由管理器。它和Vue.js的核心深度集成。让构建单页面应用变得易如反掌。

Vue-Router源码手写实现

单页面应用程序中,url发生变化,不能刷新,显示对应视图内容

需求分析

  • spa

    • Hash ---> #/home
    • History ---> /home
  • 根据url显示对应内容

    • router-view
    • 数据响应式:current变量的url,一旦发生变化,动态重新执行render

任务

  • 实现一个插件

    • 实现VueRouter类

      • 处理路由选项
      • 监控url变化,hashchange。注:这边用hash演示,history中的pushState, popState一样的原理。
      • 响应变化
    • 实现install方法

      • 注册全局的$router方法
      • 实现两个全局组件,router-link, router-view

实现

  1. 实现VueRouter类并处理路由选项,install方法。

    类要想实现插件需要内部有install方法

    let Vue; // 引用构造函数,一会注册方法class VueRouter {
      constructor(options) {
        this.$options = options
      }
    }
    ​
    VueRouter.install =. function (_Vue) {
      Vue = _Vue
    }
    ​
    export default VueRouter
    
  2. 注册全局的$router方法

    let Vue; // 引用构造函数,一会注册方法class VueRouter {
      constructor(options) {
        this.$options = options
      }
    }
    ​
    VueRouter.install =. function (_Vue) {
      Vue = _Vue
      
      // 为什么要⽤混⼊⽅式写?主要原因是use代码在前,Router实例创建在后,⽽install逻辑⼜需要⽤到该实例
      Vue.mixin({
        beforeCreate() {
          // 只有根组件拥有router实例
          if (this.$options.router) {
            Vue.prototype.$router = this.$options.router
          }
        },
      })
    }
    ​
    export default VueRouter
    
  3. 实现router-link全局组件

    let Vue; // 引用构造函数,一会注册方法class VueRouter {
      constructor(options) {
        this.$options = options
      }
    }
    ​
    VueRouter.install =. function (_Vue) {
      Vue = _Vue
      
      // 为什么要⽤混⼊⽅式写?主要原因是use代码在前,Router实例创建在后,⽽install逻辑⼜需要⽤到该实例
      Vue.mixin({
        beforeCreate() {
          // 只有根组件拥有router实例
          if (this.$options.router) {
            Vue.prototype.$router = this.$options.router
          }
        },
      })
      
      Vue.component('router-link', {
        props: {
          to: {
            type: String,
            require: true,
          },
        },
        render(h) {
          return h('a', { attrs: { href: '#' + this.to } }, this.$slots.default)
        },
      })
    }
    ​
    export default VueRouter
    
  4. 监控url变化并响应变化

    let Vue; // 引用构造函数,一会注册方法class VueRouter {
      constructor(options) {
        this.$options = options
        
        // 定义初始化url
        const initial = window.location.hash.slice(1) || '/'
        
        // 使用Vue.util.defineReactive响应这个变化
        Vue.util.defineReactive(this, 'current', initial)
        
        // 监控url变化,hashchange
        window.addEventListener('hashchange', this.onHashChange.bind(this))
      }
      
      onHashChange() {
        this.current = window.location.hash.slice(1)
      }
    }
    ​
    VueRouter.install =. function (_Vue) {
      Vue = _Vue
      
      // 为什么要⽤混⼊⽅式写?主要原因是use代码在前,Router实例创建在后,⽽install逻辑⼜需要⽤到该实例
      Vue.mixin({
        beforeCreate() {
          // 只有根组件拥有router实例
          if (this.$options.router) {
            Vue.prototype.$router = this.$options.router
          }
        },
      })
      
      Vue.component('router-link', {
        props: {
          to: {
            type: String,
            require: true,
          },
        },
        render(h) {
          return h('a', { attrs: { href: '#' + this.to } }, this.$slots.default)
        },
      })
    }
    ​
    export default VueRouter
    
  5. 实现router-view全局组件

    let Vue; // 引用构造函数,一会注册方法class VueRouter {
      constructor(options) {
        this.$options = options
        
        // 定义初始化url
        const initial = window.location.hash.slice(1) || '/'
        
        // 使用Vue.util.defineReactive响应这个变化
        Vue.util.defineReactive(this, 'current', initial)
        
        // 监控url变化,hashchange
        window.addEventListener('hashchange', this.onHashChange.bind(this))
      }
      
      onHashChange() {
        this.current = window.location.hash.slice(1)
      }
    }
    ​
    VueRouter.install =. function (_Vue) {
      Vue = _Vue
      
      // 为什么要⽤混⼊⽅式写?主要原因是use代码在前,Router实例创建在后,⽽install逻辑⼜需要⽤到该实例
      Vue.mixin({
        beforeCreate() {
          // 只有根组件拥有router实例
          if (this.$options.router) {
            Vue.prototype.$router = this.$options.router
          }
        },
      })
      
      Vue.component('router-link', {
        props: {
          to: {
            type: String,
            require: true,
          },
        },
        render(h) {
          return h('a', { attrs: { href: '#' + this.to } }, this.$slots.default)
        },
      })
      
      Vue.component('router-view', {
        render(h) {
          let Component = null
          const route = this.$router.$options.routes.find(route => route.path === this.$router.current)
          if (route) {
            Component = route.component
          }
          return h(Component)
        },
      })
    }
    ​
    export default VueRouter
    
  6. 兼容嵌套路由

    let Vue; // 引用构造函数,一会注册方法class VueRouter {
      constructor(options) {
        this.$options = options
        
        // 不支持嵌套写法
        // 定义初始化url
        // const initial = window.location.hash.slice(1) || '/'
        // 使用Vue.util.defineReactive响应这个变化
        // Vue.util.defineReactive(this, 'current', initial)
        
        // 支持嵌套的写法
        // 定义响应式的属性current
        this.current = window.location.hash.slice(1) || '/'
    ​
        // 响应这个变化
        Vue.util.defineReactive(this, 'matched', [])
        this.match()
        
        // 监控url变化,hashchange
        window.addEventListener('hashchange', this.onHashChange.bind(this))
      }
      
      // 定义match函数递归遍历url,存放在this.matched中
      match(routes) {
        routes = routes || this.$options.routes
    ​
        // 递归遍历
        for (const route of routes) {
          if (route.path === '/' && this.current === '/') {
            this.matched.push(route)
            return
          }
    ​
          if (route.path !== '/' && this.current.indexOf(route.path) !== -1) {
            this.matched.push(route)
            if (route.children) {
              this.match(route.children)
            }
            return
          }
        }
      }
     
      onHashChange() {
        this.current = window.location.hash.slice(1)
        // url变化时清空并重新执行
        this.matched = []
        this.match()
      }
    }
    ​
    VueRouter.install =. function (_Vue) {
      Vue = _Vue
      
      // 为什么要⽤混⼊⽅式写?主要原因是use代码在前,Router实例创建在后,⽽install逻辑⼜需要⽤到该实例
      Vue.mixin({
        beforeCreate() {
          // 只有根组件拥有router实例
          if (this.$options.router) {
            Vue.prototype.$router = this.$options.router
          }
        },
      })
      
      Vue.component('router-link', {
        props: {
          to: {
            type: String,
            require: true,
          },
        },
        render(h) {
          return h('a', { attrs: { href: '#' + this.to } }, this.$slots.default)
        },
      })
      
      // 实现router-view
      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) {
              if (vnodeData.routerView) {
                depth++
              }
            }
    ​
            parent = parent.$parent
          }
    ​
          let Component = null
          const route = this.$router.matched[depth]
          if (route) {
            Component = route.component
          }
          return h(Component)
        },
      })
    }
    ​
    export default VueRouter
    

最终实现代码

// 引用构造函数,一会注册方法
let Vue// 实现VueRouter类
class VueRouter {
  constructor(options) {
    // 处理路由选项
    this.$options = options
​
    // 定义响应式的属性current
    this.current = window.location.hash.slice(1) || '/'
​
    // 响应这个变化
    Vue.util.defineReactive(this, 'matched', [])
    this.match()
​
    // 监控url变化,hashchange
    window.addEventListener('hashchange', this.onHashChange.bind(this))
  }
​
  match(routes) {
    routes = routes || this.$options.routes
​
    // 递归遍历
    for (const route of routes) {
      if (route.path === '/' && this.current === '/') {
        this.matched.push(route)
        return
      }
​
      if (route.path !== '/' && this.current.indexOf(route.path) !== -1) {
        this.matched.push(route)
        if (route.children) {
          this.match(route.children)
        }
        return
      }
    }
  }
​
  onHashChange() {
    this.current = window.location.hash.slice(1)
    this.matched = []
    this.match()
  }
}
​
// 实现install⽅法
VueRouter.install = function(_Vue) {
  Vue = _Vue
​
  // * $router注册
  // * 为什么要⽤混⼊⽅式写?主要原因是use代码在前,Router实例创建在后,⽽install逻辑⼜需要⽤到该实例
  // 任务1 挂载this.$router方法
  Vue.mixin({
    beforeCreate() {
      // 只有根组件拥有router实例
      if (this.$options.router) {
        Vue.prototype.$router = this.$options.router
      }
    },
  })
​
  //
  // 两个全局组件
  // 任务2 实现两个全局组件router-view,router-link
  // router-link
  Vue.component('router-link', {
    props: {
      to: {
        type: String,
        require: true,
      },
    },
    render(h) {
      return h('a', { attrs: { href: '#' + this.to } }, this.$slots.default)
    },
  })
​
  // 实现router-view
  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) {
          if (vnodeData.routerView) {
            depth++
          }
        }
​
        parent = parent.$parent
      }
​
      let Component = null
      const route = this.$router.matched[depth]
      if (route) {
        Component = route.component
      }
      return h(Component)
    },
  })
}
export default VueRouter

结语

  1. 获取url信息并响应化,使其在变化的时候能监听到,从而找到url对应的路由信息。并在全局组件的router-view中进行渲染。
  2. 嵌套路由需要记录深度(层级),并在初始化和变化的时候,递归获取对应的路由信息,并通过深度标记找到进行渲染。
  3. Vue Router使用Vue中的响应式数据,所在它是和Vue强耦合的,这也就是只能在Vue中使用的原因。