Handwritten Vue router & what is Vue plug-in

Keywords: Javascript ECMAScript Vue.js

In this article, you can learn:

  • Implement your own Vue router
  • Learn what Vue plug-ins are

Learning the notes made by the boss of station b and the implementation of the source code

1.1.3 take you step by step to understand the core principle and implementation of Vue router_ Beep beep beep_ bilibili

Use the official Vue router

Initialize a project through Vue cli scaffolding

Download Vue router

PS: you can choose whether to install Vue router when generating Vue cli scaffold

The following is the manual installation process:

  • After NPM installs Vue router, it is imported through import
  • Then it is introduced through Vue.use()
  • Then define a routing table routes
  • Then new vueroter can get an instance
  • Two new components, Home and About, have been created

Get code:

router/index.js

import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/home'
import About from '@/components/about'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home
    },
    {
      path: '/about',
      name: 'About',
      component: About
    }
  ]
})

Import into main.js

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

Vue.config.productionTip = false

new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>'
})

Add this configuration item in new Vue

Using router link and router view

App.vue

<template>
  <div id="app">
    <router-link to="/">home</router-link>
    <router-link to="/about">about</router-link>
    <router-view/>
  </div>
</template>

effect:

Write a Vue router yourself

The old rule is to start with the source code first

Uncommented version:

let Vue;
class VueRouter {
    constructor(options) {
        this.$options = options;
        let initial = window.location.hash.slice(1) || "/";
        Vue.util.defineReactive(this, "current", initial);
        window.addEventListener("hashchange", () => {
            this.current = window.location.hash.slice(1) || "/";
        })
    }
}

VueRouter.install = (_Vue) => {
    Vue = _Vue;
    Vue.mixin({
        beforeCreate() {
            if (this.$options.router) {
                Vue.prototype.$router = this.$options.router;
            }
        }
    });
    
    Vue.component("router-link", {
        props: {
            to: {
                type: String,
                required: true,
            }
        },
        render(h) {
            return h("a",
            {
                attrs: {
                    href: `#${this.to}`
                },
            },
                this.$slots.default
            );
        }
    });
    Vue.component("router-view", {
        render(h) {
            let component = null;
            
            const current = this.$router.current;

            const route = this.$router.$options.routes.find(
                (route) => route.path === current
            )

            if (route) component = route.component;

            return h(component);
        }
    })
}
export default VueRouter;

Version with personal comments:

// 1. Implement a plug-in
// 2. Two components

// How to write Vue plug-in
// A plug-in is either a function or an object
// The plug-in must implement an install method, which will be called by vue in the future
let Vue; // Save the constructor of Vue and use it in the plug-in
class VueRouter {
    constructor(options) {
        this.$options = options;
        // Only after the current is changed into responsive data can you modify and re execute the render function in router view
        let initial = window.location.hash.slice(1) || "/";
        Vue.util.defineReactive(this, "current", initial);

        window.addEventListener("hashchange", () => {
            // Get # what's next
            this.current = window.location.hash.slice(1) || "/";
        })
    }
}

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

    // 1. Mount the $router attribute (the VueRouter instance object from new in router/index.js cannot be obtained),
    // Because Vue.use needs to point faster, it can only be used by introducing router in main.js
    // this.$router.push()
    // Global mixing (delay the following logic until the router is created and attached to the option)
    Vue.mixin({
        beforeCreate() {
            // Note that this hook is called when each component creates an instance
            // Determine whether the root instance has this option
            if (this.$options.router) {
                /**
                 * Because each Vue component instance inherits the methods above Vue.prototype, you can
                 * In each component, you can access the new VueRouter instance initialized in router/index.js through this.$router
                 */
                Vue.prototype.$router = this.$options.router;
            }
        }
    });
    
    // Implement two components: router link and router view
    // < router link to = "/" > hone < / router link > so we need to convert this router link label to: < a href = "/" > home</a>
    /**
     * The second parameter is actually a template, that is, a rendering component dom
     * What we use here is the rendering function, which returns a virtual DOM
     */
    Vue.component("router-link", {
        props: {
            to: {
                type: String,
                required: true,
            }
        },
        render(h) {
            return h("a",
            {
                attrs: {
                    // In order not to update the page again, the anchor point is used here
                    href: `#${this.to}`
                },
            },
            // If you want to get Home, you can do the following
                this.$slots.default
            );
        }
    });
    Vue.component("router-view", {
        render(h) {
            let component = null;

            // Because this.$router is obtained by mixing, you can get the component corresponding to the current route and render it
            const current = this.$router.current;

            const route = this.$router.$options.routes.find(
                (route) => route.path === current
            )

            if (route) component = route.component;

            return h(component);
        }
    })
}
export default VueRouter;

