你可能不知道的 Vue-Router 源码那些事

108 阅读2分钟

前言

正如你所了解到的Vue RouterVue.js官方的路由管理器,它可以让构建单页面应用变得更加简单。与之离不开的还有vue-view即路由容器以及路由跳转用到的router-link标签等。

接下来让我们来看下Vue Router是如何实现的?

抛砖引玉

平时我们使用Vue Router的方式一般是在router文件夹下的index.js文件找中先引入,然后在注册,如下:

import Vue from 'vue'
import Router from 'router'

Vue.use(Router)

由上可知:其实Vue Router是一个插件

引入注册之后,就是创建路由实例,并写入路由表,如下:

export default new Router({
    mode: 'hash',
    routes: [...]
})

再之后就是在main.js里把创建的路由实例挂载到Vue实例上,如下:

import Vue from 'vue'
import App from './App'
import router from './router'

new Vue({
    el: '#app',
    router,
    components: { App },
    template: '<App/>'
})

这一步比较重要,就是为了把router挂载全局上,可以让每个组件都可以使用,即Vue.prototype.$router = router

到这里Vue Router的引入、注册和挂载三步就算完成了,瞅一瞅下面这张图:

image.png

问题:
1、router-view是路由出口,是怎么替换内容的?
2、router-linkrouter-viwe为什么可以直接使用?

聪明的小伙伴可能想到了,就是在注册挂载Vue router的时候对这俩组件进行了声明和注册了,具体怎么实现的呢?

解开源码的面纱

现在开始写咱们自己的Vue Router暂且起名为MyRouter吧。既然Vue Router是一个组件,那么我们就要MyRouter中实现一个install方法,大体结构如下:

let Vue;
class MyRouter {
    constructor(options) {
        this.$options = options;
        this.current = '/';
        // 路由映射表
        this.routeMap = {};
        options.routes.forEach(router => {
            this.routeMap[router.path] = router;
        });
    }
}

MyRouter.install = function(_Vue) {
    // 保存构造函数,以便在 MyRouter 里面使用
    Vue = _Vue;
}

export default MyRouter;

问题:怎么才能获取到根实例中的router选项呢?

答案就是每个组件中都混入一个生命周期,来获得router选项,如下:

MyRouter.install = function(_Vue) {
    // 保存构造函数,以便在 MyRouter 里面使用
    Vue = _Vue;
    // 混入的目的:延迟下面的逻辑到router创建完毕并且附加到选项时才执行
    _Vue.mixin({
        beforeCreate() {
            // 获得根实例中的router选项:只有根实例才有此选项
            if (this.$options.router) {
                Vue.prototype.$router = this.$options.router;
            }
        }
    })
}

上面提到:router-linkrouter-view两个组件也是在注册router时声明并注册的,所以接下来我们要在install方法里分别实现这俩组件的注册。

router-link

_Vue.component('router-link',{
    props:{
        to: {
            type: String,
            required: red
        }
    },
    render(h){
        return h('a', {attrs:{href:'#' + this.to}}, this.$slots.default)
    }
})

注意
1、不能使用tamplate方式来注册组件,因为是在运行时版本不存在编译器,所以只能使用render函数的方式。
2、可以用this.$slots.default的方式拿到标签的HTML内容。

router-view

_Vue.component('router-view',{
    render(h){
         const { routeMap, current } = this.$router;
         const component = routeMap[current].component || null;
         return h(component)
    }
})

install 完整代码如下

MyRouter.install = function(_Vue) {
    // 保存构造函数,以便在 MyRouter 里面使用
    Vue = _Vue;
    // 混入的目的:延迟下面的逻辑到router创建完毕并且附加到选项时才执行
    _Vue.mixin({
        beforeCreate() {
            // 获得根实例中的router选项:只有根实例才有此选项
            if (this.$options.router) {
                Vue.prototype.$router = this.$options.router;
            }
        }
    })
    // 注册 router-link 组件
    _Vue.component('router-link', {
        props: {
            to: {
                type: String,
                required: red
            }
        },
        render(h) {
            return h('a', { attrs: { href: '#' + this.to } }, this.$slots.default)
        }
    });
    // 注册 router-view 组件
    _Vue.component('router-view', {
        render(h) {
            const { routeMap, current } = this.$router;
            const component = routeMap[current].component || null;
            return h(component)
        }
    })
}

组件注册完成后就开始检测Url变化了,该怎么检测呢?你可能想到了就是用addEventlistener去监听hashchange事件,如下:

class MyRouter {
    constructor(options) {
        this.$options = options;
        this.current = '/';
        // 路由映射表
        this.routeMap = {};
        options.routes.forEach(router => {
            this.routeMap[router.path] = router;
        });
        // 监听 Url 变化
        window.addEventListener('hashchange', () => {
            this.current = window.location.hash.slice(1);
        })
    }
}

问题:测试我们的代码可以发现页面是出来了,但切换路由时页面并没有切换?

因为Url并没有做到响应式处理,这里需要用到Vue提供的帮助方法库util,如下:

class MyRouter {
    constructor(options) {
        this.$options = options;
        // 需要创建响应式的 curren 属性
        // 这样将来currnt变化的时候,依赖的组件会重新rander
        Vue.util.defineReactive(this, 'current', '/');
        // 监听 Url 变化
        window.addEventListener('hashchange', () => {
            this.current = window.location.hash.slice(1);
        })
    }
}

这时再切换路由,发现可以正常访问页面了~~

问题:我们创建响应式current时给的默认值是'/',也就是说如果当前页面不是'/'而是'/about'的话,一刷新就会跳转至'/'页面,该如何避免呢?

解决起来也很简单,就是再监听一下load事件,如下:

class MyRouter {
    constructor(options) {
        this.$options = options;
        // 路由映射表
        this.routeMap = {};
        options.routes.forEach(router => {
            this.routeMap[router.path] = router;
        });
        // 需要创建响应式的 curren 属性
        // 这样将来currnt变化的时候,依赖的组件会重新rander
        Vue.util.defineReactive(this, 'current', '/');
        // 监听 Url 变化
        window.addEventListener('hashchange', this.onHashChange.bind(this));
        // 重新加载事件
        window.addEventListener('load', this.onHashChange.bind(this));
    }

    onHashChange() {
        this.current = window.location.hash.slice(1);
    }
}

这时再刷新页面就可以正常展示了,nice ~~~ 大概的思路就是这样了,希望对你有帮助,欢迎评论留言 ~~~