分析vue-router原理并手动实现一个VueRouter

vue-router原理

前置知识

  • 插件
  • 混入
  • Vue.observable()
  • 插槽
  • render函数
  • 运行时和完整版vue

vue-router 使用的核心代码

import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const router = new VueRouter({
    routes:[
        {
            path:'/',
            component:'Index'
        }
    ]
})

//--------------------------------
import router from './router'
new Vue({ 
    router,
    store,
    render: h => h(App)
}).$mount('#app')


VueRouter类图–描述类中所有成员

在这里插入图片描述

options

options记录构造函数中传入的对象,如routes记录我们的 路由规则

data

data是一个对象,里面有一个属性current,用来记录当前的路由地址 ,此处设置data对象的目的是我们需要设计一个响应式的对象,也就是data是一个响应式的对象,当路由地址发生变化的时候对应的组件要进行更新 ,那如何把data对象设置成响应式的呢?我们可以调用Vue.observable()方法

routeMap

routeMap是个对象,用了记录路由和组件之间的对应关系,将来我们会把路由规则解析到routeMap里面来

Constructor(Options):VueRouter

构造函数初始化传入的属性

install(Vue):void ===> 静态方法

install是一个静态的方法,用来实现vue的插件机制

init():void

init方法会调用initEvent、createRouteMap、initComponents三个方法 ,初始化分三个方法实现

initEvent():void

initEvent方法是用来注册popState事件,用来监听浏览器历史的变化

createRouteMap():void

createRouteMap方法是用来初始化routeMap属性的,他把构造函数中传入的路由规则转换成键值对的形式,存储到routeMap里面来,routeMap就是一个对象,键就是路由地址,值就是路由组件 ,我们在router-view组件中会使用routeMap

initComponents(Vue):void

initComponents方法用来创建router-link和router-view两个组件的

VueRouter——install

export default class VueRouter {
    static install (Vue){
        
    }
}
install干了三件事
  • 因为VueRouter走的是插件机制,我们要做的第一件事是判断这个插件是否已经安装,如果已经安装了就进行第二件事,
  • 第二件事就是把Vue的构造函数计入到全局变量中来,因为我们当前的install方法是一个静态方法,在这个静态方法中我们接收了一个参数即vue的构造函数,将来我们再VueRouter的一些实例中还要使用这个构造函数,比如说我们在创建router-link和router-view两个组件的时候,使用Vue.component来创建,所以我们需要把这个构造函数给记录下来,记录到全局变量中;
  • 第三件事就是把创建实例时创建的router对象注入到所有的route实例上,我们之前使用的this.$router就是在这个时候注入的,并且我们所有的组件都是vue的实例
第一件事–判断插件是否安装

在静态方法install创建一个属性installed判断当前插件是否被安装

export default class VueRouter {
   static install (Vue){
       if(VueRouter.install.installed){
       //已安装
           return
       }
       //没有安装,立即安装
       //VueRouter.install.installed = true
       
   }
}
第二件事–将vue的构造函数记录到全局变量
//在全局声明一个全局变量_vue
let _Vue = null
export default class VueRouter {
   static install (Vue){
       //1.判断是否安装
       if(VueRouter.install.installed){
       //已安装
           return
       }
       //没有安装,立即安装
       VueRouter.install.installed = true
       //2.将vue的构造函数记录到全局变量
       _Vue = Vue
       //3.创建实例的时候把vue传入的router对象注入到Vue实例中 
       我们要把router对象注入到所有的vue实例上,我们所有的组件都是vue实例 ,我们想要所有的实例共享一个成员,那我们可以想到应该把它设置到构造函数的原型上
       //这里我们使用到了混入,在插件里面我们可以给所有的实例混入一个选项,在这个选项里面我们可以设置一个beforeCreate,在beforeCreate这个钩子函数中 我们就可以获取到Vue实例,然后在它的原型上注入$router这个属性
        _Vue.mixin({
            beforeCreate(){
                //因为每个组件都会执行一个beforeCreate,所以我们要判断一下如果是组件就不执行了
                //我们只需要判断当前这个实例中是否有$options这个属性就可以了,因为只有vue的$options这个属性才有router这个属性,而我们组件的选项中是没有router这个属性的
                if(this.$options.router){
                    _Vue.prototype.$router = this.$options.router
                }
            }
        })
   }
}

VueRouter——构造函数

回顾类图
在这里插入图片描述

构造函数传入一个options参数,返回一个VueRouter对象,在构造函数中我们初始化了options、data、routeMap三个属性,data是一个响应式的对象,因为data中要存储我们当前的路由地址,当路由变化的时候要自动加载组件,所以data需要设置成响应式的对象