Step by step analysis - start from scratch

First, there are several questions

Question 1:

In router/index.js

import Router from 'vue-router'

Vue.use(Router)

As we know, Vue.use() introduces a plug-in

So what does the plug-in Vue router do internally?

Question 2:

In router/index.js

import Router from 'vue-router'

export default new Router({
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home
    },
    {
      path: '/about',
      name: 'About',
      component: About
    }
  ]
})
  • An imported Vue router plug-in object is initialized
  • What is passed in brackets is a {} object, which is actually a configuration item
    • The configuration item contains a routes routing table

Then in main.js

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

Vue.config.productionTip = false

new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>'
})

When creating a new Vue instance, the exported router is imported as a configuration item. Why?

Question 3: router link and router view

  • The router link component is used in the component to realize route jump
  • Use the router view component as the exit of the route

So, how are these two components implemented internally?

Why? Other components can only be used after they are declared in the Component, but if these two components are used directly, it means that these two components must have been globally registered somewhere

General idea of expansion:

In fact, it is implemented in jquery in this way: by listening to the transformation of the current hash value or the change of history, you can get a triggered event, and then you can get the current address (that is, the address to jump) Then, through this address, you can go to the routing table defined in our router/index.js, that is, match the path to get the component, so that you can get the component, and then get the real DOM, and then add it to our router view, that is, empty the contents of the previous router view, and then press the latest DOM into the router view As shown, this is a typical DOM operation

But there is a new thing in Vue: Vue's responsive principle, so you can use responsive to monitor routing changes

What is Vue's plug-in

Learn from: Deeply understand Vue's plug-in mechanism and install detailed _vue.js_script home (jb51.net)

  • Why does the plug-in implement an install method

Vue plug-ins should expose an install method. The e first parameter of this method is the Vue constructor, and the second parameter is an optional option object -- this is the Vue official specification for Vue plug-ins,

What can the install function do?

How is install implemented internally?

What exactly does the plug-in do in install?

Classic triple question~

Handling of install in plug-ins such as Vue router

Throw a question:

  1. Why can you use $router directly in a project to get the value and some methods
  2. Why do these plug-ins have to be introduced with Vue.use before creating an instance, and then introduced in Vue instances

Example of using Vue router

class Router {
    constructor(options) {
    }
}

Router.install = function(_Vue) {
    _Vue.mixin({
        beforeCreate() {
            if (this.$options.router) {
                _Vue.prototype.$router = this.$options.router
            }
        }
    })
}
export default Router;
  • _What is Vue.mixin global mixing? It is equivalent to mixing this method in all components;
  • What is beforeCreate? Of course, it is a life cycle of Vue, which is executed before create;

So:

  1. Vue router uses a global blend in the install function, and puts this when the beforeCreate life cycle is triggered o p t i o n . r o u t e r hang load reach V u e of primary type upper Yes , that Do you this kind Just can with send use t h i s . option.router is mounted on the Vue prototype, so you can use this option.router is mounted on the Vue prototype, so you can use this.router to call the router instance

  2. So what is this.$options.router

    • this. In the global mix o p t i o n s yes I Guys stay stay m a i n . j s in n e w V u e ( ) of Time Wait large Include number in noodles pass enter of match Set term , place with I Guys m a i n . j s pass enter of r o u t e r , stay this in Just can with through too t h i s . options is the configuration item passed in the {} braces when we new Vue ({}) in main.js, so the router passed in from main.js can be passed through this Options is the configuration item we passed in braces when we newVue() in main.js, so the router we passed in from main.js can get our new Vue router instance in router/index.js through this.options.router

Why is it designed like this: because in router/index.js

import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/home'
import About from '@/components/about'
Vue.use(Router)
export default new Router({
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home
    },
    {
      path: '/about',
      name: 'About',
      component: About
    }
  ]
})

The Vue.use is executed first and then the new Vue router object is operated. Therefore, if you want to use this Vue router instance in the plug-in install, you need to pass the instance into the new Vue ({}) configuration item of main.js. In this way, we can obtain the routing table defined in the new Router ({}) by dependency injection,

