简单实现VUE-Router

github

vue-router

Vue-routerVue.js官方的路由管理器。

它和Vue.js的核心深度集成,让构建单页面应用变得易如反掌。

安装

vue add router

核心步骤

  • 步骤一:使用vue-router插件
//router.js
import Router from 'vue-router';

/*
* VueRouter是一个插件
*   1)实现并声明两个组件router-view router-link
*   2)install: this.$router.push()
* */
Vue.use(Router);  // 引入插件
  • 步骤二:创建Router实例
// router.js
export default new Router({...})   // 导出Router实例
  • 步骤三:在根组件添加该实例
// main.js
import router from './router';
new Vue({
    router   // 添加到配置项
}).$mount("#app")
  • 步骤四:添加路由视图
<!--  App.vue  -->
<router-view></router-view>
  • 步骤五:导航
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
this.$router.push('/');
this.$router.push('/about')

vue-router简单实现

需求分析

  • 单页面应用程序中,url发生变化时候,不能刷新,显示对应视图
    • hash:#/about
    • History api:/about
  • 根据url显示对应的内容
    • router-view
    • 数据响应式:current变量持有url地址,一旦变化,动态执行render

任务

  • 实现一个插件
    • 实现VueRouter
      • 处理路由选项
      • 监控url变化
      • 响应变化
    • 实现install方法
      • $router注册
      • 两个全局组件

实现

创建新的插件

Vue2.x项目中的src路径下,复制一份router文件,重命名为ou-router

然后在ou-router路径下新建一个ou-vue-router.js文件,并将index.js文件中的VueRouter引入改为ou-vue-router.js

import VueRouter from './ou-vue-router'

同时将main.js中的router引入也修改一下。

import router from './ou-router'

创建Vue插件

关于Vue插件的创建:

  • 可以使用function实现,也可以使用objectclass实现;
  • 要求必须有一个install方法,将来会被Vue.use()使用
let Vue;   // 保存Vue的构造函数,插件中需要用到

class VueRouter {}

/*
* 插件:实现install方法,注册$router
*   参数1是Vue.use()一定会传入
* */
VueRouter.install = function (_Vue) {
    Vue = _Vue;  // 引用构造函数,VueRouter中要使用
}

export default VueRouter;

挂载$router

当我们发现vue-router引入vue的时候,第一次是在router/index.js中使用了Vue.use(Router),在这个时候也就会调用了vue-routerinstall方法;而第二次则是在main.js中,创建根组件实例的时候引入router,即new Vue({router}).$mount("#app")

也就是说,当调用vue-routerinstall方法的时候,项目还没有创建Vue的根组件实例。因此我们需要在vue-routerinstall方法使用全局混入,延迟到router创建完毕才执行挂载$router

let Vue;   // 保存Vue的构造函数,插件中需要用到

class VueRouter {}

/*
* 插件:实现install方法,注册$router
*   参数1是Vue.use()一定会传入
* */
VueRouter.install = function (_Vue) {
    Vue = _Vue;  // 引用构造函数,VueRouter中要使用

    /* 挂载$router */
    /*
    * 全局混入
    *   全局混入的目的是为了延迟下面逻辑到router创建完毕并且附加到选项上时才执行
    * */
    Vue.mixin({
        beforeCreate() {    // 此钩子在每个组件创建实例时都会调用
            /* this.$options即创建Vue实例的第一个参数 */
            if(this.$options.router){   // 只在根组件拥有router选项
                Vue.prototype.$router = this.$options.router;  // vm.$router
            }

        }
    })
}

export default VueRouter;

注册全局组件router-linkrouter-view

首先在install方法中注册两个全局变量。

let Vue; 

class VueRouter {}

VueRouter.install = function (_Vue) {
    Vue = _Vue;

    Vue.mixin({
        ...
    })

    /* 注册全局组件router-link和router-view */
    Vue.component('router-link',{
        render(createElement){
            return createElement('a','router-link');     // 返回虚拟Dom
        }
    });
    Vue.component('router-view',{
        render(createElement){
            return createElement('div','router-view');   // 返回虚拟Dom
        }
    })
}

export default VueRouter;

实现router-link

  • router-view是一个a标签
  • router-viewto属性设置到a标签的herf属性(先默认使用hash方法)
  • 获取router-view的插槽内容,插入a标签中
 Vue.component('router-link', {
        props: {
            to: {
                type: String,
                required: true
            }
        },

        render(createElement) {      // 返回虚拟Dom
            return createElement('a',
                {
                    attrs: {href: '#' + this.to}    // 设置a标签的href属性
                },
                this.$slots.default    // 获取标签插槽内容
            );
        }
    });

实现router-view

router-view实质上根据url的变化,实时响应渲染对应的组件,而createElement函数是可以传入一个组件参数的。

