VueRouter3的hash模式简易实现

311 阅读2分钟

VueRouter3的hash模式主要原理:

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

在router文件夹下新建myrouter.js

router文件夹下的index.js中,将引入的vuerouter更改为myrouter.js

import Vue from 'vue'
import VueRouter from './my-router'
import HomeView from '../views/HomeView.vue'

处理install方法

Vue.use(VueRouter)

const routes = [
    {
        path: '/',
        name: 'home',
        component: HomeView
    },
    {
        path: '/about',
        name: 'about',
        component: () => import('../views/AboutView.vue')
    }
]

const router = new VueRouter({
    routes
})

export default router

Vue.use内部传入对象时,需要提供一个install方法,并且最后默认导出

export default class VueRouter {
    // 给Vue添加全局功能 全局注册组件 prototype上加属性
    static install (Vue) {
        ...
    }
}

全局注册组件

全局注册router-linkrouter-view两个组件,并且通过render函数去渲染它们

render函数

主要是用来创建虚拟DOM,参数h则通过vue传递

h函数

一共接收3个参数:

  1. 节点类型 - 创建这个元素的选择器
  2. 节点属性 - 给这个标签设置属性
  3. 内部节点 - 生成元素的子元素
export default class VueRouter {
    static install (Vue) {
        Vue.component('router-link', {
            render(h) {
                return h('a', 'router-link')
            }
        })
        Vue.component('router-view', {
            render(h) {
                return h('div', 'router-view')
            }
        })
    }
}

router-link组件

  1. 配置h函数的第二个参数 - 给a标签设置属性
  2. 默认插槽处理
Vue.component('router-link', {
    props: {
        to: String
    },
    render(h) {
        return h('a',{
            attrs: {
                href: '#' + this.to, // 因为是hash模式拼接#
            }
        }, this.$slots.default) // 第三个参数传入默认插槽
    }
})

router-view组件

直接在h函数中传入组件

import HomeView from '../views/HomeView.vue'
class VueRouter {
    ...
        Vue.component('router-view', {
            render(h) {
                return h(HomeView)
            }
        })
}        

constructor构造函数

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

constructor (options) {
    this.options = options
    console.log('options', options)
}      

Snipaste_2023-07-14_21-30-59.png

监听 hashchange 处理逻辑

  1. 在构造函数中,监听hashchange变化
  2. 设置一个current属性,初始值为这个地址的hash值
constructor (options) {
    this.options = options
    this.current = window.location.hash.slice(1) || '/'
    window.addEventListener('hashchange', () => {
        const hash = window.location.hash
        this.current = hash
    })
}      

VueRouter实例挂载在Vue的原型上

原型上添加 $router 把router实例挂载在prototype的$router

Snipaste_2023-07-14_21-45-37.png

  • 在main.js中,App根组件实例化时,传入了router实例
  • 通过生命周期钩子函数beforeCreate拿到router实例
static install (Vue) {
    // 通过全局混入Vue.mixin来执行beforeCreate函数
    Vue.mixin({
        beforeCreate() {
            // if判断 当只有进入根组件的时候 才可以获取到router实例
            if(this.$options.router) {
                Vue.prototype.$router = this.$options.router
            }
        }
    })
}

嵌套路由实现

  1. 编写二级路由
  2. 配置二级路由出口,在AboutView.vue
{
    path: '/about',
    name: 'about',
    component: () => import('../views/AboutView.vue'),
    children: [
        {
            path: '/about/info',
            component: {
                render(h) {
                    return h('h1', 'About Info页面')
                }
            }
        }
    ]
<template>
    <div class="about">
        <h1>This is an about page</h1>
        <!-- 新写的二级路由出口 -->
        <router-view />
    </div>
</template>

实现depth

核心是去标记深度

render(h) {
    // 标记这个组件是routerView
    this.$vnode.data.routerView = true
    // 默认深度为0
    let depth = 0
    // parent 寻找当前父组件
    let parent = this.$parent
    // while条件一直网上找父组件 直到找不到为止
    while(parent) {
        const vnodeData = parent.$vnode ? parent.$vnode.data : {}
        // 如果是你标记的routerView组件 层级++
        if(vnodeData.routerView) {
            depth++
        }
        // 父组件的父组件 如果存在的话会再次进入while循环
        parent = parent.$parent
        console.log(depth)
    }
}

实现matched

将matched处理成响应式

  1. 不能使用Vue.set 因为set的语法第一个参数本身就是需要响应式的对象
  2. 不能使用Object.defineProperty,因为这个是数据劫持,无法实现响应式的功能
// 定义一个变量_Vue
let _Vue

export default class VueRouter {
    constructor (options) {
        this.options = options
        this.current = window.location.hash.slice(1) || '/'
    
        // defineReactive(对象, 对象想要响应式的键值, 初始值)
        _Vue.util.defineReactive(this, 'matched', [])
        // 新增调用match方法
        this.match()
    
        window.addEventListener('hashchange', () => {
            const hash = window.location.hash
            this.current = hash
        })
    }
    
    static install (Vue) {
        _Vue = Vue
        ...
    }
}

提供match方法

  1. 处理matched添加路由规则 this.matched.push
  2. 递归 route.children
match(routes) {
    // 如果不传routes 默认使用options的routes
    routes = routes || this.options.routes
    for (const route of routes) {
        // 如果路径为首页,直接向matched数组里添加路由规则
        if(route.path === '/' && this.current === '/') {
            this.matched.push(route)
            return
        }
        // current: /about/info
        // matched: [{about路由规则}, {aboutinfo路由规则}]
        // route.path: /about        /about/info
        // 如果路径不为首页,当前路由包含路由规则
        if(route.path !== '/' && this.current.includes(route.path)) {
            this.matched.push(route)
            // 如果路由规则属性有children
            if(route.children) {
                // 递归
                this.match(route.children)
            }
        }
    }
}

根据depth匹配对应的路由规则

render(h) {
    ...
    // console.log(depth)
    // 根据路由规则 url匹配的页面组件去渲染
    let component = null
    const route = this.$router.matched[depth]
    if(route) {
        component = route.component
    }
    return h(component)
}

hashchange后重置matched数组并且重新调用match匹配规则

切换路由后,无法重新渲染页面组件,需要在hashchange进行重置

    window.addEventListener('hashchange', () => {
      const hash = window.location.hash.slice(1)
      this.current = hash
      // 重置 matched数组清空 并且重新匹配
      this.matched = []
      this.match()
    })

嵌套路由完成