Vue全家桶原理剖析之Vue-router篇

Vue-router原理剖析从入门到放弃

  • 第一个问题:vue-router的本质是什么
  • 第二个问题 为什么要在根Vue实例的选项对象中注入router?
  • 第三个问题 为什么将router加入到此处Vue的选项对象后就可以全局使用了呢?
  • 进一步优化代码
  • 进进进一步优化

由于此文主要为介绍Vue-router的原理,就不赘述router的基本用法啦。
tips:以下代码及测试结果均在Vue2.X的环境下测试得出。

第一个问题:vue-router的本质是什么

vue-router是一个插件,包括在使用Vue-cli创建一个Vue项目的过程中,都可以选择当前项目使用或者不使用Vue-router,但是在任何一个使用l路由管理的Vue项目中,在main.js都可以看到如下代码。

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

第二个问题 为什么要在根Vue实例的选项对象中注入router?

答案自然是很简单,在此处将router加入到vue的选项对象后,新建出来的Vue实例的所有子实例都可以使用Vue-router,也就是在全局都可以使用了。那么问题又来了

第三个问题 为什么将router加入到此处Vue的选项对象后就可以全局使用了呢?

让我们带着这两个问题来深入Vue-router的源码吧。

vue-router的主要代码如下所示

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

Vue.use(VueRouter)

const routes = [
  {
     
    path: '/',
    name: 'Home',
    component: Home
  },
  {
     
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  },
  {
     
    path: '/3dview',
    name: 'topo-3d',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/topo-3D.vue')
  }
]

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

export default router

可以看到,此时第二行引用的是Vue官方提供的router,我们将此行注释掉,换成我们自己实现的Xrouter
我们将第二行的

import VueRouter from 'vue-router'

换成

import VueRouter from './x-router'

然后在同级目录下新建x-router.js
对于vue-router的index.js文件的第六行

Vue.use(VueRouter)

前文中已经提到,router是一个插件,我们需要了解的是Vue对于插件的定义是必须要暴露一个install方法,这样才可以在Vue.use中正常使用,不然会报错。现在正式开始从实现一个vue-router来实现一个vue插件。

首先搭一个基本的骨架

// 这里是自己实现的Vue-router
let vue = null;
class myVueRouter{
     
    constructor(options){
     
    }
}
myVueRouter.install = function(_vue){
     
    // vue在执行install的时候,会将Vue的构造函数传递进去
    // 这里我们将_vue保存到变量中,这样我们就可以在上面定义的myVueRouter中去使Vue构造函数了
    vue = _vue;
}

很简单的几行代码,vue在执行install方法时会将vue构造函数传递进去,然后我们将它保存到变量vue中,这样就可以在myVueRouter类中去使用了。
这里可能会有人提出疑问(包括我自己)
既然要使用vue构造函数,为什么一开始直接在第一行中import一个Vue呢?
这样做自然也是可以的,但是插件之所以是插件,自然还是要做到尽可能的轻量化,如果引入vue,那么webpack在打包的时候就必定会额外引入一大堆Vue相关的包,而这完全可以避免的。

好,现在进行下一步。
所有插件的install方法中都会将插件挂载到全局中。
也就是执行

vue.prototype.$router = router;

问题是,我们如何拿到

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

中的router项呢。

思路如下,我们知道,每一个vue组件都是通过new Vue()的构造函数构造出来的,但是只有根组件的构造函数的选项对象options中会传入router配置项。那么,我们只要在每一个组件的构造函数中判断选项对象中有没有router即可。
这里要用到一个日常开发中比较少见的一个api ----- 全局混入传送门
在这里插入图片描述
vue官方文档给出的警告正是我们想要的效果。
思路清晰之后便可以直接写代码了。

myVueRouter.install = function(_vue){
     
    // vue在执行install的时候,会将Vue的构造函数传递进去
    // 这里我们将_vue保存到变量中,这样我们就可以在上面定义的myVueRouter中去使Vue构造函数了
    vue = _vue;
    vue.mixin({
     
        beforeCreate () {
     
            if(this.$options.router){
     
            	// 如果选项对象中含有router选项,那么将$router挂载到全局中
            	// 至于为什么只在根组件的时候才执行这条语句呢?看上文中第二个问题便可获得答案
                vue.prototype.$router = this.$options.router;
            }
        }
    }) 
}

这样配置完我们自定义的vuerouter之后便可以在全局使用了,但是此时打开项目依然会报错如下。
Vue全家桶原理剖析之Vue-router篇_第1张图片
没有关系,这是因为我们没有在自定义的路由插件中定义router-link和router-view的缘故。这正是我们下一步需要实现的功能。

