手撕Vue-router源码实现原理

201 阅读3分钟

1.Vue-Router本质

根据"不同的hash值"或者"不同的路径地址",将不同的内容渲染到router-view中,所以实现VueRouter的核心关键点就在于如何监听"hash"或"路径"的变化,再将不同的内容写到router-view中.

基本使用

<body>
    <!-- <a href="#/home">首页</a>
    <a href="#/about">关于</a>
    <div id="html"></div> -->

    <a onclick="go('/home')">首页</a>
    <a onclick="go('/about')">关于</a>
    <div id="html"></div>
    <script>
        // // 原生监听哈希的变化
        // window.addEventListener('hashchange', () => {
        //     // console.log('当前的hash值发生了变化');
        //     let currentHash = location.hash.slice(1)
        //     // console.log(location.hash);//  #/home
        //     // console.log(currentHash);//  /home
        //     document.querySelector('#html').innerHTML = currentHash
        // })
        // //原生加载完毕就获取哈希
        // window.addEventListener('load', () => {
        //     let currentHash = location.hash.slice(1)
        //     document.querySelector('#html').innerHTML = currentHash
        // })
        function go(path) {
            // console.log(path);
            // history.pushState() 方法向当前浏览器会话的历史堆栈中添加一个状态(state)
            // 参数:对象,标题(但是会被所有浏览器忽略),追加的路径
            history.pushState(null,null,path)
            document.querySelector('#html').innerHTML=path
        }
        // 点击前进后退执行,当活动历史记录条目更改时,将触发popstate事件
        window.addEventListener('popstate', () => {
            // pathname 属性是一个可读可写的字符串,可设置或返回当前 URL 的路径部分
            console.log('点击了前进或者后退',location.pathname);
            document.querySelector('#html').innerHTML=location.pathname
        })

    </script>
</body>

2.提取路由信息

路由是一个插件

router/index.js

import Vue from 'vue'
// import VueRouter from 'vue-router'
import SueRouter from './Sue-Router'
import Home from '../views/Home.vue'

// Vue.use(VueRouter)
Vue.use(SueRouter)
const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

// const router = new VueRouter({
const router = new SueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router

自定义路由js

class SueRouter {
    constructor(options) {
        this.mode = options.mode || 'hash'
        this.routes = options.routes || []
        //提取路由信息
        // 格式:{
        //     '/home':hasOwnMetadata,
        //     '/about':About
        // }
        this.routesMap = this.createRoutesMap()
    }
    createRoutesMap() {
        // map是空对象,route是遍历出来的内容
        return this.routes.reduce((map, route) => {
            // 追加内容,path为key,component为值
            map[route.path] = route.component
            return map
        }, {})
    }
}
SueRouter.install = (Vue, options) => {
}
export default SueRouter

初始化路由信息

创建单独的类用来保存当前路由,初始化默认的路由信息

// ==============================更新==================
// 保存组件注入
class NewRouteInfo {
    constructor() {
        // 保存当前路由
        this.currentPath = null
    }
}
// this.$router对应
class SueRouter {
    constructor(options) {
        this.mode = options.mode || 'hash'
        this.routes = options.routes || []
        //提取路由信息
        // 格式:{
        //     '/home':hasOwnMetadata,
        //     '/about':About
        // }
        this.routesMap = this.createRoutesMap()
        // ======================更新===============
        //    用于赋值
        this.routeInfo = new NewRouteInfo()
        // 初始化默认的路由信息,监听,设置
        this.initDefault()
    }
    // =============================更新=================
    initDefault() {
        // 区分哈希还是history
        if (this.mode === 'hash') {
            if (!location.hash) {
                location.hash = '#/'
            }
            // 加载完毕就保存路由地址
            window.addEventListener('load', () => {
                this.routeInfo.currentPath = location.hash.slice(1)
            })
            //变化的时候保存
            window.addEventListener('hashchange', () => {
                this.routeInfo.currentPath = location.hash.slice(1)
                console.log(this.routeInfo);
            })
        } else {
            if (!location.pathname) {
                location.pathname = '/'
            } // 加载完毕就保存路由地址
            window.addEventListener('load', () => {
                this.routeInfo.currentPath = location.pathname
            })
            //变化的时候保存
            window.addEventListener('popstate', () => {
                this.routeInfo.currentPath = location.pathname
                console.log(this.routeInfo);
            })
        }
    }
    createRoutesMap() {
        // map是空对象,route是遍历出来的内容
        return this.routes.reduce((map, route) => {
            // 追加内容,path为key,component为值
            map[route.path] = route.component
            return map
        }, {})
    }
}
SueRouter.install = (Vue, options) => {
}
export default SueRouter

效果

在这里插入图片描述

添加全局$router属性

在index使用use调用组件时,就会执行install方法,可以在里面添加mixin方法,因为只要创建一个组件就会执行一次mixin方法,这样就可以达到给每个实例都添加$router

