10分钟教你 手写 Vue-Router

  • 方法内部分为 3 步

    1. 如果插件已经安装,直接返回

    2. 把 vue 的构造函数记录到全局变量中,因为将来我们要在 VueRouter 的实例方法中使用 vue 的构造函数

    3. 把创建 Vue 实例时候传入的 router 对象注入到 Vue 实例上


let _Vue = null

export default class VueRouter {

  // 传入两个参数,一个是 vue 的构造函数,第二个是 可选的 选项对象

  static install (Vue) {

    // 1. 如果插件已经安装直接返回

    if (VueRouter.install.installed && _vue === true) return

    VueRouter.install.installed = true // 表示插件已安装



    // 2. 把 vue 的构造函数记录到全局变量中,因为将来我们要在 VueRouter 的实例方法中使用 vue 的构造函数

    _Vue = Vue



    // 3. 把创建 Vue 实例时候传入的 router 对象注入到 Vue 实例上

    Vue.mixin({

      beforeCreate () {

        if (this.$options.router) {

          _Vue.prototype.$router = this.$options.router

        }

      }

    })

  }

}



constructor

再次我们不需要过多的操作,只需要声明 3 个属性

  1. this.options = options 记录构造函数中传入的选项 options

  2. this.routeMap = {} 把 options 中传入的 routes 也就是路由规则解析出来,键值对的形式,键---路由地址,值---路由组件

  3. this.data = _Vue.observable({ current: ‘/’ })} observable 创建响应式对象,对象中存储当前路由地址,默认是 /


  constructor (options) {

    // 记录构造函数中传入的选项 options

    this.options = options

    // 把 options 中传入的 routes 也就是路由规则解析出来,键值对的形式,键---路由地址,值---路由组件

    this.routeMap = {}

    // data 是一个响应式对象,因为 data 中存储的是当前路由地址,路由变化时要自动加载组件

    this.data = _Vue.observable({ // observable 创建响应式对象

      current: '/' // 存储当前路由地址,默认是 /

    })

  }



createRouteMap

把构造函数中传过来的 选项中的 routes 也就是路由规则转换为键值对的形式转换到routeMap对象中 键---路由地址,值---路由组件


  createRouteMap () {

    // 遍历所有的路由规则,以键值对的形式存储在 routeMap 对象中

    this.options.routes.forEach(route => {

      this.routeMap[route.path] = route.component

    })

  }



initComponents

该方法用来创建 router-link 与 router-view

router-link

  initComponents (Vue) {

    Vue.component('router-link', {

      props: {

        to: String

      },

      template: '<a :href="to"><slot></slot></a>'

    })

  }



注意点
  • 如果我们使用 vue_cli 创建项目并且运行时,使用上述创建方法会出现一些报错,原因在于创建标签时使用了 template

  • 而 vue_cli 使用的 运行时版的 vue,不支持 template 模板,需要打包的时候提前编译

解决方法有两种

  1. 修改 vue.config.js 配置使用完整版的 vue

    完整包含运行时和编译器,体积比运行时版大 10k 左右,程序运行的时候把模板转换成 render 函数

    
    module.exports = {
    
        // 渲染完整版 vue 
    
        runtimeCompiler: true
    
    }
    
    
    
    
  2. 使用 render 函数

    运行时版本的 vue 不带编译器,所以不支持组件中的 template 选项,编译器的作用就是把 template 编译成 render 函数,运行时的组件可以直接写 render 函数

    单文件组件时一直使用 template 没写 render 是因为在打包的过程中把单文件的 template 编译成 render 函数了,这叫做预编译

render
  • render 函数接受一个参数,通常叫做 h 作用是帮我们创建虚拟 DOM,h 由 vue 传递

  • h 函数接受 3 个参数

    1. 创建这个元素的选择器。

    2. 给标签设置一些属性,如果是 DOM 对象的属性,需要添加到 attrs 中。

    3. 生成的元素的子元素,所以是数组形式

    
      initComponents (Vue) {
    
        Vue.component('router-link', {
    
          props: {
    
            to: String
    
          },
    
    	  /*
    
    	    运行时版本的 vue 不带编译器,所以不支持组件中的 template 选项,编译器的作用就是把 template 编译成 render 函数,运行时的组件可以直接写 render 函数
    
    
    
    		单文件组件时一直使用 template 没写 render 是因为在打包的过程中把单文件的 template 编译成 render 函数了,这叫做预编译
    
    	  */
    
          render (h) { // 该函数接收一个参数,通常叫做 h 作用是帮我们创建虚拟 DOM,h 由 vue 传递
    
            return h('a', {	// h 函数接受 3 个参数,1. 创建这个元素的选择器。2. 给标签设置一些属性,如果是 DOM 对象的属性,需要添加到 attrs 中。3. 生成的元素的子元素,所以是数组形式
    
              attrs: {
    
                href: this.to
    
              }
    
            }, [this.$slots.default])
    
          }
    
          // template: '<a :href="to"><slot></slot></a>'
    
        })
    
      }
    
    
    
    
