VueRouter实现原理

Hash模式

  • URL中#后面的内容作为路径地址(改变#后面的内容,并不会向服务器发起请求,只会添加到历史记录中)
  • 监听hashchange事件
  • 根据当前路由地址找到对应组件重新渲染

History模式

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

使用时的核心代码

Vue.use(VueRouter)
// Vue.use()
// 注册插件
// Vue.use() 可以传入函数和对象
// 1. 传入函数,Vue.use会直接调用这个函数
// 2. 传入对象,Vue.use会调用对象的install方法

// 由此推断,我们可以使用类实现VueRouter,添加静态方法install
const router = new VueRouter({
  routes: [
    { name: 'home', path: '/', component: homeComponent }
  ]
})

// main.js
// 插件vue实例,注册router对象
new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

实现 类图

三个属性
  • options 记录构造函数传入的对象,传入的路由规则
  • data data是一个对象,有一个current属性记录当前路由地址,设置data的目的是需要一个响应式的对象,(因为路由地址发生改变之后,对应的组件要进行相应的更新)使用vue.observable方法
  • routeMap 记录路由地址和组件的关系,会把路由规则解析到routeMap中来(以键值对的方式)
方法
  • 代表对外公开的方法 - 代表静态方法
  • +Constructor(Options): VueRouter
  • +init(): void 把需要初始化的方法集合起来一起执行
  • +initEvent():void 注册popstate事件, 监听浏览器历史的变化
  • +createRouteMap(): void 初始化routeMap属性的,把构造函数中传入的规则转换成键值对的形式存储到routeMap(对象)中,键是路由的对象,值是组件的地址
  • +initComponents(Vue): void 用来创建routerLink,routerview组件的
  • -install(Vue): void 静态方法,用于vue.use注册插件时,调用

Vue的构建版本

  • 运行时版:不带编译器,不支持template模板,需要打包的时候提前编译,组件可以直接写render函数(使用render创建虚拟dom,渲染到视图)(默认版本)
  • 完整版:包含运行时和编译器,体积比运行时版大10k左右,程序运行的时候把模板转换成render函数,(性能不如运行时版本)
  • Vuecli默认使用运行时的vue,因为效率更高
运行时使用template报错,解决办法:

1. 使用完整版本的Vue (带有编译器等,可以使用template)
module.exports = defineConfig({
  runtimeCompiler: true // 就会加载完整版本的vue
})

2. 使用运行时的Vue(需要自己写render函数)

为什么单文件组件可以使用template呢,因为打包的过程中把template编译成了render函数,这叫预编译
所以我们使用运行时的vue版本的时候,直接写render函数

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

// 相当于
render(h) {
  // h 帮我们创建虚拟dom
  // 接收三个参数,1. 选择器 2.给dom对象设置属性,通过attrs设置 3. slot
  return h('a', {
    // 设置dom对象的属性
    attrs: {
      href: this.to // 就是to的值
    }
  },
  [this.$slots.default]) // 获取默认插槽的内容设置到a标签中
}

出现问题

  • 点击浏览器的前进后退,组件不会切换
原因:
  • popstate,当历史发送变化的时候触发,当我们调用pushState和replaceState的时候,不会触发popstate
解决办法:
  • 注册popstate事件,在初始化的时候执行

完整代码

let _Vue = null // 记录Vue的构造函数
export default class VueRouter {
    // 静态方法,Vue.use()时调用
    static install(Vue) {
        // 1. 判断当前插件是否已经被安装
        if (VueRouter.install.installed) {
            return // 表示插件已经安装
        }
        VueRouter.install.installed = true

        // 2. 把vue的构造函数记录到全局变量中(在静态方法中接收了vue的构造函数,但是在实例中还要使用vue的构造函数,比如创建routerlink。routerview组件等)
        _Vue = Vue

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

        // 因为install是静态方法,调用的时候是VueRouter.install,所以this 谁调用指向谁,此时指向的是VueRouter而不是实例
        // 从选项中获取router
        // _Vue.prototype.$router = this.$options.router

        // 应该在能够获取到vue实例的时候写
        // 混入(所有Vue的实例中都会有,组件中也有)
        _Vue.mixin({
            beforeCreate() {
                // 此时的this,就是vue的实例
                // 挂载$router只需要执行一次,而混入会执行很多次,所以需要判断一下,如果是vue组件的话就不执行了,vue实例的时候执行
                // 只需要判断当前实例的$options选项中是否有router这个属性
                // 因为只有Vue的$options中才有router这个属性,而组件的选项中是没有这个属性的
              
                // $options代表的是new Vue({}),{}里面的东西就是options  https://cn.vuejs.org/v2/api/#%E9%80%89%E9%A1%B9-%E6%95%B0%E6%8D%AE
                if (this.$options.router) {
                    // 代表是Vue实例
                    _Vue.prototype.$router = this.$options.router

                    // 初始化中调用 先找到router对象(此时相当于找到这个类,然后执行类下面的方法),然后调用init方法
                    this.$options.router.init()
                }
            }
        })
    }

