vueRouter实现原理解析(从零实现一个简易版的vueRouter)

前一篇文章,我们说了vueRouter的两种路由模式,以及两种路由模式的特点,区别,今天我们通过来自己写一个简易版的vueRouter来剖析vueRouter内部的核心实现原理

vueRouter所需实现功能

首先,我们先来总结下vueRouter需要实现哪些基本功能

  1. 我们可以直接通过this.$router取到router实例,故我们需要将router挂载到vue实例下
  2. 我们需要实现一个router-link组件,用于去改变地址栏中的路由地址
  3. 我们需要实现一个router-view组件,用于渲染当前路由地址对应的路由组件
  4. 我们需要实现当我们首次进入页面时,或者手动修改浏览器url地址栏中的路由地址时,也会渲染该路由地址对应的路由组件
  5. 我们需要实现,当点击浏览器的前进后退时,也会自动渲染前进或者后退以后,当前路由地址对应的路由组件

好了,根据我们列出来的功能,我们现在开始来写我们自己的vueRouter

准备工作

我们先准备一个新创建的vue项目,然后新建自己vueRouter.js文件,然后将router.js中引入vueRouter插件的代码改成引入我们自己创建的vueRouter.js文件
在这里插入图片描述
准备工作做好后,我们开始来写自己的vueRouter

自定义VueRouter

1、定义一个VueRouter类

我们都知道,我们是通过 new 关键字 去创建一个router实例,那么,很显然VueRouter是一个构造函数或者是一个类

export default class VueRouter {
	
}
2、注册插件

我们每次引入VueRouter后,都会先执行一条命令Vue.use(VueRouter),这其实是注册插件
了解Vue.use()的朋友应该就知道,调用use方法,Vue内部实际是会去调用插件內部的install方法去进行注册。调用install时,会将Vue构造函数作为参数传入install方法中
所以,我们首先需要去顶一个install 静态方法

export default class VueRouter {
	static install(Vue) {
		
	}
}

此时,在这里我们可以做3件事

  1. 判断该插件是否注册过,如果未注册,那我们就继续走注册逻辑
  2. 用一个变量存储一下Vue, 方便后面在其他函数中使用
  3. 在vue实例上挂载$router,以后vue中可以直接通过this.$router访问router实例对象
    代码如下:
    在这里插入图片描述

3、vueRouter构造器

之后,我们可以来写vueRouter的构造器了,大家都知道,构造器中的代码是在new一个实例的时候会执行的。那我们总结下,在这里,我们需要做些什么。

  1. 其次,我们需要定义一个变量来接收当前的路由模式。
  2. 我们需要实现根据当前路由地址,渲染对应的路由组件,那么,我们就需要一个路由地址和组件的一个映射表。所以,我们还需要定义一个路由映射表。
  3. 我们需要去注册vue的两个全局组件 router-link 和 router-view,用来改变路由地址,已经渲染路由组件,这个毫无疑问。
  4. 我们还需要实现,当我们手动改变路由地址时,然后enter键重新加载页面时,页面也需要根据当前的路由地址来渲染对应的组件。故,我们需要注册一个load事件,来监听页面的加载。
  5. 最后,也是最关键的一点,大家有没有想过,当我们的路由地址发生变化后,我们如何让vue知道路由地址发生变化了,然后在router-view标签的位置那去重新渲染当前路由地址所对应的路由组件呢。很明显,vue实现数据发生变化,从而使页面无刷新更新视图,靠的就是将数据转化为响应式数据,那么当数据发生变化时,就会触发数据的set, 然后再去更新这个数据的所有依赖者。
    所以,我们是不是可以定义一个变量,来存储当前的路由地址,然后在router-view这个组件内,去使用这个变量的值,去路由映射表中,找到对应的路由组件,然后渲染对应的路由组件啊。这么依赖,router-view是不是就是这个变量的一个依赖者啊,那么当变量发生变化时,router-view就会自动重新渲染,从而更新视图啊。

下面,我们来一一实现这些功能

3.1 定义一个变量接收路由模式
constructor(options) {
	this.mode = options.mode || 'history'
}
3.2 定义一个路由映射表
constructor(options) {
	this.mode = options.mode || 'history'

    // 实现routerMap(用于存储路由与组件的映射关系)
    this.routerMap = {}
    this.createRouteMap(options.routes || [])
}

// 解析路由表,得到路由与组件的映射关系
createRouteMap (routes, parentPath) {
    if (routes && routes.length && routes.length > 0) {
      routes.forEach((item) => {
        let cPath = ''
        if (parentPath) {
          cPath = `${parentPath}/${item.path}`
        } else {
          cPath = item.path
        }
        this.routerMap[cPath] = item.component
        if (item.children && item.children.length > 0) {
          this.createRouteMap(item.children, cPath)
        }
      })
    }
}
3.3 定义一个响应式变量,用来存储当前路由地址

