从零开始实现一个vue-router插件

1、回顾一下官方vue-router插件的使用

要想本身实现一个vue-router插件,就必须 先了解一下vue-router插件的基本使用,咱们在使用vue-router的时候,一般会定义一个router.js文件,里面主要就是干了如下几件事:

引入vue-router模块html

import Router from 'vue-router'

② 引入Vue并使用vue-router,即所谓的安装vue-router插件vue

import Vue from 'vue'
Vue.use(Router)

建立路由对象并对外暴露
配置路由对象,即在建立路由对象的时候传递一个options参数,里面主要包括mode(路由模式)、routes(路由表)vue-router

export default new Router({
    mode: 'history',
    routes: [
        {
          path: '/',
          name: 'home',
          component: Home
        },
        {
          path: '/about',
          name: 'about',
          component: About
        }
    ]
});
固然,还有最后一步就是在main.js中 引入router.js对外暴露的路由对象,而且将该路由对象 配置到Vue项目的根实例上,至此,就能够在项目中使用vue-router了,所谓使用路由,就是 在页面中使用<router-link>和<router-view>组件来实现路由的跳转和跳转内容的显示。而<router-link>和<router-view>组件这两个组件是 vue-router提供的只有通过安装路由以后,才能识别这两个组件,不然会报错,提示 Unknown custom element: <router-link> - did you register the component correctly?,由于vue-router内部会经过Vue.component()方法进行 全局注册这两个组件。

2、理解路由的两种模式

路由有两种模式,一种是 hash路由,即以 #号加路径名的方式,将路由添加到 浏览器url地址末尾;另外一种就是 history路由,就是经过 浏览器提供的history api实现路由的跳转,能够实现直接将路由路径追加到 浏览器域名末尾。而且这两种方式,都是 路径变化,即 浏览器url地址变化,可是 页面不会跟着刷新,因此vue-router中要作的事,就是 监听浏览器路径即路由的变化,而后动态渲染对应的路由组件,能够经过 双向绑定机制实现,后面会具体讲。咱们先来简单了解一下这两种路由的使用方式

① hash路由
// index.htmlvuex

<a href="#/home">Home</a>
<a href="#/about">About</a>
<div id="content"></div> <!--显示路由内容区域-->
当用户点击上面的这两个连接,就会在浏览器 url地址末尾追加上"#/home"或者"#/about",但此时页面并无刷新,因此咱们要监听hash路由的变化,浏览器提供了一个hashchange事件,咱们能够经过该事件实现路由显示区内容的变化
window.addEventListener("hashchange", () => {
   content.innerHTML = location.hash.slice(1); // 监听到hash值变化后更新路由显示区内容
});

② history路由api

history路由,要实现路由地址的变化,那么须要用到history提供的 pushState(state, title, path)方法,从该方法名能够知道,第一个参数为 state对象, 能够为null;第二个参数为 title字符串,即路由title,暂时无具体做用,后续可能会有用, 能够为null;第三个参数才是有用的,即 路由要跳转的路径字符串,虽然 前两个参数均可觉得null,可是咱们为了给State进行标识,能够传递一个state对象, 对象中包含title和path属性分别表示路由的名称和要跳转的路径,这样就能够经过 history.state知道当前路由的状态信息,是哪一个路由了。

// index.html数组

<a onclick="go('/home')">Home</a>
<a onclick="go('/about')">About</a>
<div id="content"></div>
这里没有直接使用href="/home",由于路由的变化要经过pushState()实现,路由变化的时候才能监听到popstate事件,因此这里监听了click事件经过pushState()的方式去更改路由地址
function go(path) {
   history.pushState({title:path.slice(1), path}, path.slice(1), path);
   content.innerHTML = path;
}
window.addEventListener("popstate", () => { // 监听到路由变化后再次调go()方法更新路由显示区内容
   go(location.pathname);
});

3、开始实现本身的vue-router路由插件

① 在路由插件中声明一个VueRouter类,由于咱们使用vue-router的时候,是先引入路由插件,而后经过路由插件去new出一个router对象,因此引入的路由插件是一个类,同时在建立router对象的时候须要传递一个options参数配置对象,有moderoutes等属性配置,如:
// vue-router.js浏览器