// 保存组件注入
class NewRouteInfo {
    constructor() {
        // 保存当前路由
        this.currentPath = null
    }
}
// this.$router对应
class SueRouter {
    constructor(options) {
        this.mode = options.mode || 'hash'
        this.routes = options.routes || []
        //提取路由信息
        // 格式:{
        //     '/home':hasOwnMetadata,
        //     '/about':About
        // }
        this.routesMap = this.createRoutesMap()
        //    用于赋值
        this.routeInfo = new NewRouteInfo()
        // 初始化默认的路由信息,监听,设置
        this.initDefault()
    }
    initDefault() {
        // 区分哈希还是history
        if (this.mode === 'hash') {
            if (!location.hash) {
                location.hash = '#/'
            }
            // 加载完毕就保存路由地址
            window.addEventListener('load', () => {
                this.routeInfo.currentPath = location.hash.slice(1)
            })
            //变化的时候保存
            window.addEventListener('hashchange', () => {
                this.routeInfo.currentPath = location.hash.slice(1)
                console.log(this.routeInfo);
            })
        } else {
            if (!location.pathname) {
                location.pathname = '/'
            } // 加载完毕就保存路由地址
            window.addEventListener('load', () => {
                this.routeInfo.currentPath = location.pathname
            })
            //变化的时候保存
            window.addEventListener('popstate', () => {
                this.routeInfo.currentPath = location.pathname
                console.log(this.routeInfo);
            })
        }
    }
    createRoutesMap() {
        // map是空对象,route是遍历出来的内容
        return this.routes.reduce((map, route) => {
            // 追加内容,path为key,component为值
            map[route.path] = route.component
            return map
        }, {})
    }
}
// ======================更新=========================
SueRouter.install = (Vue, options) => {
    // 给每个实例都添加$router与route属性
    // 在install方法中调用mixin,mixin只要创建组件就会执行
    Vue.mixin({
        // 重写beforeCreate方法
        beforeCreate() {
            // 会先创建根组件,再创建子组件,根组件就是main.js new出来的组件
            // 在创建的时候已经有router,所以只需要将router变成$router即可
            // 如果创建的时候传递了参数
            if (this.$options && this.$options.router) {
                this.$router = this.$options.router
                // console.log(this.$router);
                // console.log(this.$router.routeInfo);
                // route是上面SueRouter创建的实例,而该实例保存在routeInfo当中
                this.$route = this.$router.routeInfo
            } else {
                // 如果在创建的时候没有传递参数,代表是子组件,就将父组件赋值给子组件
                this.$router = this.$parent.$router
                // console.log(this.$router.routeInfo);
                this.$route = this.$router.routeInfo
            }
        }
    })
}
export default SueRouter

效果 在这里插入图片描述

实现router-link

只要使用vue实例,就需要提供两个全局组件给外界使用,而使用时会调用install,所以在install里面可以写全局组件

// 保存组件注入
class NewRouteInfo {
    constructor() {
        // 保存当前路由
        this.currentPath = null
    }
}
// this.$router对应
class SueRouter {
    constructor(options) {
        this.mode = options.mode || 'hash'
        this.routes = options.routes || []
        //提取路由信息
        // 格式:{
        //     '/home':hasOwnMetadata,
        //     '/about':About
        // }
        this.routesMap = this.createRoutesMap()
        //    用于赋值
        this.routeInfo = new NewRouteInfo()
        // 初始化默认的路由信息,监听,设置
        this.initDefault()
    }
    initDefault() {
        // 区分哈希还是history
        if (this.mode === 'hash') {
            if (!location.hash) {
                location.hash = '#/'
            }
            // 加载完毕就保存路由地址
            window.addEventListener('load', () => {
                this.routeInfo.currentPath = location.hash.slice(1)
            })
            //变化的时候保存
            window.addEventListener('hashchange', () => {
                this.routeInfo.currentPath = location.hash.slice(1)
                console.log(this.routeInfo);
            })
        } else {
            if (!location.pathname) {
                location.pathname = '/'
            } // 加载完毕就保存路由地址
            window.addEventListener('load', () => {
                this.routeInfo.currentPath = location.pathname
            })
            //变化的时候保存
            window.addEventListener('popstate', () => {
                this.routeInfo.currentPath = location.pathname
                console.log(this.routeInfo);
            })
        }
    }
    createRoutesMap() {
        // map是空对象,route是遍历出来的内容
        return this.routes.reduce((map, route) => {
            // 追加内容,path为key,component为值
            map[route.path] = route.component
            return map
        }, {})
    }
}
SueRouter.install = (Vue, options) => {
    // 给每个实例都添加$router与route属性
    // 在install方法中调用mixin,mixin只要创建组件就会执行
    Vue.mixin({
        // 重写beforeCreate方法
        beforeCreate() {
            // 会先创建根组件,再创建子组件,根组件就是main.js new出来的组件
            // 在创建的时候已经有router,所以只需要将router变成$router即可
            // 如果创建的时候传递了参数
            if (this.$options && this.$options.router) {
                this.$router = this.$options.router
                // console.log(this.$router);
                // console.log(this.$router.routeInfo);
                // route是上面SueRouter创建的实例,而该实例保存在routeInfo当中
                this.$route = this.$router.routeInfo
            } else {
                // 如果在创建的时候没有传递参数,代表是子组件,就将父组件赋值给子组件
                this.$router = this.$parent.$router
                // console.log(this.$router.routeInfo);
                this.$route = this.$router.routeInfo
            }
        }
    })
    // ===============================更新===================================
    // 只要外界使用了Vue-Router,那么就必须提供两个自定义的组件给外界使用
    // 只要外界通过Vue.use注册了Vue-Router,就代表外界使用了Vue-Router
    Vue.component('router-link', {
        // 外界会通过"to"来传递参数,子组件可以通过props接收父组件传递的参数
        props: {
            to: String
        },
        render() {
            // 不能直接修改to
            let path = this.to
            if (this._self.$router.mode === 'hash') {
                path = "#" + path
            }
            // 返回的是a标签,代表外界使用router-link就是a标签
            // 再把to的参数放到a标签
            return <a href={path}>{this.$slots.default}</a>
        }
    })
}
export default SueRouter