上面写的第五点,我们把它提到第二步来写,因为后面会用到这个变量

constructor(options) {
	// 记录当前的路由模式,如果没传,就默认hash模式
	this.mode = options.mode || 'hash'

	// 定义一个响应式对象,当后续current的值发生变化时,vue可以监测到
    this.data = _Vue.observable({
      current: this.mode === 'history' ? '/' : '#/' // 存放当前url地址
    })
}
3.4 注册router-link组件 和 router-view组件

相关代码解释我都写在代码里面了,这里就不再过多的说组件注册逻辑了

constructor(options) {
	// 记录当前的路由模式,如果没传,就默认hash模式
	this.mode = options.mode || 'hash'

	// 实现routerMap(用于存储路由与组件的映射关系)
    this.routerMap = {}
    this.createRouteMap(options.routes || [])
    
	// 定义一个响应式对象,当后续current的值发生变化时,vue可以监测到
    this.data = _Vue.observable({
      current: this.mode === 'history' ? '/' : '#/' // 存放当前url地址
    })

	// 注册全局组件
	this.initComponent()
}

initComponents () {
    // 初始化router-link组件
    this.initLink()

    // 初始化router-view组件
    this.initView()
}

// 注册router-link组件
initLink () {
    _Vue.component('router-link', {
      props: {
        to: String
      },
      render (h) {
        return h('a', {
          attrs: { href: this.to },
          on: {
            click: this.locationHref
          }
        }, [this.$slots.default])
      },
      methods: {
        locationHref (e) {
          if (this.$router.mode === 'history') {
            /**
             * @description pushState用于改变浏览器跳转地址
             * 参数有3个 第一个参数:一个对象,后续触发popState事件时,传给popState的事件的事件对象
             * 第二个参数:是title,网页标题
             * 第三个参数:需要跳转的url地址
             */
            history.pushState({}, '', this.to)

            // 更新data下的current变量的值(该变量用于记录当前url地址,当url发生变化时,需要改变这个变量的值)
            // 因current是响应式数据,故当值发生变化时,会触发对应组件的重新渲染,从而当url发生变化时,页面也会发生变化
            this.$router.data.current = this.to
          } else {
            window.location.hash = `#${this.to}`
            this.$router.data.current = `#${this.to}`
          }

          // 阻止a标签默认事件,这里需要阻止a标签的href跳转,因为a标签的href跳转是会让浏览器直接向服务器去发送请求的
          e.preventDefault()
      }
    }
  })
}

// 注册router-view组件
initView () {
  const self = this
    _Vue.component('router-view', {
      render (h) {
        // 从路由表中获取当前path对应的component组件
        let component = null
        if (this.$router.mode === 'history') {
          // 找到跳转的路由地址对应的路由组件,这里依赖了data.current。故当current的值发生变化时,会触发router-view的重新渲染
          component = self.routerMap[self.data.current]
        } else {
          // hash模式下时,截图#后面的地址作为path路径,然后再去路由表中匹配对应的组件
          const path = self.data.current.slice(1, self.data.current.length)
          component = self.routerMap[path]
        }
        // 渲染对应的组件
        return h(component)
    }
  })
}

// 解析路由表,得到路由与组件的映射关系
createRouteMap (routes, parentPath) {
    if (routes && routes.length && routes.length > 0) {
      routes.forEach((item) => {
        let cPath = ''
        if (parentPath) {
          cPath = `${parentPath}/${item.path}`
        } else {
          cPath = item.path
        }
        this.routerMap[cPath] = item.component
        if (item.children && item.children.length > 0) {
          this.createRouteMap(item.children, item.path)
        }
      })
    }
}

这里有几点需要说明下,

  1. 注册router-link组件时,如果是hash模式,我们可以直接通过修改location.hash的值去改变浏览器地址栏中的地址,因为带#的是hash地址,不会触发浏览器向服务器去发送请求。
  2. 但是 如果是history模式时,我们不可以直接通过修改location.pathname或者直接location.href去改变地址栏中的路由地址,因为这样会导致浏览器直接向web服务器去发送请求。因为,此时,我们需要借助html5的window.history对象中的pushState方法,去改变浏览器地址栏中的路由地址
3.5 注册load事件

注册load事件,实现首次加载或者浏览器重新加载页面时,能监听到,从而去改变data.current 的值,从而触发router-view的重新更新