class VueRouter { // VueRouter其实是一个function,也能够看作是一个对象
    constructor(options) {
        this.mode = options.mode || '';
        this.routes = options.routes || [];
    }
    static install(Vue) { // 添加一个静态的install方法,该方法执行的时候会传入Vue构造函数
        // 这里主要是给全部Vue实例添加一些属性等操做
    }
}
export default VueRouter;
上面在VueRouter类中添加了一个 静态的install方法,之因此是静态的,是由于咱们使用路由插件的时候传递给Vue.use()方法的参数是导出的这个VueRouter类,而 use方法执行的时候会调用传递给其参数的install方法,即调用 VueRouter.install(),因此install是静态方法,因此若是路由插件导出的是一个对象,那么 这个对象上也必需要有一个install()方法
// 如下导出插件对象的方式也能够,好比,vuex就是导出的插件对象
class VueRouter {}
const install = () => {}
export default { // 导出插件对象
    VueRouter,
    install // 导出的插件对象中必需要有一个install方法
}

// 使用的时候也要进行相应的变化,如:函数

import rt from "./vue-router"
Vue.use(rt);
const router = new rt.VueRouter({});

② 建立路由表对象,咱们要根据路径的变化动态渲染出相应的组件,因此为了方便,咱们须要构造一个路由表对象,其属性为路径属性值为组件,这样当路径发生变化的时候,咱们能够直接经过路由表对象获取到对应的组件并渲染出来this

this.routesMap = this.createMap(this.routes); // 给VueRouter类添加一个routesMap属性,用于保存构建的路由表对象
createMap(routes) { // 经过传递进来的routes构建路由表对象
    return routes.reduce((result, current) => {
        result[current.path] = current.component;
        return result;
    }, {});
}
这里构造路由表对象使用到了reduce方法,reduce方法是一个用于 实现累加操做的方法,array.reduce(function(total, currentValue, currentIndex, arr), initialValue),当传入了initialValue,那么total就会等于initialValue的值,currentValue就是数组的第一个元素,接着下一轮循环,会把函数的返回值当作total传入,数组的第二个元素当作currentValue传入,一直循环直到数组元素遍历完毕。

保存当前路由,咱们能够建立一个对象用于专门保存当前路由信息,咱们只须要去更改当前路由信息,就能够动态渲染出相应的路由视图了,如:url

class CurrentRoute { // 建立一个类,专门用于保存当前路由信息,同时方便在当前路由对象上添加属性和方法
    constructor() {
        this.path = null; // 添加一个path属性保存当前路由的路径
    }
}
this.currentRoute = new CurrentRoute();// 给VueRouter添加一个currentRoute属性,保存当前路由信息对象

执行init()方法进行路由初始化,就是根据当前url地址进行判断,若是页面一加载什么路径都没有,那么就跳转到"/"首页,若是路径变化则保存当前路由信息,如:

init() { // 初始化路由信息
        if (this.mode === "hash") { // 若是是hash路由
            if (location.hash) { // 若是页面一加载的时候就有hash值
                this.currentRoute.path = location.hash.slice(1); // 保存当前路由的路径
            } else { // 若是页面一加载的时候没有hash值
                location.hash = "/"; // 跳转到首页"/",即在url地址末尾添加"#/"
            }
            window.addEventListener("hashchange", () => { // 监听浏览器地址栏hash值变化
                this.currentRoute.path = location.hash.slice(1); // hash改变,一样更新当前路由信息
            });
        } else { // 若是是history路由
            if (location.pathname) { // 若是页面一加载的时候就有pathname值
                this.currentRoute.path = location.pathname; // 保存当前路由的路径
            } else { // 若是页面一加载的时候没有pathname值
                location.pathname = "/"; // 跳转到首页"/",即在域名地址末尾添加"/"
            }
            window.addEventListener("popstate", () => { // 监听点击浏览器前进或后退按钮事件
                this.currentRoute.path = location.pathname; 
            });
        }
    }
须要注意的是, history.pushState()方法不会触发popstate事件只有点击浏览器前进或后退按钮才会触发popstate事件,可是只要浏览器地址栏hash值变化就会触发hashchange事件

在每一个Vue实例上都添加上$router和$route属性