效果 在这里插入图片描述

实现router-view

注册router-view全局组件给外界使用

// 保存组件注入
class NewRouteInfo {
    constructor() {
        // 保存当前路由
        this.currentPath = null
    }
}
// this.$router对应
class SueRouter {
    constructor(options) {
        this.mode = options.mode || 'hash'
        this.routes = options.routes || []
        //提取路由信息
        // 格式:{
        //     '/home':hasOwnMetadata,
        //     '/about':About
        // }
        this.routesMap = this.createRoutesMap()
        //    用于赋值
        this.routeInfo = new NewRouteInfo()
        // 初始化默认的路由信息,监听,设置
        this.initDefault()
    }
    initDefault() {
        // 区分哈希还是history
        if (this.mode === 'hash') {
            if (!location.hash) {
                location.hash = '#/'
            }
            // 加载完毕就保存路由地址
            window.addEventListener('load', () => {
                this.routeInfo.currentPath = location.hash.slice(1)
            })
            //变化的时候保存
            window.addEventListener('hashchange', () => {
                this.routeInfo.currentPath = location.hash.slice(1)
                console.log(this.routeInfo);
            })
        } else {
            if (!location.pathname) {
                location.pathname = '/'
            } // 加载完毕就保存路由地址
            window.addEventListener('load', () => {
                this.routeInfo.currentPath = location.pathname
            })
            //变化的时候保存
            window.addEventListener('popstate', () => {
                this.routeInfo.currentPath = location.pathname
                console.log(this.routeInfo);
            })
        }
    }
    createRoutesMap() {
        // map是空对象,route是遍历出来的内容
        return this.routes.reduce((map, route) => {
            // 追加内容,path为key,component为值
            map[route.path] = route.component
            return map
        }, {})
    }
}
SueRouter.install = (Vue, options) => {
    // 给每个实例都添加$router与route属性
    // 在install方法中调用mixin,mixin只要创建组件就会执行
    Vue.mixin({
        // 重写beforeCreate方法
        beforeCreate() {
            // 会先创建根组件,再创建子组件,根组件就是main.js new出来的组件
            // 在创建的时候已经有router,所以只需要将router变成$router即可
            // 如果创建的时候传递了参数
            if (this.$options && this.$options.router) {
                this.$router = this.$options.router
                // console.log(this.$router);
                // console.log(this.$router.routeInfo);
                // route是上面SueRouter创建的实例,而该实例保存在routeInfo当中
                this.$route = this.$router.routeInfo
                // =====================更新===============================
                // 变成双向数据绑定
                Vue.util.defineReactive(this,'xxx',this.$router)
            } else {
                // 如果在创建的时候没有传递参数,代表是子组件,就将父组件赋值给子组件
                this.$router = this.$parent.$router
                // console.log(this.$router.routeInfo);
                this.$route = this.$router.routeInfo
            }
        }
    })
    // 只要外界使用了Vue-Router,那么就必须提供两个自定义的组件给外界使用
    // 只要外界通过Vue.use注册了Vue-Router,就代表外界使用了Vue-Router
    Vue.component('router-link', {
        // 外界会通过"to"来传递参数,子组件可以通过props接收父组件传递的参数
        props: {
            to: String
        },
        render() {
            // 不能直接修改to
            let path = this.to
            if (this._self.$router.mode === 'hash') {
                path = "#" + path
            }
            // 返回的是a标签,代表外界使用router-link就是a标签
            // 再把to的参数放到a标签
            return <a href={path}>{this.$slots.default}</a>
        }
    })
    // =========================更新==================================
    // 注册全局组件给外界使用
    Vue.component('router-view', {
        // 根据当前的路由地址,取出对应的组件,并将组件渲染到router-view
        // 注意点:渲染的时候会先渲染组件,所有组件渲染完时才渲染load,导致无法渲染对应的组件
        // 解决:只要把route变为双向绑定
        render(h) {
            // 调用渲染函数,把我们渲染的组件传递过去
            // 根据路由地址与路由信息去取出对应的组件
            let routesMap=this._self.$router.routesMap
            let currentPath=this._self.$route.currentPath
            let currentComponent=routesMap[currentPath]
            return h(currentComponent)
        }
    })
}
export default SueRouter

效果

在这里插入图片描述