router-view

    const self = this

	Vue.component('router-view', {

      // 获取到当前路由地址对应的路由组件

      render (h) {

        const component = self.routeMap[self.data.current]

        return h(component)

      }

    })



集合上述方法 检查是否存在问题

  • 因为我们默认只会调用 install 方法,但是我们还有一些其他方法需要调用,还需要一个 init 方法,在 install 方法 被调用后,调用 createRouteMap() 与 initComponents()

  init () {

    this.createRouteMap()

    this.initComponents(_Vue)

  }



  • 将上述所有方法整合(为了便于阅读,去掉注释)

    
    // ../vuerouter/index.js
    
    let _Vue = null
    
    export default class VueRouter {
    
      static install (Vue) {
    
        if (VueRouter.install.installed && _Vue === true) return
    
        VueRouter.install.installed = true
    
    
    
        _Vue = Vue
    
    
    
        Vue.mixin({
    
          beforeCreate () {
    
            if (this.$options.router) {
    
              _Vue.prototype.$router = this.$options.router
    
              this.$options.router.init()
    
            }
    
          }
    
        })
    
      }
    
    
    
      constructor (options) {
    
        this.options = options
    
        this.routeMap = {}
    
        this.data = _Vue.observable({
    
          current: '/'
    
        })
    
      }
    
    
    
      init () {
    
        this.createRouteMap()
    
        this.initComponents(_Vue)
    
    	  this.initEvent()
    
      }
    
    
    
      createRouteMap () {
    
        this.options.routes.forEach(route => {
    
          this.routeMap[route.path] = route.component
    
        })
    
      }
    
    
    
      initComponents (Vue) {
    
        Vue.component('router-link', {
    
          props: {
    
            to: String
    
          },
    
    	  render (h) {
    
            return h('a', {
    
              attrs: {
    
                href: this.to
    
              }
    
            }, [this.$slots.default])
    
          }
    
        })
    
        const self = this
    
        Vue.component('router-view', {
    
          render (h) {
    
            const component = self.routeMap[self.data.current]
    
            return h(component)
    
          }
    
        })
    
      }
    
    }
    
    // 记得替换 router/index.js 中引入的 VueRouter
    
    import Vue from 'vue'
    
    // import VueRouter from 'vue-router'
    
    import VueRouter from '../vuerouter'
    
    import Home from '../views/Home.vue'
    
    import Home from '../views/About.vue'
    
    
    
    Vue.use(VueRouter)
    
    
    
    const routes = [
    
      {
    
        path: '/',
    
        name: 'Home',
    
        component: Home
    
      },
    
      {
    
        path: '/about',
    
        name: 'About',
    
        component: About
    
      }
    
    ]
    
    
    
    const router = new VueRouter({
    
      routes
    
    })
    
    
    
    export default router
    
    
    
    
    
    
    • 实际测试后,我们会发现,上述仍然存在一些小问题,也就是我们在点击 router-link 时,会改变路径刷新页面,但是在单页面组件中,我们是不需要刷新页面的,所以我们需要对 router-link 在做一点小调整

    完善 router-link

    • 我们需要给 a 标签添加 1 个 方法,这个方法有三个作用

      1. 通过 pushState 方法 改变浏览器地址栏,但不给服务器发请求

      2. 将当前路由路径 同步给默认值

      3. 阻止标签默认事件

      pushState 方法接受 3 个参数

      1. data。
      1. title 网页的标题。
      1. url 地址。
    
        Vue.component('router-link', {
    
          props: {
    
            to: String
    
          },
    
          render (h) {
    
            return h('a', {
    
              attrs: {
    
                href: this.to
    
              },
    
    		  on: {
    
    			  click: this.clickHandler
    
    		  }
    
            }, [this.$slots.default])
    
          },
    
    	  methods: {
    
    		  clickHandler (e) {
    
              // 1. 通过 pushState 方法 改变浏览器地址栏,但不给服务器发请求
    
              history.pushState({}, '', this.to)	// 该方法接受 3 个参数,1. data。2. title 网页的标题。3. url 地址
    
              this.$router.data.current = this.to
    
              e.preventDefault()
    
    		  }
    
    	  }
    
        })
    
    
    
    