如何实现一个名为router-link,或是router-view的元素呢,很自然的是使用组件的形式。

myVueRouter.install = function (_vue) {
     
    vue = _vue;
    vue.mixin({
     
        beforeCreate() {
     
            if (this.$options.router) {
     
                vue.prototype.$router = this.$options.router;
            }
        }
    })

    vue.component('router-link',{
     
        
    })

    vue.component('router-view',{
     
        
    })
}

此时,需要思考一个问题。在定义这些组件时,能不能使用template呢?

vue.component('router-link',{
     
        template:`这是一个超链接`
    })

这就是vue-router的陷阱了,注意在这个地方是不能使用template语法的。具体原因是在运行程序时主要有两个环境,一个是携带编译器的环境,也就是在浏览器中,它可以实时的将template转换成真实dom,一个是预编译环境,也就是在webpack打包过程中,是无法 识别template语法的。

所以我们只能用渲染函数来编写虚拟dom。
由于渲染函数个人感觉写起来比较麻烦,所以我在这里用JSX来偷个懒。

myVueRouter.install = function (_vue) {
     
    vue = _vue;
    vue.mixin({
     
        beforeCreate() {
     
            if (this.$options.router) {
     
                vue.prototype.$router = this.$options.router;
            }
        }
    })
    vue.component('router-link',{
     
        props:{
     
            to:{
     
                type:String,
            }
        },
        render(){
     
            return <a style='color:#42b983' href={
     '#'+this.to}>{
     this.$slots.default}</a>
        }
    })
    vue.component('router-view',{
     
    })
}

此时再次打开首页,可以看到的是,router-link的功能已经正常了。点击页面链接首页url也可以正常变化。
Vue全家桶原理剖析之Vue-router篇_第2张图片
那么很自然的就知道下一步需要实现的是监听url的变化并渲染对应的组件。
那么在router的构造函数中可以实现监听url变化的逻辑。

class myVueRouter {
     
    constructor(options) {
     
        this.$options = options;
        eventBus = new vue();
        this.currentUrl = '/';
        window.addEventListener('hashchange',function(){
     
            this.currentUrl = window.location.hash.slice(1);
        })
    }
}

可以看到在这里我们将变化后的hash值保存到了this.currentUrl中,所以可以在此构造函数实例化后的对象通过this.currentUrl拿到当前的url值,而在之前我们已经将router挂在到了全局,所以在Vue中可以通过this.$router.currentUrl拿到此值。
有此思路之后便可以直接编写router-view了。

vue.component('router-view',{
     
        render(h){
     
            let currentComp = null;
            this.$router.$options.routes.forEach(route => {
     
                if(route.path === this.$router.currentUrl){
     
                    currentComp = route.component
                }
            })
            return h(currentComp);
        }
    })

可以看到,我们将当前url值和配置项中的比较,然后拿到应该渲染的组件直接挂载。此时页面也应该可以成功展示了。
Vue全家桶原理剖析之Vue-router篇_第3张图片
最后一步,渲染是没有问题的,但是我们并没有做响应式的逻辑。因此,此时点击router-link是无法跳转的。所以最后一步是在当前url更新时通知到router-view。这里可以各展神通,博主用的是一个自定义的发布订阅模式。具体代码如下

let vue = null;


class EventBusClass{
     
    constructor(){
     
        this.eventStore=[]
    }
    emit(type,params=null){
     
        this.eventStore[type](params);
    }
    on(type,callBack){
     
        this.eventStore[type] = callBack;
    }
}

const eventBus = new EventBusClass();



class myVueRouter {
     
    constructor(options) {
     
        this.$options = options;
        this.currentUrl = '/';
        vue.util.defineReactive(this,'currentUrl','/');
        window.addEventListener('hashchange',function(){
     
            this.currentUrl = window.location.hash.slice(1);
            eventBus.emit('bashUpdate',this.currentUrl);
        })
    }
}

myVueRouter.install = function (_vue) {
     
    vue = _vue;
    vue.mixin({
     
        beforeCreate() {
     
            if (this.$options.router) {
     
                vue.prototype.$router = this.$options.router;
            }
        }
    })
    vue.component('router-link',{
     
        props:{
     
            to:{
     
                type:String,
            }
        },
        render(){
     
            return <a style='color:#42b983' href={
     '#'+this.to}>{
     this.$slots.default}</a>
        }
    })
    vue.component('router-view',{
     
        mounted(){
     
            eventBus.on('bashUpdate',(currentUrl)=>{
     
                this.$router.currentUrl = currentUrl;
                this.$forceUpdate();
            })
        },
        render(h){
     
            let currentComp = null;
            this.$router.$options.routes.forEach(route => {
     
                if(route.path === this.$router.currentUrl){
     
                    currentComp = route.component
                }
            })
            return h(currentComp);
        }
    })
}