咱们在使用vue-router的时候, 每一个实例上均可以经过this.$router和this.$route获取到对应的路由对象和当前路由对象,因此咱们须要使用Vue的 mixin()方法在每一个实例上混入$router和$route,咱们在第①步的时候还遗留了install()方法的具体实现,咱们能够在执行install()方法的时候 混入一个beforeCreate钩子函数,mixin混入的方法若是和vue实例上的方法 同名并不会覆盖,而是将同名的方法 放到一个数组中,而且mixin中混入的方法在数组的最前面,即 mixin中混入的方法先执行,这样 每一个实例建立的时候都会执行该beforeCreate(),那么咱们能够在这里将$router和$route混入到每一个实例上,如:
static install(Vue) { // install方法执行的时候会传入Vue构造函数
        Vue.mixin({ // 调用Vue的mixin方法在每一个Vue实例上添加一个beforeCreated钩子
            beforeCreate () {
                if (this.$options && this.$options.router) { // 若是是根组件,那么其options上就会有router属性
                    this._router = this.$options.router;
                } else { // 非根组件
                    this._router = this.$parent && this.$parent._router; // 从其父组件上获取router
                }
                Object.defineProperty(this, "$router", { // 给每一个Vue实例添加$router属性
                    get() { // 返回VueRouter实例对象
                        return this._router;
                    }
                });
                Object.defineProperty(this, "$route", { // 给每一个Vue实例添加$route属性
                    get() {
                        return { // 返回一个包含当前路由信息的对象
                            path: this._router.currentRoute.path
                        }
                    }
                });
            }
        });
    }
在使用路由插件的时候, 会在根实例上注入一个router属性,因此若是this.$options有router的就是根实例,即 main.js中建立的那个Vue实例,因为Vue组件的建立顺序是 由外到内的,也就是说 根组件-->子组件 --> 孙组件 -->...,因此渲染的时候能够 依次从其父组件上获取到父组件上保存的_router实例,从而保存到当前组件的_router上。

注册router-link和router-view组件

咱们须要在install()方法执行的同时,经过Vue在全局上注册router-link和router-view这两个组件,可使用 Vue.component()方法全局注册。
static install(Vue) {
    Vue.component("router-link", { // 全局注册router-link组件
        props: {
            to: {
                type: String,
                default: "/"
            }
        },
        methods: {
          handleClick(e) {
              if (this._router.mode === "history") { // 若是是history路由
                  history.pushState({}, null, this.to); //经过pushState()方法更新浏览器地址栏路径,不会刷新页面
                  this._router.currentRoute.path = this.to; // 点击连接后更新当前路由路径
                  e.preventDefault(); // 阻止<a>标签的默认行为,防y页面默认跳刷新s页面
              }
          }  
        },
        render() {
            const mode = this._router.mode; // 当前<router-link>组件上也会注入_router属性从而能够获取到路由的mode
            return <a on-click={this.handleClick} href={mode === "hash" ? `#${this.to}` : this.to}>{this.$slots.default}</a>;
        }
    });
    Vue.component("router-view", { // 全局注册router-view组件
        render(h) {
            const currentPath = this._router.currentRoute.path;
            const routesMap = this._router.routesMap;
            return h(routesMap[currentPath]); // 根据路由表传入当前路由路径,获取对应的组件,并渲染
        }
    });
}
上面给<a>标签添加了一个click事件,由于当使用history路由的时候,<a>标签上的href是一个路径,点击后会进行默认跳转到该路径,从而 会刷新页面,因此须要 阻止其默认跳转行为,并 经过history的api进行跳转

添加响应式路由

这里目前有一个问题,就是如今点击<router-link>的连接,仅仅是浏览器地址栏url地址发生了变化,咱们也能够看到点击连接后, 咱们在handleClick事件函数中确实更新了当前路由的path,可是<router-view>中的内容并无跟着变化,由于这个当前路由currentRoute对象里面的数据并非响应式的,因此当前路由变化,视图并不会跟着变化,要想让currentRoute对象中的数据变成响应式的,那么咱们能够经过 Vue.util提供的一个defineReactive()方法,其能够给某个对象添加某个属性,而且 其属性值是响应式的,如:
static install(Vue) { // 在install方法中添加响应式路由,由于install方法会传入Vue
    // 将当前路由对象定义为响应式数据
    Vue.util.defineReactive(this, "current", this._router.currentRoute);
}
这样,this._router.currentRoute这个当前路由对象中的数据就 变成响应式的了, <route-view>组件的render()函数中使用到了this._router.currentRoute,因此其 render()函数就会再次执行从而动态渲染出当前路由

