Analysis of Vue router's principle

Posted by jkm4201 on Tue, 08 Feb 2022 22:38:52 +0100


Since this article mainly introduces the principle of Vue router, I won't repeat the basic usage of router.
tips: the following codes and test results are in vue2 Tested in the environment of X.

First question: what is the essence of Vue router

Vue router is a plug-in. In the process of creating a Vue project using Vue cli, you can choose whether the current project uses or does not use Vue router. However, in any Vue project using l route management, in main JS can see the following code.

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

Second, why inject router into the option object of the root Vue instance?

Naturally, the answer is very simple. After adding router to Vue's option object here, all sub instances of the newly created Vue instance can use Vue router, that is, it can be used globally. Then the problem comes again

The third question is why the router can be used globally after being added to Vue's option object here?

Let's go deep into the source code of Vue router with these two questions.

The main codes of Vue router are as follows:

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

As you can see, the second line refers to the router officially provided by Vue. We will comment this line and replace it with our own Xrouter
We'll take the second line

import VueRouter from 'vue-router'

change into

import VueRouter from './x-router'

Then create a new x-router under the same level directory js
For Vue router index Line 6 of JS file

Vue.use(VueRouter)

As mentioned earlier, router is a plug-in. What we need to know is that Vue defines a plug-in by exposing an install method, so that it can be used in Vue Use normally in use, otherwise an error will be reported. Now we officially start to implement a Vue plug-in by implementing a Vue router.

First, build a basic skeleton

// Here is the Vue router implemented by ourselves
let vue = null;
class myVueRouter{
    constructor(options){
    }
}
myVueRouter.install = function(_vue){
    // Vue will pass in the constructor of Vue when executing install
    // Here we will_ Vue is saved to the variable so that we can use the Vue constructor in the myVueRouter defined above
    vue = _vue;
}

Very simple lines of code. vue will pass in the vue constructor when executing the install method, and then we will save it to the variable vue, so that it can be used in the myVueRouter class.
There may be questions here (including myself)
Since you want to use the Vue constructor, why import a Vue directly in the first line at the beginning?
It's natural to do this, but the reason why plug-ins are plug-ins is that they should be lightweight as much as possible. If Vue is introduced, a lot of Vue related packages will be introduced when webpack is packaged, which can be completely avoided.

OK, now go to the next step.
In the install method of all plug-ins, the plug-ins will be mounted in the global.
That is, execution

vue.prototype.$router = router;

The question is, how do we get it

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

The router item in.

The idea is as follows. We know that each Vue component is constructed through the constructor of new Vue(), but only the option object options of the constructor of the root component will pass in the router configuration item. Then, we just need to judge whether there is a router in the option object in the constructor of each component.
Here we need to use an api that is rare in daily development----- Global blend portal

The warning given by vue official documents is exactly what we want.
After clear thinking, you can write code directly.

myVueRouter.install = function(_vue){
    // Vue will pass in the constructor of Vue when executing install
    // Here we will_ Vue is saved to the variable so that we can use the Vue constructor in the myVueRouter defined above
    vue = _vue;
    vue.mixin({
        beforeCreate () {
            if(this.$options.router){
            	// If the option object contains the router option, Mount $router to the global
            	// As for why this statement is executed only at the root component time? Look at the second question above to get the answer
                vue.prototype.$router = this.$options.router;
            }
        }
    }) 
}

After the user-defined item is configured, we can still use the vueter to open the following error message.

It doesn't matter because we didn't define router link and router view in the customized routing plug-in. This is what we need to achieve in the next step.

How to implement an element named router link or router view? It is natural to use the form of components.

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',{
        
    })
}

At this point, we need to think about a problem. Can I use template when defining these components?

vue.component('router-link',{
        template:`<a>This is a hyperlink</a>`
    })

This is the trap of Vue router. Note that template syntax cannot be used here. The specific reason is that there are two environments when running the program. One is the environment with the compiler, that is, in the browser, it can convert the template into real dom in real time, and the other is the precompiled environment, that is, the template syntax cannot be recognized in the process of webpack packaging.

So we can only use rendering function to write virtual dom.
Because the rendering function personally feels more troublesome to write, I use JSX here to steal a lazy.

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',{
    })
}

When you open the home page again, you can see that the function of router link is normal. Click the page link and the home page url can also change normally.

Then it's natural to know that the next step is to monitor the changes of url and render the corresponding components.
Then the logic of monitoring url changes can be realized in the constructor of router.

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

You can see here that we saved the changed hash value to this Currenturl, so the object instantiated in this constructor can be passed through this Currenturl gets the current url value, and we have linked the router to the global before, so we can use this. In Vue$ router. Currenturl gets this value.
With this idea, you can directly write 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);
        }
    })

As you can see, we compare the current url value with that in the configuration item, and then get the component that should be rendered and mount it directly. At this time, the page should also be displayed successfully.

In the last step, there is no problem with rendering, but we have not made responsive logic. Therefore, clicking router link at this time cannot jump. So the last step is to notify the router view when the current url is updated. Here you can show your magic power. Bloggers use a custom publish and subscribe mode. The specific codes are as follows

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

Here, I use a publish subscribe mode and even use forceUpdate, but I always implement a basic vue router. In fact, vue provides its own method to turn variables into response, but after using it, I found that the view has not been updated, so I implemented one myself. In fact, the methods and functions provided by the real vue router must be much more complex. Here we just read the source code from the most basic functions of the router and even start to implement a view router by ourselves.