Core principles of handwritten Vue-Router

@[toc]

I. Core principles

1. What is front-end routing?

In the Single Page Application (SPA) of the Web front-end, the route describes the mapping between THE URL and the UI. This mapping is one-way, that is, THE URL changes cause UI updates (without refreshing the Page).

2. How to implement front-end routing?

To implement front-end routing, two cores need to be addressed:

  1. How do I change a URL without causing a page refresh?

  2. How do I detect URL changes?

The following two core questions are answered using hash and History implementations, respectively.

Hash implementation

The hash is the hash (#) and the following part of the URL. It is often used as an anchor to navigate through the page. Changing the hash part of the URL does not cause the page to refresh

Using the HashChange event to listen for URL changes, there are only a few ways to change a URL:

  1. Change the URL backwards and forwards through the browser
  2. through<a>Tags change urls
  3. Change the URL using window.location
The history to achieve

History provides pushState and replaceState methods that change the PATH portion of the URL without causing the page to refresh

History provides popState events that are similar to hashChange events, but popState events are a little different:

  1. The POPState event is triggered when the URL is changed backwards and forwards through the browser
  2. Through the pushState/replaceState or<a>Tags that change urls do not trigger popState events.
  3. Thankfully, we can intercept pushState/replaceState calls and<a>Tag click event to detect URL changes
  4. This event is triggered by calling history’s back, go, and forward methods

So listening for URL changes is possible, just not as convenient as hashchange.

Second, native JS to achieve front-end routing

1. Hash based implementation

html

<! DOCTYPEhtml>
<html lang="en">
<body>
<ul>
    <ul>
        <! -- Define routing -->
        <li><a href="#/home">home</a></li>
        <li><a href="#/about">about</a></li>

        <! -- render route UI -->
        <div id="routeView"></div>
    </ul>
</ul>
</body>
<script>
    let routerView = routeView
    window.addEventListener('hashchange'.() = >{
        let hash = location.hash;
        routerView.innerHTML = hash
    })
    window.addEventListener('DOMContentLoaded'.() = >{
        if(! location.hash){// If there is no hash value, redirect to #/
            location.hash="/"
        }else{// If there is a hash value, render the corresponding UI
            let hash = location.hash;
            routerView.innerHTML = hash
        }
    })
</script>
</html>


Copy the code

To explain the above code, it’s actually quite simple:

  1. We change the hash value of the URL using the a tag’s href attribute (of course, you can also trigger the browser’s back and forward buttons, or enter the window.location assignment on the console to change the hash).
  2. We listen for hashChange events. Once the event is triggered, it changes the contents of the routerView, which in vUE would be the contents of the Router-View component
  3. Why is the load event listening again? Since the hashChange is not triggered when the page is first loaded, the load event is used to listen for the hash value and render the view as the corresponding content.

2. Implement based on history

<! DOCTYPEhtml>
<html lang="en">
<body>
<ul>
    <ul>
        <li><a href='/home'>home</a></li>
        <li><a href='/about'>about</a></li>

        <div id="routeView"></div>
    </ul>
</ul>
</body>
<script>
    let routerView = routeView
    window.addEventListener('DOMContentLoaded', onLoad)
    window.addEventListener('popstate'.() = >{
        routerView.innerHTML = location.pathname
    })
    function onLoad () {
        routerView.innerHTML = location.pathname
        var linkList = document.querySelectorAll('a[href]')
        linkList.forEach(el= > el.addEventListener('click'.function (e) {
            e.preventDefault()
            history.pushState(null.' ', el.getAttribute('href'))
            routerView.innerHTML = location.pathname
        }))
    }

</script>
</html>
Copy the code

Explain the above code, which is pretty much the same:

  1. We use the a tag’s href attribute to change the URL’s path (of course, you can also trigger the browser’s forward and back buttons, or the popState event by typing history.go,back, and forward on the console). The important thing to note here is that changing the path value triggers a jump in the page by default, so interception is required <a>The default behavior of the TAB click event is to use pushState to modify the URL and update the manual UI when clicked, thus achieving the effect of clicking a link to update the URL and UI.
  2. We listen for popState events. Once the event is triggered, the contents of the routerView are changed.
  3. The load event is the same

Here’s a question: Hash mode, can we also use history. Go,back,forward to trigger hashChange events?

A: That’s ok. Because no matter what the mode, the browser will have a stack for saving records.

Third, implement VueRouter based on Vue

Let’s first build a project using vue-CLI

Delete some unnecessary builds after the project directory is temporarily as follows:

Have put the project on Github: github.com/Sunny-lucki… Can you humble yourself and ask for a star. Any questions or suggestions, please comment below

We mainly look at the App. Vue, the About. Vue, Home. Vue, the router/index. Js

The code is as follows:

App.vue

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/home">Home</router-link> |
      <router-link to="/about">About</router-link>
    </div>
    <router-view/>
  </div>
</template>
Copy the code

router/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '.. /views/Home.vue'
import About from ".. /views/About.vue"
Vue.use(VueRouter)
  const routes = [
  {
    path: '/home'.name: 'Home'.component: Home
  },
  {
    path: '/about'.name: 'About'.component: About
  }
]
const router = new VueRouter({
  mode:"history",
  routes
})
export default router

Copy the code

Home.vue

<template>
  <div class="home">
    <h1>This is the Home component</h1>
  </div>
</template>
Copy the code

About.vue

<template>
  <div class="about">
    <h1>This is the About component</h1>
  </div>
</template>
Copy the code

Now let’s start the project. See if the project initialization was successful.

Ok, nothing wrong, initialization succeeded.

Now we’ve decided to create our own VueRouter, so create the myvuerouter.js file

The current directory is as follows

Let’s introduce VueRouter and change that to our myvuerouter.js

//router/index.js
import Vue from 'vue'
import VueRouter from './myVueRouter' // Modify the code
import Home from '.. /views/Home.vue'
import About from ".. /views/About.vue"
Vue.use(VueRouter)
  const routes = [
  {
    path: '/home'.name: 'Home'.component: Home
  },
  {
    path: '/about'.name: 'About'.component: About
  }
];
const router = new VueRouter({
  mode:"history",
  routes
})
export default router

Copy the code

Fourth, analyze the essence of VueRouter

Let’s start with a question about how VueRouter is introduced into the Vue project.

  1. Install the VueRouter, then passimport VueRouter from 'vue-router'The introduction of
  2. First,const router = new VueRouter({... })With router as a property value,new Vue({router})
  3. Use (VueRouter) allows each component to have a Store instance

What do we learn from this introduction process?

  1. We are using new VueRouter({… }) to get a router instance, that is, the VueRouter we introduced is actually a class.

So we can make a preliminary assumption

class VueRouter{}Copy the code
  1. We also use vue.use (), and one of the principles of vue.use is to execute the install method on the object

So, we can go one step further and assume that VueRouter has the install method.

class VueRouter{

}
VueRouter.install = function () {}Copy the code

So over here, can you write the VueRouter roughly?

It’s easy to just export the VueRouter above, which is myvuerouter.js

//myVueRouter.js
class VueRouter{

}
VueRouter.install = function () {}export default VueRouter
Copy the code

V. Analyze vue.use

Vue.use(plugin);

(1) Parameters

{ Object | Function } plugin
Copy the code

(2) Usage

Install the vue.js plug-in. If the plug-in is an object, you must provide the install method. If the plug-in is a function, it is treated as the install method. When the install method is called, Vue is passed in as an argument. When the install method is called multiple times by the same plug-in, the plug-in is installed only once.

For more information on how to develop Vue plug-ins, check out this article, which is very simple and takes less than two minutes to read: How to develop Vue plug-ins?

(3) Function

To register the plug-in, simply call the install method and pass Vue as a parameter. But in detail there are two parts of logic to deal with:

1. The type of the plug-in, which can be either the install method or an object containing the install method.

2. The plug-in can be installed only once. Ensure that there are no duplicate plug-ins in the plug-in list.

(4) Implementation

Vue.use = function(plugin){
	const installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
	if(installedPlugins.indexOf(plugin)>-1) {return this; } <! -- Other parameters -->const args = toArray(arguments.1);
	args.unshift(this);
	if(typeof plugin.install === 'function'){
		plugin.install.apply(plugin,args);
	}else if(typeof plugin === 'function'){
		plugin.apply(null,plugin,args);
	}
	installedPlugins.push(plugin);
	return this;
}
Copy the code

1. Add the use method to vue.js and accept a parameter plugin.

2. First check whether the plugin has been registered. If it has been registered, the method execution will be terminated directly.

The toArray method is used to convert a class-like array into a real array. Use the toArray method to get arguments. All arguments except the first assign the resulting list to Args, and then add Vue to the top of args’ list. The purpose of this is to ensure that the first parameter of the install method is Vue and the rest of the parameters are passed when the plug-in is registered.

4. Since the plugin parameter supports objects and function types, you can know which way the user uses the plug-in by determining which function is plugin.install or plugin, and then execute the plug-in written by the user and pass args as the parameter.

Finally, add the plugin to installedPlugins to ensure that the same plugin is not registered over and over again. ~~ reminds me of the time when an interviewer asked me why plug-ins wouldn’t be reloaded!! Crying, now I understand.)