4、总结

至此,已经实现了一个简单的vue-router插件,其核心就是, 监听路由路径变化,而后动态渲染出对应的组件,其完整代码以下:
class CurrentRoute {
    constructor() {
        this.path = null;
    }
}
class VueRouter { // VueRouter其实是一个function,也能够看作是一个对象
    constructor(options) {
        this.mode = options.mode || '';
        this.routes = options.routes || [];
        this.routesMap = this.createMap(this.routes);
        this.currentRoute = new CurrentRoute();
        this.init();
    }
    init() {
        if (this.mode === "hash") { // 若是是hash路由
            if (location.hash) { // 若是页面一加载的时候就有hash值
                this.currentRoute.path = location.hash.slice(1); // 保存当前路由的路径
            } else { // 若是页面一加载的时候没有hash值
                location.hash = "/"; // 跳转到首页"/",即在url地址末尾添加"#/"
            }
            window.addEventListener("hashchange", () => { //监听浏览器地址栏hash值变化
                this.currentRoute.path = location.hash.slice(1); // hash改变,一样更新当前路由信息
            });
        } else { // 若是是history路由
            if (location.pathname) { // 若是页面一加载的时候就有pathname值
                this.currentRoute.path = location.pathname; // 保存当前路由的路径
            } else { // 若是页面一加载的时候没有pathname值
                location.pathname = "/"; // 跳转到首页"/",即在域名地址末尾添加"/"
            }
            window.addEventListener("popstate", () => { // 监听点击浏览器前进或后退按钮事件 
                this.currentRoute.path = location.pathname; // 若是页面一加载就带有pathname,那么就将路径保存到当前路由中
            });
        }
    }
    createMap(routes) {
        return routes.reduce((result, current) => {
            result[current.path] = current.component;
            return result;
        }, {});
    }
    // Vue的use方法会调用插件的install方法,也就是说,若是导出的插件是一个类,那么install就是类的静态方法
    // 若是导出的是一个对象,那么install就是该对象的实例方法
    static install(Vue) { // install方法执行的时候会传入Vue构造函数
        Vue.mixin({ // 调用Vue的mixin方法在每一个Vue实例上添加一个beforeCreated钩子
            beforeCreate () {
                if (this.$options && this.$options.router) { // 若是是根组件,那么其options上就会有router属性
                    this._router = this.$options.router;
                } else { // 非根组件
                    this._router = this.$parent && this.$parent._router; // 从其父组件上获取router
                }
                // 将当前路由对象定义为响应式数据
                Vue.util.defineReactive(this, "current", this._router.currentRoute);
                Object.defineProperty(this, "$router", {
                    get() { // 返回VueRouter实例对象
                        return this._router;
                    }
                });
                Object.defineProperty(this, "$route", {
                    get() {
                        return { // 返回一个包含当前路由信息的对象
                            path: this._router.currentRoute.path
                        }
                    }
                });
            }
        });
        Vue.component("router-link", {
            props: {
                to: {
                    type: String,
                    default: "/"
                }
            },
            methods: {
              handleClick(e) {
                  if (this._router.mode === "history") { // 若是是history路由
                      history.pushState({}, null, this.to); //经过pushState()方法更路径,不会刷新页面
                      this._router.currentRoute.path = this.to; // 更新路径
                      e.preventDefault(); // 阻止<a>标签的默认行为,防y页面默认跳刷新s页面
                  }
              }  
            },
            render() {
                const mode = this._router.mode; // 当前<router-link>组件上也会注入_router属性从而能够获取到路由的mode
                return <a on-click={this.handleClick} href={mode === "hash" ? `#${this.to}` : this.to}>{this.$slots.default}</a>;
            }
        });
        Vue.component("router-view", {
            render(h) {
                const currentPath = this._router.currentRoute.path;
                // const currentPath = this.current.path;
                const routesMap = this._router.routesMap;
                return h(routesMap[currentPath]);
            }
        });
    }
}
export default VueRouter;
相关文章
相关标签/搜索