export default myVueRouter

在这里我杀鸡用牛刀的用了一个发布订阅模式,甚至还用了forceUpdate,但是总归是实现了一个基本的vue-router,其实vue自己提供了将变量变成响应式的方法,但是我使用之后发现视图并没有更新于是就自己实现了一个。其实真正的vue-router提供的方法以及功能肯定是要复杂的多,这里我们仅仅是从router的最基本的功能来阅读源码甚至入手来自己实现一个view-router啦。

进一步优化代码

到现在为止代码是可以正常运行且实现了router的基本功能的,但是可以看到的是,在router-view的渲染函数中,每次url更新之后都要在route里面遍历一次才能找到对应的component。而router配置项又是不变的,所以路由跳转都遍历一次显然是不合理的。所以我们可以将组件和路由之间的对应关系映射到一张表中。改写如下

let vue = null;
// 中央事件总线
class EventBusClass{
     
    constructor(){
     
        this.eventStore=[]
    }
    emit(type,params=null){
     
        this.eventStore[type](params);
    }
    on(type,callBack){
     
        this.eventStore[type] = callBack;
    }
}
const eventBus = new EventBusClass();
class myVueRouter {
     
    constructor(options) {
     
        this.$options = options;
        this.currentUrl = '/';
        window.addEventListener('hashchange',function(){
     
            this.currentUrl = window.location.hash.slice(1);
            eventBus.emit('bashUpdate',this.currentUrl);
        })
        this.componentMap = new Map();
        options.routes.forEach(routeItem => {
     
            this.componentMap.set(routeItem.path,routeItem.component);
        })
    }
}

myVueRouter.install = function (_vue) {
     
    vue = _vue;
    vue.mixin({
     
        beforeCreate() {
     
            if (this.$options.router) {
     
                vue.prototype.$router = this.$options.router;
            }
        }
    })
    vue.component('router-link',{
     
        props:{
     
            to:{
     
                type:String,
            }
        },
        render(){
     
            return <a style='color:#42b983' href={
     '#'+this.to}>{
     this.$slots.default}</a>
        }
    })
    vue.component('router-view',{
     
        mounted(){
     
            eventBus.on('bashUpdate',(currentUrl)=>{
     
                this.$router.currentUrl = currentUrl;
                this.$forceUpdate();
            })
        },
        render(h){
     
            let currentComp = this.$router.componentMap.get(this.$router.currentUrl) || null;
            return h(currentComp);
        }
    })
}

export default myVueRouter

finished!

进进进一步优化

入乡随俗,既然都用了Vue,为啥不直接用它的响应式呢。duck不必自己实现响应式。优优化代码如下。待童鞋消化理解

// 这里是自己实现的Vue-router

let Vue = null;

class myVueRouter{
     
    constructor(options){
     
        this.$options = options;
        this.componentMap = new Map()
        options.routes.forEach(routeItem =>{
     
            this.componentMap.set(routeItem.path,routeItem.component);
        })
        this.activeParams = new Vue({
     
            data () {
     
                return {
     
                    currentUrl:location.hash ? location.hash.slice(1) : '/'
                };
            }
        });
        window.addEventListener('hashchange',() => {
     
            this.activeParams.currentUrl = location.hash.slice(1);
        })
    }
}

myVueRouter.install = function(_Vue){
     
    // vue在执行install的时候,会将Vue的构造函数传递进去
    // 这里我们将_vue保存到变量中,这样我们就可以在上面定义的myVueRouter中去使Vue构造函数了
    Vue = _Vue;
    Vue.mixin({
     
        beforeCreate () {
     
            if(this.$options.router){
     
                Vue.prototype.$router = this.$options.router;
            }
        }
    }) 

    Vue.component('router-link',{
     
        props: {
     
            to:{
     
                type:String
            }
        },
        render() {
     
            return <a href={
     `#${
       this.to}`}>{
     this.$slots.default}</a>
        }
    })

    


    Vue.component('router-view',{
     
        render(h) {
     
            const currentComponent = this.$router.componentMap.get(this.$router.activeParams.currentUrl);
            return h(currentComponent);
        }
    })
}

export default myVueRouter

你可能感兴趣的:(Vue学习之路)