We put Vue.prototype r o u t e r = t h i s . router = this. router=this.options.router; so other components can use this r o u t e r Obtain take interview ask reach I Guys set righteousness of road from surface Yes , place with by What Do you can with use t h i s . The router has accessed the routing table defined by us, so why can we use this The router has accessed the routing table defined by us, so why can we use this.router.push() to add routes? Part of the reason is that this.$router routing table is an array, so it can be pushed

  • Vue.use mainly calls the install method inside the plug-in and passes in the Vue instance as a parameter

Internal implementation of plug-in install in vue

The following is the source code of Vue.use

export function initUse (Vue: GlobalAPI) {
    // Register a use method mounted on the instance
    Vue.use = function (plugin: Function | Object) {
        // Initializes the array of the current plug-in
        const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
        // If the plug-in has been registered, it will not be processed
        if (installedPlugins.indexOf(plugin) > -1) {
            return this
        }

        ...
        
        // Here comes the point!!!
        if (typeof plugin.install === 'function') {
        // When install is a function in the plug-in, call the install method, point to the plug-in, and pass in a number of parameters
            plugin.install.apply(plugin, args)

        } else if (typeof plugin === 'function') {
        // When the plug-in itself is a function, take it as an install method, point to the plug-in, and pass in a number of parameters
            plugin.apply(null, args)

        }
        
        // Put the plug-in into the plug-in array
        installedPlugins.push(plugin)

        return this
    }
}


See here, everyone should have a certain understanding of the plug-in, stick to it!!

Start implementation

  • First: because router/index initializes the instance of the plug-in, the plug-in can be represented by a class, and an install method should be implemented
class VueRouter {

}

VueRouter.install = (_Vue) => {

}

As mentioned above, the first parameter of the plug-in install method is the Vue instance itself

optimization

vue instances are also used in other places later, so we declare a global vue in the plug-in to save the incoming vue instance

And: it is also a way to ensure the independence of the plug-in and vue. After this operation, when we package the plug-in, we will not package vue into the plug-in

And mount the configuration item router from new Vue ({router}) to the Vue instance prototype object

let Vue; 
class VueRouter {

}

VueRouter.install = (_Vue) => {
    Vue = _Vue;
    Vue.mixin({
        beforeCreate() {
            if (this.$options.router) {
                Vue.prototype.$router = this.$options.router;
            }
        }
    })
}

Not only that, we also implemented two components, router link and router view, in the install function
Principle:

Home, so we need to convert this router link tag to: Home

  • Receive a to attribute
  • And it returns a render rendering function, that is, it returns a virtual DOM

So how to get the text Home in the middle of router link?

Expansion: Vue.$slots

Therefore, because there is only home text in router link, you can get it directly through vue.$slots.default

let Vue;
class VueRouter {

}

VueRouter.install = (_Vue) => {
    Vue = _Vue;
    Vue.mixin({
        beforeCreate() {
            if (this.$options.router) {
                Vue.prototype.$router = this.$options.router;
            }
        }
    });

    Vue.component("router-link", {
        props: {
            to: {
                type: String,
                required: true,
            }
        },
        render(h) {
            return h("a",
            {
                attrs: {
                    // In order not to update the page again, the anchor point is used here
                    href: `#${this.to}`
                },
            },
            // If you want to get Home, you can do the following
                this.$slots.default
            );
        }
    });
}

The above is the specific implementation of router link

The following is the router view implementation

Principle: get the current route, find the corresponding component from the route table and render it

Note: in the install method, we mount the vue router instance instantiated in router/index.js to $router on the vue prototype object through global mixing

  • Then: we can get our instantiated component through this.$router in the component

Here's how to implement the class of the plug-in

In router/index.js, we

new Router({
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home
    },
    {
      path: '/about',
      name: 'About',
      component: About
    }
  ]
}) 

A routing table is passed in as the configuration item of this plug-in instance

So we can get this configuration item through parameters in the constructor of this class. In order to get the routing table in other components, we mount the configuration item to the class itself

class VueRouter {
    constructor(options) {
        this.$options = options
    }
}

Why?

In this case, in these components of router view

You can use this r o u t e r . router. router.options access the routing table in the configuration item passed in the Vue router class of new in router/index

class VueRouter {
    constructor(options) {
        this.$options = options
        this.current = window.location.hash.slice(1) || "/";
        window.addEventListener("hashchange", () => {
            // Get # what's next
            this.current = window.location.hash.slice(1) || "/";
        })
    }
}

Initialize current, listen for route changes through onhashchange, and assign values to current

slice (1) is used to get the # following value

In this way, the router view component can be implemented

let Vue;
class VueRouter {
    constructor(options) {
        this.$options = options
        this.current = window.location.hash.slice(1) || "/";
        window.addEventListener("hashchange", () => {
            // Get # what's next
            this.current = window.location.hash.slice(1) || "/";
        })
    }
}