constructor(options) {
	// 记录当前的路由模式,如果没传,就默认hash模式
	this.mode = options.mode || 'hash'

	// 实现routerMap(用于存储路由与组件的映射关系)
    this.routerMap = {}
    this.createRouteMap(options.routes || [])
    
	// 定义一个响应式对象,当后续current的值发生变化时,vue可以监测到
    this.data = _Vue.observable({
      current: this.mode === 'history' ? '/' : '#/' // 存放当前url地址
    })

	// 注册全局组件
	this.initComponent()

	// 注册事件
	this.initEvent()
}

// 注册相关事件
initEvent () {
    if (this.mode === 'history') {
      // 用户首次打开时,加载当前路由对应的组件
      window.addEventListener('load', () => {
        // 获取浏览器路由地址
        const path = location.pathname ? location.pathname : '/'
        // 将路由地址赋值给data.current,从而触发router-view的重新渲染
        this.data.current = path
        history.pushState({}, '', path)
      })
    } else {
      // 页面首次加载时,加载当前路由对应的组件
      window.addEventListener('load', () => {
        // 页面加载时,如果没有hash符,添加hash符
        location.hash = location.hash || '/'
        this.data.current = location.hash
      })
    }
}

到这一步,我们就已经可以实现,点击router-link链接时,可以跳转对应的路由页面了。以及,手动修改路由地址,然后再刷新浏览器时,可以加载对应的路由页面了

但其实,到这里,还会有点问题。

当我们点击浏览器的前进和后退按钮时,我们会发现,浏览器地址栏中的路由地址发生变化了,但是,我们的页面却没有发生变化。这是为什么
大家想想,我们需要页面发生变化,是不是要data.current的值发生了变化,vue才能监测到变化啊,router-view才能重新渲染对应的路由组件啊
而当浏览器点前进,后退时,是浏览器去帮我们改变了地址栏中的路由地址,这时,我们的data.current的值是不是还没有发生改变啊,所以,我们的视图肯定就不会更新了
所以,我们是不是需要一个事件,来监听浏览器地址栏的变化啊。
此时,我们需要用到两个事件,popstate事件以及hashchange事件

  • popstate事件
    在history模式下,当浏览器的历史发生变化时,会触发该事件
  • hashchange事件
    在hash模式下,只要浏览器地址栏中的hash地址发生了变化,就会触发该事件

所以,我们可以在这两个事件中,去获取到最新的路由地址,然后赋值给data.current,那么就会触发router-view的重新渲染

话不多说,上代码

// 注册相关事件
initEvent () {
    if (this.mode === 'history') {
      /**
       * @description popstate事件,当浏览器历史发生变化时,会触发该事件,主要用于处理当浏览器点击前进后退时,
       * 触发该事件,去改变current变量的值,从而触发对应组件的重新渲染,从而改变页面
       * @ps history模式下,浏览器历史发生变化才会触发popstate事件
       */

      // 用户首次打开时,加载当前路由对应的组件
      window.addEventListener('load', () => {
        const path = location.pathname ? location.pathname : '/'
        this.data.current = path
      })
      
      window.addEventListener('popstate', () => {
        this.data.current = location.pathname
      })
    } else {
      /**
       * @description hashchange事件,当浏览器url的hash地址发生变化时,会触发该事件,主要用于处理当浏览器url的hash地址发生变化时
       * 触发该事件,去改变current变量的值,从而触发对应组件的重新渲染,从而改变页面
       * @ps hash模式下,浏览器url的hash发生变化时才会触发hashchange事件
       */

      // 页面首次加载时,加载当前路由对应的组件
      window.addEventListener('load', () => {
        // 页面加载时,如果没有hash符,添加hash符
        location.hash = location.hash || '/'
        this.data.current = location.hash
      })
      
      window.addEventListener('hashchange', () => {
        this.data.current = location.hash
      })
    }
}

结尾,附完整代码

到这里,我们的代码算是都写完了,下面贴上全部完整代码

/**
 * @description 自定义router
 * @author chendada
 */

let _Vue = null
export default class VueRouter {
  static install (Vue) {
    // 1. 判断是否注册过,如果已注册过,不再注册
    // 2. 缓存当前的Vue构造函数
    // 3. 挂载$router到vue实例下,利用混入,混入到每个组件的实例中

    if (VueRouter.install.installed) {
      return
    }
    VueRouter.install.installed = true

    _Vue = Vue

    // Vue.prototype.$router = this.$options.router
    // 为什么不能直接在Vue的原型对象上挂载$router呢,因为在当前函数中,this指向的是并不是vue实例,故this.$options是不存在的
    // 故我们通过混入的形式,在vue实例的beforeCreate生命周期中去给vue原型添加$router
    Vue.mixin({
      beforeCreate () {
        if (_Vue.prototype.$router) {
          return
        }
        _Vue.prototype.$router = this.$options.router
      }
    })
  }