//在全局声明一个全局变量_vue
let _Vue = null
export default class VueRouter {
   static install (Vue){
       if(VueRouter.install.installed){
           return
       }
       VueRouter.install.installed = true
       _Vue = Vue
        _Vue.mixin({
            beforeCreate(){
                if(this.$options.router){
                    _Vue.prototype.$router = this.$options.router
                }
            }
        })
   }
   //创建构造函数
   constructor(options){
       //初始化三个属性
       this.options = options  //用来记录我们传入的选项
       this.routeMap = {}
       //routeMap是一个对象,将来把options中传入的routes这个路由规则解析出来,存储到routeMap里面来,routeMap对象是一个键值对的形式 ,我们的键存储的是路由地址,值就是路由组件,将来在router-view这个组件里面会根据当前的路由地址来routeMap里面找到对应的组件,把它渲染到我们的浏览器中 
       this.data = _Vue.observable({
           current:'/'
       })
       //data是一个响应式的对象,它里面有一个属性current用来当前我们的路由地址,默认情况下,我们当前的路由地址就是'/',就是根目录,那我们怎么创建一个响应式的对象呢?使用_Vue.observable()方法创建响应式对象,可以直接用在渲染函数或者计算属性里面,在observable里面传入一个对象,它内部会把这个对象转换成一个响应式的对象 ,在这个对象里面我们需要创建一个属性叫做current,current的作用是用来存储我们当前的路由地址,默认情况下是'/'
   }
}

VueRouter——createRouteMap

回顾类图
在这里插入图片描述

createRouteMap方法的作用是把我们构造函数中传入的routes,也就是传入的路由规则,转换成键值对的形式存入到routeMap里面来,那么routeMap里面的键就是路由地址,里面的值就是我们这个地址所对应的组件,将来在路由地址发生变化的时候,可以很容易的根据这个地址在routeMap里面找到对应的组件并把它渲染到视图上来

//在全局声明一个全局变量_vue
 let _Vue = null
export default class VueRouter {
    static install (Vue){
        if(VueRouter.install.installed){
            return
        }
        VueRouter.isntall.installed = true
        _Vue = Vue
         _Vue.mixin({
             beforeCreate(){
                 if(this.$options.router){
                     _Vue.prototype.$router = this.$options.router
                 }
             }
         })
    }
    //创建构造函数
    constructor(options){
        //初始化三个属性
        this.options = options  //用来记录我们传入的选项
        this.routeMap = {}
       
        this.data = _Vue.observable({
            current:'/'
        })
       
    }
    createRouteMap(){
        //遍历所有的路由规则把路由规则解析成键值对的形式存储到routeMap里
        this.options.routes.forEach(route=>{
            this.routeMap[route.path] = route.component
        })
    }
}

VueRouter——initComponents

回顾类图
在这里插入图片描述