    constructor(options) {
        this.options = options // 记录构造函数传入的对象,传入的路由规则
        this.routeMap = {} // 把options传入的路由规则解析出来,存储到routeMap中,键是路由组件,值是路由地址
        this.data = _Vue.observable({
            current: '/'
        }) // 创建响应式对象,可用于计算属性和方法中 记录当前路由地址
    }

    // 把下面两个方法整合
    init() {
        this.createRouteMap()
        this.initComponents(_Vue)
        this.initEvent()
    }

    // 把构造函数中传过来的选项规则,转换成键值对的方式,存储到routeMap中,键是路由组件,值是路由地址,发生变化时,根据地址找到对应的组件,渲染到视图上
    createRouteMap() {
        // 作用:遍历所有路由规则,把路由规则解析成键值对的形式,存储到routeMap中
        this.options.routes.forEach(route => {
            this.routeMap[route.path] = route.component
        })
    }

    // 可传可不传,因为已经记录了,传的目的是 减少方法和外部的依赖
    initComponents(Vue) {
        // 创建router-lint,最终是渲染成一个超链接
        // 路由名字,组件的选项
        Vue.component('router-link', {
            // 接收外部传入的to
            props: {
                to: String
            },
            // 完整版本的Vue(带有编译器)支持template模板
            // 将来会把router-link之间的内容填到slot插槽中
            // template: '<a :href="to"><slot></slot></a>'

            // 使用运行时的Vue,不支持template模板
            // h函数是vue传的
            render(h) {
                // h的作用 帮我们创建虚拟dom
                // h函数接收三个参数,1. 选择器 2.给dom对象设置属性,通过attrs设置 3. slot
                return h('a', {
                        // 设置dom对象的属性
                        attrs: {
                            href: this.to // 就是to的值
                        },
                        on:{
                            //给click注册点击事件,不需要加()
                            click: this.clickHandler
                        }
                    },
                    [this.$slots.default]
                ) // 获取默认插槽的内容设置到a标签中数组中
            },
            methods: {
                clickHandler(e){
                    // 传入的参数,名字,跳转的位置
                    history.pushState({},'',this.to)
                    // 找到路由东西,之前注册插件的时候,已经把$router挂载到了Vue构造函数的原型属性上,所以所有的实例都能访问到$router
                    this.$router.data.current = this.to
                    e.preventDefault() // 阻止默认事件
                }
            }
        })

        const self = this // 此时的this是VueRouter的实例,在h函数中拿不到VueRouter的实例,所以在外部标识一下
        
        // 创建router-view, 相当于一个占位符,在组件内部,根据当前路由地址,获取到路由组件,并渲染到router-view位置
        Vue.component('router-view', {
            render(h) {
                // 1. 找到当前路由的地址
                // 2. 根据当前地址去routeMap中找到对应的组件
                // 3. 调用h函数帮我们转换成虚拟dom,返回,h函数可以直接把一个组件转换成虚拟dom

                // 此时的this,是在render方法内,已经不是VueRouter的实例了
                // self.data.current
               const component = self.routeMap[self.data.current]

               // 不让超链接跳转,不希望向服务端发,给超链接注册点击事件,不让他跳转,修改地址栏中的地址
               // 使用history.pushState,不会向服务器发送请求,修改地址栏中的地址,把当前地址记录到current,
                // h函数帮我们创建虚拟dom,返回虚拟dom
                return h(component)
            }
        })
    }

    // popstate是当历史发送变化的时候触发,调用pushState,replaceState的时候是不会触发的
    // 注册popstate事件
    initEvent(){
        window.addEventListener('popstate',() => {
            // 把当前地址栏的地址取出来,/后面的
            // 此时的this是initEvent,也就是当前VueRouter的对象
            this.data.current = window.location.pathname
        })
    }
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值