如何手写简易的vue-router插件

1,105 阅读2分钟

我们都知道vue单页应用需要借助vue-router插件来实现页面的跳转,但实现原理如何呢?下面简单的分析下。单页的跳转一般有两种方式:hash变化和history控制。下面以hash变化为例说明:

1. 实现思路

  • 第一步,实现插件的install方法
  • 第二步:定义全局组件router-link和router-view,router-link用于路由跳转,router-view用于匹配组件内容
  • 第三步:监控url的变化,比如监听hashchange或popstate事件
  • 第四步:响应最新的url,借助响应式属性matched来实现,current为路由变化的值,当current改变时,重新匹配路由对应关系到matched数组里,然后触发组件内容更新

2. 代码实现

// rvue-router.js

let Vue;

class Router {
    constructor(options){
        this.$options = options;
        // this.current保存当前页面的hash值
        this.current = window.location.hash.slice(1) || '/';
        // matched数组保存路由与组件的映射表,只保留当前路由和当前路由的嵌套路由
        Vue.util.defineReactive(this, 'matched', []);
        // match方法递归遍历路由表,获得匹配关系的数组,也就是创建新的路由映射表
        this.match();

        // 3. 监控url的变化:监听hashchange或popstate事件
        window.addEventListener('hashchange', this.onHashChange.bind(this));
        // 页面刷新时获取hash值
        window.addEventListener('load', this.onHashChange.bind(this));
    }

    onHashChange(){
        this.current = window.location.hash.slice(1);
        // hash变化时,路由映射表重新匹配
        this.matched = [];
        this.match();
    }

    match(routes){
        routes = routes || this.$options.routes;

        for(const route of routes){
            // 根路由
            if(route.path === '/' && this.current == '/'){
                this.matched.push(route);
                return;
            }

            // 嵌套路由 /main/about
            if(route.path !== '/' && this.current.indexOf(route.path) > -1){
                this.matched.push(route);
                if(route.children){
                    this.match(route.children);
                }
                return;
            }
        }
    }
}

// 1. 作为一个插件:实现VueRouter的install方法
Router.install = function(_Vue){
    // 保存_Vue构造函数,在Router内部使用
    // 避免import Vue时导致打包文件过大,所以通过变量Vue来保存_Vue引用
    Vue = _Vue;
 
    // 挂载$router
    // (入口文件实例化Vue时的根组件)在组件根实例中挂载$router Vue.prototype.$router = router
    // 通过混入的方式,在每个组件生命周期中实现挂载$router
    Vue.mixin({
        beforeCreate() {
            // 确保是根实例的时候才执行,只有根实例组件才有router选项
            if(this.$options.router){
                Vue.prototype.$router = this.$options.router;
            }
        },
    });

    // 2. 实现两个全局组件:router-view用于匹配组件内容,router-link用于跳转
    Vue.component('router-link', {
        props:{
            to: {
                type: String,
                required: true
            }
        },
        render(h) {
            // <a href="#/form">链接</a>
            // <router-link to="/slot" /> 调用方式
            // h(tag, data, children)
            return h('a', {attrs: {href: `#${this.to}`}}, this.$slots.default);
        }
    });
    Vue.component('router-view', {
        render(h){
            // 标记当前router-view的深度,标记自己是router-view组件
            this.$vnode.data.routeView = true;
    
            let depth = 0;
            let parent = this.$parent;
            while(parent){
                const vnodeData = parent.$vnode && parent.$vnode.data;
                // 如果是router-view组件,则记录routeView的深度
                if(vnodeData && vnodeData.routeView){
                    depth ++;
                }
                parent = parent.$parent;
            }
    
            // 4. 响应最新的url:创建一个响应式的属性matched,当它改变时获取对应组件的显示内容
            // mathed保存的是组件路由的映射关系,只涉及当前路由和当前路由的子路由
            // this.$router指当前VueRouter的实例
            const { matched } = this.$router;
            const component = matched[depth] && matched[depth].component;
    
            return h(component);
        }
    });
}

export default Router

3. 使用方法

引入rvue-router插件后,先使用Vue.use(Router)注册插件,调用rvue-router的install方法,然后创建router实例,将路由配置项作为参数传入。

//router.js

import Vue from 'vue'
import Router from 'rvue-router'  // 引入上面的rvue-router.js

import Form from 'components_path/form'

const routes = [{
    path: '/form',
    name: 'form',
    component: Form
}]

// 第一步 应用注册插件
Vue.use(Router);

// 第二步 创建实例
const router = new Router({
    mode: 'history',
    routes  //将路由配置项传入
})

export default router

在入口文件main.js中引入路由配置router.js

// main.js

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

new Vue({
    render: h => h(App),
    router  // Vue.prototype.$router = router
}).$mount('#app')

npm地址:www.npmjs.com/package/rvu…

以上内容为网上学习课程的复习总结。