创建router-link组件
<router-link to="/">首页</router-link>
首先要具备两个要素,一个to属性,一个首页的文本 的slot
//在全局声明一个全局变量_vue
let _Vue = null
export default class VueRouter {
   static install (Vue){
       if(VueRouter.install.installed){
           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初始化方法去调用createRouteMap和initComponents方法
   init(){
       this.createRouteMap()
       this.initComponents(_Vue)
   }
   //创建createRouteMap方法
   createRouteMap(){
       //遍历所有的路由规则把路由规则解析成键值对的形式存储到routeMap里
       this.options.routes.forEach(route=>{
           this.routeMap[route.path] = route.component
       })
   }
   //创建initComponents方法
   initComponents(Vue){
       //传入Vue是为了减少对全局变量_Vue的依赖
       //创建router-link组件
       Vue.component('router-link',{
           //设置组件的选项
           props:{
               to:String
           },
           template:'<a :href="to"><slot></slot></a>'
       })
   }
}
修改vue版本,实现对自定义vueRouter的调用
Vue的构建版本
  • 运行时版:不支持template模板,需要打包的时候提前编译,把template变成render函数
  • 完整版:包含运行时和编译器,体积比运行时大版本大10KB左右,程序运行时把模板转换成render函数,性能是不如运行时版本
完整版解决vue-cli使用自定义VueRouter报错的问题

如何从运行时vue切换成完整版vue?

//vue.config.js
module.exports = {
    runtimeComplier:true,//开启完整版编译器 
}
runTime版本解决vue-cli使用自定义VueRouter报错的问题

使用render函数解决问题,runtime版本需要自行编写render函数来解决问题

//创建initComponents方法
    initComponents(Vue){
        //传入Vue是为了减少对全局变量_Vue的依赖
        //创建router-link组件
        Vue.component('router-link',{
            //设置组件的选项
            props:{
                to:String
            },
            render(h){
                //h函数的作用是创建虚拟DOM
                //h函数是Vue中的,包含三个参数:
                //1.创建的元素对应的选择器,
                //2.给标签设置的属性,对象的形式
                //3.生成的元素汇总的子元素,数组的形式
                return h('a',{
                    attsr:{//DOM对象的属性可以在这个属性里写
                        href:this.to//不需要拼#,因为使用的history模式
                        
                    }
                },
                [//生成的元素汇总的子元素,数组的形式,可以是标签
                     this.$slots.default//获取默认的插槽这样写  
                ])
            }
        })
    }
创建router-view组件
//在全局声明一个全局变量_vue
let _Vue = null
export default class VueRouter {
   static install (Vue){
       if(VueRouter.install.installed){
           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初始化方法去调用createRouteMap和initComponents方法
   init(){
       this.createRouteMap()
       this.initComponents(_Vue)
   }
   //创建createRouteMap方法
   createRouteMap(){
       //遍历所有的路由规则把路由规则解析成键值对的形式存储到routeMap里
       this.options.routes.forEach(route=>{
           this.routeMap[route.path] = route.component
       })
   }
   //创建initComponents方法
   initComponents(Vue){
       //传入Vue是为了减少对全局变量_Vue的依赖
       //创建router-link组件
       Vue.component('router-link',{
           //设置组件的选项
           props:{
               to:String
           },
           //template:'<a :href="to"><slot></slot></a>'
           render(h){
               //h函数的作用是创建虚拟DOM
               //h函数是Vue中的,包含三个参数:
               //1.创建的元素对应的选择器,
               //2.给标签设置的属性,对象的形式
               //3.生成的元素汇总的子元素,数组的形式
               return h('a',{
                   attsr:{//DOM对象的属性可以在这个属性里写
                       href:this.to//不需要拼#,因为使用的history模式
                       
                   }
               },
               [//生成的元素汇总的子元素,数组的形式,可以是标签
                    this.$slots.default//获取默认的插槽这样写  
               ])
           }
       })
       const self = this
       //创建router-view组件
       Vue.component('router-view',{
           render(h){
               //思路:在render函数内部我们先要找到当前路由地址
               //,根据路由地址在routeMap对象中找到这个当前路由地址对应的组件,
               //然后再调用h函数帮我们把这个组件转换成虚拟DOM直接返回,
               //那么h函数其实还可以直接把一个组件转换成虚拟DOM
               
              //获取当前路由地址对应的组件
              const component = self.routeMap[self.data.current]
              return h(component)
           }
       })
   }
}
客户端处理路径变化的操作,怎样做到路径变化不刷新页面呢?

当我们点击router-link标签(a标签)的时候回调用超链接,这个时候会发送服务器请求,但是单页面应用中不希望发送服务器请求,所以我希望给这个超链接注册一个点击事件,取消这个超链接后继的执行,不让它继续跳转,同时还要做到把地址栏的URL改成href中的值,我们既要改地址栏的地址又不想要请求服务器,这个时候我们想到了一个方法,也就是history.pushState()方法,history的pushState方法会改变地址栏的路径,但是 不会向服务器发送请求,还会把这次路径记录到历史中来,注意pushState仅仅是帮我们把地址栏中的地址改变了,router-view显示的内容还继续处理,所以我们还有一个步骤就是把当前的路由地址记录到data.current中来 ,current这个属性的作用就是记录当前的路由地址的,地址记录下来以后别忘了,我们的this.data这个属性是响应式数据 ,也就是当数据发生变化的时候会自动更新视图。我们通过observable创建的这个响应式对象,可以用在我们组建的渲染函数也就是render函数中

//创建initComponents方法
    initComponents(Vue){
        //传入Vue是为了减少对全局变量_Vue的依赖
        //创建router-link组件
        Vue.component('router-link',{
            //设置组件的选项
            props:{
                to:String
            },
            render(h){
                //h函数的作用是创建虚拟DOM
                //h函数是Vue中的,包含三个参数:
                //1.创建的元素对应的选择器,
                //2.给标签设置的属性,对象的形式
                //3.生成的元素汇总的子元素,数组的形式
                return h('a',{
                    attsr:{//DOM对象的属性可以在这个属性里写
                        href:this.to//不需要拼#,因为使用的history模式
                        
                    },
                    on:{//创建事件
                        click:this.clickHandler
                    },
                },
                [//生成的元素汇总的子元素,数组的形式,可以是标签
                     this.$slots.default//获取默认的插槽这样写  
                ])
            },
            methods:{
                clickHandler(e){
                    //思路:这里我们要考虑两个件事,第一件事就是调用pushState方法来改变我们的地址栏,
                    //但是它不会向服务器发送请求
                    //history.pushState方法第一个参数是state对象是将来触发popState的时候,传给popState这个事件的事件对象的参数;
                    //第二个参数是网页的标题
                    //第三个参数是当前超链接要跳转的地址,而我们要跳转的地址在this.to这里面
                    history.pushState({},'',this.to)
                    //加载对应路径地址的组件
                    //首先获取点击router-link后的当前路由地址(响应式的)
                    this.$router.data.current = this.to
                    //当current改变以后,对应的组件就会加载到页面中来
                    e.preventDefault()//阻止默认是的向服务器发送请求的事件
                }
            }
        })
    }

VueRouter——initEvent

当点击前进和后退按钮的时候,地址栏发生了变化,但是页面router-view对应的组件却没有发生变化,也就是当我们前进或后退的时候是没有路由地址对应的组件的,因为我们没有代码去处理这件事情,下面我们来解决这样的问题:

//在全局声明一个全局变量_vue
let _Vue = null
export default class VueRouter {
   static install (Vue){
       if(VueRouter.install.installed){
           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初始化方法去调用createRouteMap和initComponents方法
   init(){
       this.createRouteMap()
       this.initComponents(_Vue)
       this.initEvent()
   }
   //创建createRouteMap方法
   createRouteMap(){
       //遍历所有的路由规则把路由规则解析成键值对的形式存储到routeMap里
       this.options.routes.forEach(route=>{
           this.routeMap[route.path] = route.component
       })
   }
   //创建initComponents方法
   initComponents(Vue){
       //传入Vue是为了减少对全局变量_Vue的依赖
       //创建router-link组件
       Vue.component('router-link',{
           //设置组件的选项
           props:{
               to:String
           },
           render(h){
               //h函数的作用是创建虚拟DOM
               //h函数是Vue中的,包含三个参数:
               //1.创建的元素对应的选择器,
               //2.给标签设置的属性,对象的形式
               //3.生成的元素汇总的子元素,数组的形式
               return h('a',{
                   attsr:{//DOM对象的属性可以在这个属性里写
                       href:this.to//不需要拼#,因为使用的history模式
                       
                   },
                   on:{//创建事件
                       click:this.clickHandler
                   },
               },
               [//生成的元素汇总的子元素,数组的形式,可以是标签
                    this.$slots.default//获取默认的插槽这样写  
               ])
           },
           methods:{
               clickHandler(e){
                   //思路:这里我们要考虑两个件事,第一件事就是调用pushState方法来改变我们的地址栏,
                   //但是它不会向服务器发送请求
                   //history.pushState方法第一个参数是state对象是将来触发popState的时候,传给popState这个事件的事件对象的参数;
                   //第二个参数是网页的标题
                   //第三个参数是当前超链接要跳转的地址,而我们要跳转的地址在this.to这里面
                   history.pushState({},'',this.to)
                   //加载对应路径地址的组件
                   //首先获取点击router-link后的当前路由地址(响应式的)
                   this.$router.data.current = this.to
                   //当current改变以后,对应的组件就会加载到页面中来
                   e.preventDefault()//阻止默认是的向服务器发送请求的事件
               }
           }
       })
       const self = this
       //创建router-view组件
       Vue.component('router-view',{
           render(h){
               //思路:在render函数内部我们先要找到当前路由地址
               //,根据路由地址在routeMap对象中找到这个当前路由地址对应的组件,
               //然后再调用h函数帮我们把这个组件转换成虚拟DOM直接返回,
               //那么h函数其实还可以直接把一个组件转换成虚拟DOM
               
              //获取当前路由地址对应的组件
              const component = self.routeMap[self.data.current]
              return h(component)
           }
       })
   }
   
   initEvent(){
       //这个方法的作用是注册popstate事件
       window.addEventLinstener('popstate',()=>{
           //在这个事件处理函数中我们需要做的是把当前地址栏中的地址取出来,我们要的仅仅是地址栏的路径部分,我们要把这个路径部分作为路由地址存储到this.data.current里来
           this.data.current = window.location.pathname 
       })
   }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值