因此,我们不进行渲染任何内容,后面实现监听url变化后,从映射表获取到组件后,再来实现router-view

Vue.component('router-view', {
        render(createElement) {
            let component = null;
            return createElement(component);   // 返回虚拟Dom
        }
    })

监听url变化

我们在VueRouter类的constructor函数中监听url的变化,这里我们默认使用hash方式。

而且,我们需要将存入url的变量设置为响应式数据,这样子当其发生变化的时候,router-viewrender函数才能够再次执行。

class VueRouter {
    /*
    * options:
    *   mode: 'hash'
    *   base: process.env.BASE_URL
    *   routes
    * */
    constructor(options) {
        this.$options = options;

        // 将current设置为响应式数据,即current变化时router-view的render函数能够再次执行
        const initial = window.location.hash.slice(1) || '/';
        Vue.util.defineReactive(this, 'current',initial);

        // 监听hash变化
        window.addEventListener('hashchange', () => {
            this.current = window.location.hash.slice(1);
        })
    }
}

因此,我们可以来实现router-view组件。

render函数中,this.$router指向的是VueRouter创建的实例,因此我们可以通过this.$router.$option.routes获取路由映射表,this.$router.current获取当前路由,然后通过遍历匹配获取组件。

Vue.component('router-view', {
    render(createElement) {
            let component = null;
         // 获取当前路由对应的组件
         const route = this.$router.$options.routes
                .find(route => route.path === this.$router.current);

          if (route) {
                    component = route.component;
          }
          return createElement(component);   // 返回虚拟Dom
         }
})

实现history模式

前面的实现都默认为hash模式,接下来简单实现一下history模式。

首先将监听url的代码优化一下,并判别mode的值来设置current的初始值,而history模式下初始值为window.location.pathname

class VueRouter {
    /*
    * options:
    *   mode: 'hash'
    *   base: process.env.BASE_URL
    *   routes
    * */
    constructor(options) {
        this.$options = options;

        switch (options.mode) {
            case 'hash':
                this.hashModeHandle();
                break;
            case 'history':
                this.historyModeHandle();
        }
    }

    // Hash模式处理
    hashModeHandle() {
        // 将current设置为响应式数据,即current变化时router-view的render函数能够再次执行
        const initial = window.location.hash.slice(1) || '/';
        Vue.util.defineReactive(this, 'current', initial);

        // 监听hash变化
        window.addEventListener('hashchange', () => {
            this.current = window.location.hash.slice(1);
        })
    }

    // History模式处理
    historyModeHandle() {
        const initial = window.location.pathname || '/';
        Vue.util.defineReactive(this, 'current', initial);
    }
}

然后我们来实现history模式下的router-link组件。

history模式下,当我们点击router-link时,即点下a标签时,页面会重新刷新。所以我们需要设置一下其点击事件,取消默认事件,然后通过history.pushState去修改url,然后重设current的值。

Vue.component('router-link', {
    render(createElement) {      // 返回虚拟Dom
        const self = this;
        const route = this.$router.$options.routes
            .find(route => route.path === this.to);
        return createElement('a',
            {
                attrs: {href: this.to},    // 设置a标签的href属性
                on: {
                    click(e) {
                        e.preventDefault();   // 取消a标签的默认事件,即刷新页面
                        history.pushState({}, route.name, self.to);   // 通过history.pushState来改变url
                        self.$router.current = self.to;
                    }
                }
            },
            this.$slots.default    // 获取标签插槽内容
        );
    }
})

最后我们将两种模式的router-link组件进行一个合并。

Vue.component('router-link', {
    props: {
        to: {
            type: String,
            required: true
        }
    },

    render(createElement) {      // 返回虚拟Dom
        if(this.$router.$options.mode === 'hash'){
            return createElement('a',
                {
                    attrs: {href: '#' + this.to}    // 设置a标签的href属性
                },
                this.$slots.default    // 获取标签插槽内容
            );
        }else{
            const self = this;
            const route = this.$router.$options.routes
                .find(route => route.path === this.to);
            return createElement('a',
                {
                    attrs: {href: this.to},    // 设置a标签的href属性
                    on: {
                        click(e) {
                            e.preventDefault();   // 取消a标签的默认事件,即刷新页面
                            history.pushState({}, route.name, self.to);   // 通过history.pushState来改变url
                            self.$router.current = self.to;
                        }
                    }
                },
                this.$slots.default    // 获取标签插槽内容
            );
        }
    }
});
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,636评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,890评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,680评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,766评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,665评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,045评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,515评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,182评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,334评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,274评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,319评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,002评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,599评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,675评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,917评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,309评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,885评论 2 341

推荐阅读更多精彩内容