最终的完善

将上边的 router-link 完善后,我们的 Vue-Router 就实现了,但是还差一个小功能,也就是浏览器左上角的前进后退就失效了,所以我们需要在添加一个小方法来实现最终的完善

  • 其实就是通过 监听 popstate 将当前的路由路径赋值给 current 来达到左上角的前进后退功能**(记得将其添加到 init 方法中)**

  initEvent () {

	  window.addEventListener('popstate', () => {

		  this.data.current = window.location.pathname

	  })

  }



  • 最终完整版代码(分为两版,有注释的在最后)

let _Vue = null

export default class VueRouter {

  static install (Vue) {

    if (VueRouter.install.installed && _Vue === true) return

    VueRouter.install.installed = true



    _Vue = Vue



    Vue.mixin({

      beforeCreate () {

        if (this.$options.router) {

          _Vue.prototype.$router = this.$options.router

          this.$options.router.init()

        }

      }

    })

  }



  constructor (options) {

    this.options = options

    this.routeMap = {}

    this.data = _Vue.observable({

      current: '/'

    })

  }



  init () {

    this.createRouteMap()

    this.initComponents(_Vue)

	this.initEvent()

  }



  createRouteMap () {

    this.options.routes.forEach(route => {

      this.routeMap[route.path] = route.component

    })

  }



  initComponents (Vue) {

    Vue.component('router-link', {

      props: {

        to: String

      },

      render (h) {

        return h('a', {

          attrs: {

            href: this.to

          },

		  on: {

			  click: this.clickHandler

		  }

        }, [this.$slots.default])

      },

	  methods: {

		  clickHandler (e) {

          history.pushState({}, '', this.to)

          this.$router.data.current = this.to

          e.preventDefault()

		  }

	  }

    })

    const self = this

    Vue.component('router-view', {

      render (h) {

        const component = self.routeMap[self.data.current]

        return h(component)

      }

    })

  }



  initEvent () {

	  window.addEventListener('popstate', () => {

		  this.data.current = window.location.pathname

	  })

  }

}






let _Vue = null

export default class VueRouter {

  // 传入两个参数,一个是 vue 的构造函数,第二个是 可选的 选项对象

  static install (Vue) {

    // 1. 如果插件已经安装直接返回

    if (VueRouter.install.installed && _Vue === true) return

    VueRouter.install.installed = true // 表示插件已安装



    // 2. 把 vue 的构造函数记录到全局变量中,因为将来我们要在 VueRouter 的实例方法中使用 vue 的构造函数

    _Vue = Vue



    // 3. 把创建 Vue 实例时候传入的 router 对象注入到 Vue 实例上

    Vue.mixin({

      beforeCreate () {

        if (this.$options.router) {

          _Vue.prototype.$router = this.$options.router

          this.$options.router.init()

        }

      }

    })

  }



  constructor (options) {

    // 记录构造函数中传入的选项 options

    this.options = options

    // 把 options 中传入的 routes 也就是路由规则解析出来,键值对的形式,键---路由地址,值---路由组件

    this.routeMap = {}

    // data 是一个响应式对象,因为 data 中存储的是当前路由地址,路由变化时要自动加载组件

    this.data = _Vue.observable({ // observable 创建响应式对象

      current: '/' // 存储当前路由地址,默认是 /

    })

  }



  init () {

    this.createRouteMap()

    this.initComponents(_Vue)

	this.initEvent()

  }



  // 把构造函数中传过来的 选项中的 routes  也就是路由规则转换为键值对的形式转换到routeMap对象中   键---路由地址,值---路由组件

  createRouteMap () {

    // 遍历所有的路由规则,以键值对的形式存储在 routeMap 对象中

    this.options.routes.forEach(route => {

      this.routeMap[route.path] = route.component

    })

  }



  // 创建两个组件 router-link 与 router-view

  initComponents (Vue) {	// 接收参数,减少与外界联系
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值