10分钟教你 手写 Vue-Router

Posted X可乐

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了10分钟教你 手写 Vue-Router相关的知识,希望对你有一定的参考价值。

Vue-Router 原理

Hash 模式

  • URL 中 # 后面的内容作为路径地址
  • 监听 hashchange 事件
  • 根据当前路由地址找到对应组件重新渲染

History 模式

  • 通过 history.pushState() 方法改变地址栏
  • 监听 popstate 事件
  • 根据当前路由地址找到对应组件重新渲染

手写 Router

下列方法皆为拆解方法,最终会重组在一起

分析

回顾核心代码

Vue.use(VueRouter) // Vue.use 内部传入对象时,会调用对象的 install 方法
// 创建路由对象
const router = new VueRouter(
    routes: [
         name: 'home', path: '/', component: homeComponent 
    ]
)
// 创建 Vue 实例,注册 router 对象
new Vue(
    router,
    render: h => h(App)
).$mount('#app')
  • Vue.use 内部传入对象时,会调用对象的 install 方法,所以我们就来先处理一下 install 方法

install 方法

  • install 接收两个参数
    • vue 的构造函数
    • 可选的选项对象(我们这里没用到,所以不传递)
  • 方法内部分为 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。
      2. title 网页的标题。
      3. 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) 	// 接收参数,减少与外界联系
    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
          ,
		  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()
		  
	  
      // template: '<a :href="to"><slot></slot></a>'
    )
    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
	  )
  


以上是关于10分钟教你 手写 Vue-Router的主要内容,如果未能解决你的问题,请参考以下文章

10分钟教你 手写 Vue-Router

10分钟教你 手写 Vue-Router

10分钟教你看懂mongodb的npm包

10分钟教你如何hack掉Java编译器

实用 | 10分钟教你通过网页点灯

10分钟教你写一个数据库