In the third part, we use Vue as the first parameter of install, so we can save Vue

//myVueRouter.js
let Vue = null;
class VueRouter{

}
VueRouter.install = function (v) {
    Vue = v;
};

export default VueRouter
Copy the code

Then create two components, router-link and router-view, with the Vue passed in

//myVueRouter.js
let Vue = null;
class VueRouter{

}
VueRouter.install = function (v) {
    Vue = v;
    console.log(v);

    // Add code
    Vue.component('router-link', {render(h){
            return h('a', {},'home')
        }
    })
    Vue.component('router-view', {render(h){
            return h('h1', {},'Home View')}}};export default VueRouter
Copy the code

Let’s execute the project, and if there are no errors, our assumptions are correct.

Oh, my God. It was right. No problem!

Vi. Improve the install method

Install generally adds something to each vUE instance

In this case, add $route and $Router to each component.

$routeand$routerWhat’s the difference?

A: $router is an instance of VueRouter, and $route is the current route object

Note that each component adds the same $route and the same $router, which are shared by all components.

What does that mean??

To see mian. Js

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

Vue.config.productionTip = false

new Vue({
  router,
  render: function (h) { return h(App) }
}).$mount('#app')
Copy the code

We can see that we are only using the store instance that the router, i.e./router, is exported as part of the Vue parameter.

The problem is that Vue is the root component. The root component does not have the router value, so we need to make sure that other components have the router value as well.

Therefore, the install method can be perfected this way

//myVueRouter.js let Vue = null; class VueRouter{ } VueRouter.install = function (v) { Vue = v; Mixin ({beforeCreate(){if (this.$options && this.$options.router){// if the root component is this._root = this; // Mount the current instance to _root. This._router = this.$options.router; $parent && this.$parent._root} object.defineProperty (this,'$router',{get(){return) This. _root. _router}}}})) Vue.com ponent (' the router - link, {render (h) {return h (' a ', {}, 'front page')}}) Vue.com ponent (' the router - view, {render (h) {return h (' h1, {}, 'home page view')}})}; export default VueRouterCopy the code

Explain the code:

  1. The Vue parameter is passed as the parameter when we analyze Vue. Use in section 4, and then when we execute install.
  2. Mixins are used to mix the contents of mixins into Vue’s initial parameters, options. Those of you who use Vue have already used mixins.
  3. Why beforeCreate rather than created? Because if you’re doing this at created, $options is already initialized.
  4. If we determine that the current component is the root component, we will hook the router and _root we passed in to the root component instance.
  5. If the current component is determined to be a child component, we mount our _root root component to the child component. Note replication of references, so that each component has the same _root root component mounted on it.

Why is it possible to get the _root component directly from the parent component if the current component is a child? This reminds me of a question I was once asked by an interviewer: the order in which the parent and child components are executed?

A: Parent beforeCreate-> Parent created -> parent beforeMounte -> child beforeCreate-> child Create-> child beforeMount -> Child Mounted -> parent

When beforeCreate of the child component is executed, the parent component has completed beforeCreate, so the parent component has _root.

And then we go through

Object.defineProperty(this,'$router',{
  get(){
      return this._root._router
  }
})
Copy the code

Mount $Router to the component instance.

We get the $router from the component, and we return the root component’s _root._router

$route is not yet implemented. Now it can not be realized. If there is no perfect VueRouter, it can not get the current path

7. Perfect the VueRouter class

So let’s first look at what did we pass into our new VueRouter class

//router/index.js
import Vue from 'vue'
import VueRouter from './myVueRouter'
import Home from '.. /views/Home.vue'
import About from ".. /views/About.vue"
Vue.use(VueRouter)
  const routes = [
  {
    path: '/home'.name: 'Home'.component: Home
  },
  {
    path: '/about'.name: 'About'.component: About
  }
];
const router = new VueRouter({
  mode:"history",
  routes
})
export default router
Copy the code

We passed an array of routes and a mode that represents the current mode. So we can implement VueRouter this way

class VueRouter{
    constructor(options) {
        this.mode = options.mode || "hash"
        this.routes = options.routes || [] // The route you pass is an array table}}Copy the code

The first two arguments are received.

However, it is very inconvenient for us to directly deal with routes, so we first need to convert them into key: value format

//myVueRouter.js
let Vue = null;
class VueRouter{
    constructor(options) {
        this.mode = options.mode || "hash"
        this.routes = options.routes || [] // The route you pass is an array table
        this.routesMap = this.createMap(this.routes)
        console.log(this.routesMap);
    }
    createMap(routes){
        return routes.reduce((pre,current) = >{
            pre[current.path] = current.component
            returnpre; }}}, {})Copy the code

With createMap we’re going to

const routes = [
  {
    path: '/home',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  }
Copy the code

Converted to

The current path must be stored in a route to represent the current path status. To facilitate management, you can use an object to represent the path status

//myVueRouter.js
let Vue = null; The new codeclass HistoryRoute {
    constructor(){
        this.current = null}}class VueRouter{
    constructor(options) {
        this.mode = options.mode || "hash"
        this.routes = options.routes || [] // The route you pass is an array table
        this.routesMap = this.createMap(this.routes) added codethis.history = new HistoryRoute();
        
    }

    createMap(routes){
        return routes.reduce((pre,current) = >{
            pre[current.path] = current.component
            returnpre; }}}, {})Copy the code

But now we see that current is still null, so we need to initialize it.

When initializing, determine whether it is in hash mode or history mode. Then save the current path value to current

//myVueRouter.js
let Vue = null;
class HistoryRoute {
    constructor(){
        this.current = null}}class VueRouter{
    constructor(options) {
        this.mode = options.mode || "hash"
        this.routes = options.routes || [] // The route you pass is an array table
        this.routesMap = this.createMap(this.routes)
        this.history = newHistoryRoute(); The new codethis.init()} added codeinit(){
        if (this.mode === "hash") {// Check whether the user has a hash value when it opens. If not, jump to #/
            location.hash? ' ':location.hash = "/";
            window.addEventListener("load".() = >{
                this.history.current = location.hash.slice(1)})window.addEventListener("hashchange".() = >{
                this.history.current = location.hash.slice(1)})}else{
            location.pathname? ' ':location.pathname = "/";
            window.addEventListener('load'.() = >{
                this.history.current = location.pathname
            })
            window.addEventListener("popstate".() = >{
                this.history.current = location.pathname
            })
        }
    }

    createMap(routes){
        return routes.reduce((pre,current) = >{
            pre[current.path] = current.component
            returnpre; }}}, {})Copy the code

Listening for events is the same as the previous native JS implementation.

8. Perfect $route

VueRouter’s history. Current ($route, $route, $route) ¶

It’s very simple, just like implementing $router

VueRouter.install = function (v) {
    Vue = v;
    Vue.mixin({
        beforeCreate(){
            if (this.$options && this.$options.router){ // If it is the root component
                this._root = this; Mount the current instance to _root
                this._router = this.$options.router;
            }else { // If it is a child component
                this._root= this.$parent && this.$parent._root
            }
            Object.defineProperty(this.'$router', {get(){
                    return this._root._router } }); The new codeObject.defineProperty(this.'$route', {get(){
                    return this._root._router.history.current
                }
            })
        }
    })
    Vue.component('router-link', {render(h){
            return h('a', {},'home')
        }
    })
    Vue.component('router-view', {render(h){
            return h('h1', {},'Home View')}}};Copy the code

Improve the Router-View component

Now that we have saved the current path, we can get the current path and then get the corresponding component from the routing table for rendering

Vue.component('router-view', {render(h){
        let current = this._self._root._router.history.current
        let routeMap = this._self._root._router.routesMap;
        return h(routeMap[current])
    }
})
Copy the code

To explain:

This in render refers to a Proxy object that represents the Vue component, and each component has a _root attribute that refers to the root component, which contains the _router routing instance. So we can get the routing table from the router instance, and we can also get the current path. The obtained component is then rendered in h().

Now that the router-view component is rendered, there is a problem: you change the path, the view is not rerendered, so you need to render _router.history responsive.

Vue.mixin({beforeCreate(){if (this.$options && this.$options.router){this._root = this; // Mount the current instance to _root. This._router = this.$options.router; }else {// if it is a child component this._root= this.$parent && this.$parent._root } Object.defineProperty(this,'$router',{ get(){ return this._root._router } }); Object.defineProperty(this,'$route',{ get(){ return this._root._router.history.current } }) } })Copy the code

We made use of the Vue API: defineReactive to make the this._router.history object listen.

So when we first render the router-view component, we get the this._router.history object, which will be listened to to get this._router.history. The dependency wacther of the router-view component is collected into the collector DEP corresponding to this._router.history, so every time this._router.history changes. The this._router-history collector dep tells the wacther that the component of the router-view depends on to perform an update(), causing the router-view to be rerendered. (This is how vUE responds.)

Now let’s test whether changing the value of the URL triggers a router-view rerendering

The path to the home

You u can see that the current path is successfully implemented listening.

Improve the Router-link component

Let’s take a look at how router-link works.

<router-link to="/home">Home</router-link> 
<router-link to="/about">About</router-link>
Copy the code

That means that the to path between the parent goes in and the child receives it so we can do that

Vue.component('router-link', {props: {to:String
    },
    render(h){
        let mode = this._self._root._router.mode;
        let to = mode === "hash"?"#"+this.to:this.to
        return h('a', {attrs: {href:to}},this.$slots.default)
    }
})
Copy the code

We render the Router-link as a tag, which is the easiest thing to do. You can switch paths on the URL by clicking on the A TAB. To achieve the view re-render

Ok, here to finish the project.

Check out the complete code for VueRouter

//myVueRouter.js
let Vue = null;
class HistoryRoute {
    constructor(){
        this.current = null}}class VueRouter{
    constructor(options) {
        this.mode = options.mode || "hash"
        this.routes = options.routes || [] // The route you pass is an array table
        this.routesMap = this.createMap(this.routes)
        this.history = new HistoryRoute();
        this.init()

    }
    init(){
        if (this.mode === "hash") {// Check whether the user has a hash value when it opens. If not, jump to #/
            location.hash? ' ':location.hash = "/";
            window.addEventListener("load".() = >{
                this.history.current = location.hash.slice(1)})window.addEventListener("hashchange".() = >{
                this.history.current = location.hash.slice(1)})}else{
            location.pathname? ' ':location.pathname = "/";
            window.addEventListener('load'.() = >{
                this.history.current = location.pathname
            })
            window.addEventListener("popstate".() = >{
                this.history.current = location.pathname
            })
        }
    }

    createMap(routes){
        return routes.reduce((pre,current) = >{
            pre[current.path] = current.component
            return pre;
        },{})
    }

}
VueRouter.install = function (v) {
    Vue = v;
    Vue.mixin({
        beforeCreate(){
            if (this.$options && this.$options.router){ // If it is the root component
                this._root = this; Mount the current instance to _root
                this._router = this.$options.router;
                Vue.util.defineReactive(this."xxx".this._router.history)
            }else { // If it is a child component
                this._root= this.$parent && this.$parent._root
            }
            Object.defineProperty(this.'$router', {get(){
                    return this._root._router
                }
            });
            Object.defineProperty(this.'$route', {get(){
                    return this._root._router.history.current
                }
            })
        }
    })
    Vue.component('router-link', {props: {to:String
        },
        render(h){
            let mode = this._self._root._router.mode;
            let to = mode === "hash"?"#"+this.to:this.to
            return h('a', {attrs: {href:to}},this.$slots.default)
        }
    })
    Vue.component('router-view', {render(h){
            let current = this._self._root._router.history.current
            let routeMap = this._self._root._router.routesMap;
            return h(routeMap[current])
        }
    })
};

export default VueRouter
Copy the code

Now let’s see if it works

|

I’m going to hit Yes and I’m going to switch views.

Perfect end !!!!

Any questions or suggestions, please comment below

Thank you also congratulations you see here, I can humble beg a star!!

Github:github.com/Sunny-lucki…

Reference: this article from one, two section principle parts: blog.csdn.net/qq867263657…