  constructor (options) {
    // 实现routerMap(用于存储路由与组件的映射关系)
    this.routerMap = {}
    this.createRouteMap(options.routes || [])

    // 记录当前路由模式,如果不传模式,默认给history模式
    this.mode = options.mode || 'history'

    // 定义一个响应式对象
    this.data = _Vue.observable({
      current: this.mode === 'history' ? '/' : '#/' // 存放当前url地址
    })

    // 初始化公用组件
    this.initComponents()

    // 注册相关事件
    this.initEvent()
  }

  initComponents () {
    // 初始化router-link组件
    this.initLink()

    // 初始化router-view组件
    this.initView()
  }

  // 注册相关事件
  initEvent () {
    if (this.mode === 'history') {
      /**
       * @description popstate事件,当浏览器历史发生变化时,会触发该事件,主要用于处理当浏览器点击前进后退时,
       * 触发该事件,去改变current变量的值,从而触发对应组件的重新渲染,从而改变页面
       * @ps history模式下,浏览器历史发生变化才会触发popstate事件
       */

      // 用户首次打开时,加载当前路由对应的组件
      window.addEventListener('load', () => {
        const path = location.pathname ? location.pathname : '/'
        this.data.current = path
      })
      
      window.addEventListener('popstate', () => {
        this.data.current = location.pathname
      })
    } else {
      /**
       * @description hashchange事件,当浏览器url的hash地址发生变化时,会触发该事件,主要用于处理当浏览器url的hash地址发生变化时
       * 触发该事件,去改变current变量的值,从而触发对应组件的重新渲染,从而改变页面
       * @ps hash模式下,浏览器url的hash发生变化时才会触发hashchange事件
       */

      // 页面首次加载时,加载当前路由对应的组件
      window.addEventListener('load', () => {
        // 页面加载时,如果没有hash符,添加hash符
        location.hash = location.hash || '/'
        this.data.current = location.hash
      })
      
      window.addEventListener('hashchange', () => {
        this.data.current = location.hash
      })
    }
  }

  // 注册router-link组件
  initLink () {
    _Vue.component('router-link', {
      props: {
        to: String
      },
      render (h) {
        return h('a', {
          attrs: { href: this.to },
          on: {
            click: this.locationHref
          }
        }, [this.$slots.default])
      },
      methods: {
        locationHref (e) {
          if (this.$router.mode === 'history') {
            /**
             * @description pushState用于改变浏览器跳转地址
             * 参数有3个 第一个参数:一个对象,后续触发popState事件时,传给popState的事件的事件对象
             * 第二个参数:是title,网页标题
             * 第三个参数:需要跳转的url地址
             */
            history.pushState({}, '', this.to)

            // 更新data下的current变量的值(该变量用于记录当前url地址,当url发生变化时,需要改变这个变量的值)
            // 因current是响应式数据,故当值发生变化时,会触发对应组件的重新渲染,从而当url发生变化时,页面也会发生变化
            this.$router.data.current = this.to
          } else {
            window.location.hash = `#${this.to}`
            this.$router.data.current = `#${this.to}`
          }

          // 阻止a标签默认事件,这里需要阻止a标签的href跳转,因为a标签的href跳转是会让浏览器直接向服务器去发送请求的
          e.preventDefault()
        }
      }
    })
  }

  // 注册router-view组件
  initView () {
    const self = this
    _Vue.component('router-view', {
      render (h) {
        // 从路由表中获取当前path对应的component组件
        let component = null
        if (this.$router.mode === 'history') {
          component = self.routerMap[self.data.current]
        } else {
          // hash模式下时,截取#后面的地址作为path路径,然后再去路由表中匹配对应的组件
          const path = self.data.current.slice(1, self.data.current.length)
          component = self.routerMap[path]
        }
        // 渲染对应的组件
        return h(component)
      }
    })
  }

  // 解析路由表,得到路由与组件的映射关系
  createRouteMap (routes, parentPath) {
    if (routes && routes.length && routes.length > 0) {
      routes.forEach((item) => {
        let cPath = ''
        if (parentPath) {
          cPath = `${parentPath}/${item.path}`
        } else {
          cPath = item.path
        }
        this.routerMap[cPath] = item.component
        if (item.children && item.children.length > 0) {
          this.createRouteMap(item.children, cPath)
        }
      })
    }
  }
}

好了,一个简单的vueRouter,我们就写到这了。各位多多指教

  • 14
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值