VueRouter.install = (_Vue) => {
    Vue = _Vue;
    Vue.mixin({
        beforeCreate() {
            if (this.$options.router) {
                Vue.prototype.$router = this.$options.router;
            }
        }
    });

    Vue.component("router-link", {
        props: {
            to: {
                type: String,
                required: true,
            }
        },
        render(h) {
            return h("a",
            {
                attrs: {
                    // In order not to update the page again, the anchor point is used here
                    href: `#${this.to}`
                },
            },
            // If you want to get Home, you can do the following
                this.$slots.default
            );
        }
    });
    Vue.component("router-view", {
        render(h) {
            let component = null;

            // Because this.$router is obtained by mixing, you can get the component corresponding to the current route and render it
            const current = this.$router.current;

            const route = this.$router.$options.routes.find(
                (route) => route.path === current
            )

            if (route) component = route.component;

            return h(component);
        }
    })  
}

So the current code is like this

However, we can find that the current has changed and the router view remains unchanged. This is because the current is not a responsive data at this time. Therefore, when the current changes, the render function in the router view will not execute and render again

Therefore, the current in the class class will become responsive data

Extension: Vue.util.defineReactive

Vue.util.defineReactive(obj,key,value,fn)

obj: target object,

key: target object attribute;

Value: attribute value

fn: called only when set is in the node debugging environment

In fact, the bottom layer is an Object.defineProperty()

Dependencies are collected through dep, and the ob attribute is added through the Observer class

class VueRouter {
    constructor(options) {
        this.$options = options;
        // Only after the current is changed into responsive data can you modify and re execute the render function in router view
        let initial = window.location.hash.slice(1) || "/";
        Vue.util.defineReactive(this, "current", initial);

        window.addEventListener("hashchange", () => {
            // Get # what's next
            this.current = window.location.hash.slice(1) || "/";
        })
    }
}

So the complete code is:

// 1. Implement a plug-in
// 2. Two components

// How to write Vue plug-in
// A plug-in is either a function or an object
// The plug-in must implement an install method, which will be called by vue in the future
let Vue; // Save the constructor of Vue and use it in the plug-in
class VueRouter {
    constructor(options) {
        this.$options = options;
        // Only after the current is changed into responsive data can you modify and re execute the render function in router view
        let initial = window.location.hash.slice(1) || "/";
        Vue.util.defineReactive(this, "current", initial);

        window.addEventListener("hashchange", () => {
            // Get # what's next
            this.current = window.location.hash.slice(1) || "/";
        })
    }
}

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

    // 1. Mount the $router attribute (the VueRouter instance object from new in router/index.js cannot be obtained),
    // Because Vue.use needs to point faster, it can only be used by introducing router in main.js
    // this.$router.push()
    // Global mixing (delay the following logic until the router is created and attached to the option)
    Vue.mixin({
        beforeCreate() {
            // Note that this hook is called when each component creates an instance
            // Determine whether the root instance has this option
            if (this.$options.router) {
                /**
                 * Because each Vue component instance inherits the methods above Vue.prototype, you can
                 * In each component, you can access the new VueRouter instance initialized in router/index.js through this.$router
                 */
                Vue.prototype.$router = this.$options.router;
            }
        }
    });
    
    // Implement two components: router link and router view
    // < router link to = "/" > hone < / router link > so we need to convert this router link label to: < a href = "/" > home</a>
    /**
     * The second parameter is actually a template, that is, a rendering component dom
     * What we use here is the rendering function, which returns a virtual DOM
     */
    Vue.component("router-link", {
        props: {
            to: {
                type: String,
                required: true,
            }
        },
        render(h) {
            return h("a",
            {
                attrs: {
                    // In order not to update the page again, the anchor point is used here
                    href: `#${this.to}`
                },
            },
            // If you want to get Home, you can do the following
                this.$slots.default
            );
        }
    });
    Vue.component("router-view", {
        render(h) {
            let component = null;

            // Because this.$router is obtained by mixing, you can get the component corresponding to the current route and render it
            const current = this.$router.current;

            const route = this.$router.$options.routes.find(
                (route) => route.path === current
            )

            if (route) component = route.component;

            return h(component);
        }
    })
}
export default VueRouter;

For later optimizations, such as changing the mode (history and hash) through mode, hash is used by default, and routing interceptors.

Posted by richierich on Fri, 29 Oct 2021 20:19:50 -0700