vue-router学习之使用hash的方式实现一个简单的router插件

402 阅读2分钟

这是我参与更文挑战的第1天,活动详情查看: 更文挑战

学习了vue-router原理,记录一下,方便加深记忆。感谢村长

通过vue-router的使用方式创建测试用例

// 1. 在./frouter/index.js 引入router插件并使用
import FRouter from './frouter.js'
Vue.use(FRouter)

// 2.在./frouter/index.js创建router实例并导出
const routes = [
    {
        path: '/',
        name: 'Home',
        component: Home
    },
    {
        path: '/about',
        name: 'About',
        // 注意此处必须是以函数形式引入
        component: () => import('../views/About.vue'),
        children: [
            {
                path: '/about/test',
                name: 'Test',
                component: {
                    render (h) {
                        return h (
                            'div',
                            null,
                            ['我是嵌套路由啊~']
                        )
                    }
                }
            }
        ]
    }
]
export default new FRouter({ routes })

// 3.在main.js中引入router实例并挂载到Vue实例
import router from './frouter'
new Vue({
    router
}).$mount('#app')

// 4.在App.vue中添加路由导航和路由视图
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link> |
<router-link to="/about/test">嵌套路由</router-link>
<router-view></router-view>

// 5.在About.vue中添加路由视图
<router-view></router-view>

思考实现方式

创建frouter插件

  • 在插件中实现FRouter类
    • 保存并处理router实例传过来的路由配置表
    • 监控路由的变化并保存当前路由的信息
    • 对当前路由做响应式处理,以便在路由变化的时候驱动路由视图进行变化
  • 在插件中实现install方法提供给Vue.use()使用
    • 将$router写入Vue的原型链中,以便在所有组件中通过$router实例进行访问
    • 全局注册router-link组件与router-view组件
  • 新加一个嵌套路由 2021/6/2 17:19
    • 标记当前路由视图的深度
    • 在路由匹配的时候获取深度层级的matched数组
    • 根据深度在matched数组中获取当前路由及组件
// 创建frouter.js
let Vue;
class FRouter {
    constructor (options = {}) {
        this.$options = options
        // this.mapRouter = {}
        // 保存path与route之间的映射关系,避免每次都需要循环查找
        // this.$options.routes.forEach(route => {
            // this.mapRouter[route.path] = route
        // });
        // 获取hash值并截取#后面的值
        // const initialCurrent = window.location.hash.slice(1) || '/'
        this.current = window.location.hash.slice(1) || '/'
        // 使用Vue的工具函数defineReactive对当前路由存放的matched数组做响应式处理
        Vue.util.defineReactive(this, 'matched', [])
        // match方法可以递归遍历路由表,获得当前路由及其存在的子路由的数组,按照深度进行排序
        // 初始化的时候匹配依次
        this.match()
        // 此处的this指的是当前类
        // Vue.util.defineReactive(this, 'current', initialCurrent)
        // 监听hashchange事件并绑定当前类作为上下文以防外界在定时器中访问onHashChange方法时改变this指向
        window.addEventListener('hashchange', this.onHashChange.bind(this))
    }
    onHashChange () {
        this.current = window.location.hash.slice(1)
        // 每次路由变化的时候清空matched数组并重新匹配
        this.matched = []
        this.match()
    }
    match (routes) {
        // 递归,所以传值,如果传了就用传的,没传就用用户定义的路由表
        routes = routes || this.$options.routes
        routes.forEach(route => {
            // 判断循环的路由是否是根路由且当前路由也是根路由
            if (route.path === '/' && this.current === '/') {
                this.matched.push(route)
                return
            }
            // /about 或 /about/test
            if (route.path !== '/' && this.current.indexOf(route.path) !== -1) {
                this.matched.push(route)
                if (route.children) {
                    this.match(route.children)
                }
                return
            }
        })
    }
}
FRouter.install = function (_Vue) {
    // 传入Vue的构造函数方便修改它的原型,绑定$router
    Vue = _Vue; // 将传入的_Vue构造函数保存以便在FRouter类中使用Vue中的工具函数
    // 通过全局混入将router注入到Vue的原型链中
    Vue.mixin({
        // Vue中的所有组件都会进入beforeCreate生命周期
        // 判断只有当前组件实例中传入了router实例才将router实例写入到Vue的原型链中(查看测试用例中的第3点)
        beforeCreate () {
            // 此处的this指的是当前的组件实例
            if (this.$options.router) {
                Vue.prototype.$router = this.$options.router
            }
        }
    })
    // 全局注册router-link组件
    Vue.component('router-link', {
        props: {
            to: {
                type: String,
                required: true
            }
        },
        render (h) {
            // 在runtime运行环境中不能放<template>标签,必须用render函数渲染
            return h ('a', {
                attrs: {
                    href: '#' + this.to
                }
            }, this.$slots.default)
        }
    })
    // 全局注册router-view组件
    Vue.component('router-view', {
        render (h) {
            // 给当前路由视图做标记,标明是router-view
            this.$vnode.data.routerView = true;
            // 定义当前路由深度
            let depth = 0;
            // 定义当前路由的父路由视图
            // 注意:为何不是从当前路由一层一层的循环找子路由?
            // 因为当前只可能显示一个路由(不管它是父路由还是子路由)
            // 如果依次查找子路由,可能会在matched数组中存在多分枝关系
            // eg: a父路由, b子路由,c是b的兄弟路由,d是b的子路由,e是c的子路由,不好处理
            // 而如果从当前路由依次查找父路由,则每次查找都只会查到一个父路由
            // 所以每次重新match,更新matched数组后,可以按照深度来直接查找到当前路由的组件进行渲染
            let parent = this.$parent
            // 依次对父路由进行循环判断,如果是router-view,则当前路由深度+1
            while (parent) {
                // 注意此处必须判断父路由视图是否存在虚拟dom
                const vNodeData = parent.$vnode && parent.$vnode.data
                if (vNodeData && vNodeData.routerView) {
                    depth++
                }
                // 将父路由赋值为祖父路由,再次判断
                parent = parent.$parent
            }
            // 此处的this指的是组件实例,通过vue组件实例访问路由$router实例中的$options与current及mapRouter
            let component = null;
            const route = this.$router.matched[depth]
            if (route) {
                component = route.component
            }
            // const { mapRouter, current } = this.$router
            // const component = mapRouter[current] ? mapRouter[current].component : null
            return h(component)
        }
    })
}
// 导出插件
export default FRouter;