7 pictures, realize a simple version of Vue router from zero, which is too easy to understand!

Keywords: Javascript node.js Vue.js

preface

In the most easy to understand words, talk about the most difficult knowledge points. I believe everyone must have used Vue router in Vue project, that is, routing. Therefore, in this article, I will not explain the basic explanation of Vue router. I will not explain the source code of Vue router to you. I will take you to implement a Vue router from scratch!!!

Basic usage of routing

We usually use Vue router a lot, and basically every project will use it, because Vue is a single page application, which can switch components through the path to achieve the effect of switching pages. We usually use it like this. In fact, it is divided into three steps

  • 1. Introduce Vue router and use Vue.use(VueRouter)
  • 2. Define the routing array, pass the array into the vueroouter instance, and expose the instance
  • 3. Introduce the vueroter instance into main.js and register it with the root Vue instance
// src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import home from '../components/home.vue'
import hello from '../components/hello.vue'
import homeChild1 from '../components/home-child1.vue'
import homeChild2 from '../components/home-child2.vue'

Vue.use(VueRouter) // First step
const routes = [
    {
        path: '/home',
        component: home,
        children: [
            {
                path: 'child1',
                component: homeChild1
            },
            {
                path: 'child2',
                component: homeChild2
            }
        ]
    },
    {
        path: '/hello',
        component: hello,
        children: [
            {
                path: 'child1',
                component: helloChild1
            },
            {
                path: 'child2',
                component: helloChild2
            }
        ]
    },
]

export default new VueRouter({
    routes // Step 2
})

// src/main.js
import router from './router'

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

Distribution of router view and router link

// src/App.vue
<template>
  <div id="app">
    <router-link to="/home">home of link</router-link>
    <span style="margin: 0 10px">|</span>
    <router-link to="/hello">hello of link</router-link>
    <router-view></router-view>
  </div>
</template>

// src/components/home.vue

<template>
    <div style="background: green">
        <div>home Oh, hey, hey</div>
        <router-link to="/home/child1">home Son 1</router-link>
        <span style="margin: 0 10px">|</span>
        <router-link to="/home/child2">home Son 2</router-link>
        <router-view></router-view>
    </div>
</template>

// src/components/hello.vue

<template>
    <div style="background: orange">
        <div>hello Oh, hey, hey</div>
        <router-link to="/hello/child1">hello Son 1</router-link>
        <span style="margin: 0 10px">|</span>
        <router-link to="/hello/child2">hello Son 2</router-link>
        <router-view></router-view>
    </div>
</template>

// src/components/home-child1.vue the other three sub components are similar. The difference is that the text and background colors are different, so they are not written
<template>
    <div style="background: yellow">I am home My 1 son home-child1</div>
</template>

After the above three steps, what effect can we achieve?

  • 1. Enter the corresponding path in the web address, and the corresponding components will be displayed
  • 2. You can access $router and $router in any used component and use their methods or properties
  • 3. You can use the route link component for path jumping
  • 4. You can use the router view component to display the corresponding content of the route

Do it!!!

Vueroter class

In the src folder, create a my-router.js

The options parameter of VueRouter class is actually the parameter object passed in when new VueRouter(options), and install is a method, and the VueRouter class must have this method. Why? We'll talk about it later.

// src/my-router.js

class VueRouter {
    constructor(options) {}
    init(app) {}
}

VueRouter.install = (Vue) => {}

export default VueRouter

install method

Why do you have to define an install method and assign it to vueroter? In fact, this is related to the Vue.use method. Do you remember how Vue uses VueRouter?

import VueRouter from 'vue-router'

Vue.use(VueRouter) // First step

export default new VueRouter({ // Incoming options
    routes // Step 2
})

import router from './router'

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

In fact, the second and third steps are very clear, that is, to instance a VueRouter object and hang the VueRouter object on the root component App. The problem arises. What is the purpose of Vue.use(VueRouter) in the first step? In fact, Vue.use(XXX) is to execute the install method on XXX, that is, Vue.use(VueRouter) === VueRouter.install(), but here, we know that install will execute, but we still don't know what it is for and what's the use of install?

We know that the VueRouter object is attached to the root component App, so App can directly use the methods on the VueRouter object. However, we know that we certainly want every component used to use VueRouter methods, such as this.$router.push, but now only App can use these methods. What should we do? How can every component be used? At this time, the install method comes in handy. Let's talk about the implementation idea first, and then write the code.

Knowledge point: when Vue.use(XXX), it will execute the install method of XXX and pass Vue as a parameter into the install method

// src/my-router.js

let _Vue
VueRouter.install = (Vue) => {
    _Vue = Vue
    // Mix in each component with Vue.mixin
    Vue.mixin({
        // Execute in the beforeCreate life cycle of each component
        beforeCreate() {
            if (this.$options.router) { // If root component
                // this is the root component itself
                this._routerRoot = this

                // this.$options.router is the vueroter instance that is hung on the root component
                this.$router = this.$options.router

                // Execute the init method on the vueroter instance to initialize
                this.$router.init(this)
            } else {
                // If it is not a root component, the parent component should also be_ routerRoot is saved to itself
                this._routerRoot = this.$parent && this.$parent._routerRoot
                // The sub components should also be hung with $router
                this.$router = this._routerRoot.$router
            }
        }
    })
}

createRouteMap method

What is this method for? As the name suggests, it is to convert the transmitted routes array into a data structure of Map structure. key is path and value is the corresponding component information. Why do you want to convert it? We'll talk about this later. Let's implement the conversion first.

 

// src/my-router.js

function createRouteMap(routes) {

    const pathList = []
    const pathMap = {}

    // Traverse the passed in routes array
    routes.forEach(route => {
        addRouteRecord(route, pathList, pathMap)
    })

    console.log(pathList)
    // ["/home", "/home/child1", "/home/child2", "/hello", "/hello/child1"]
    console.log(pathMap)
    // {
    //     /hello: {path: xxx, component: xxx, parent: xxx },
    //     /hello/child1: {path: xxx, component: xxx, parent: xxx },
    //     /hello/child2: {path: xxx, component: xxx, parent: xxx },
    //     /home: {path: xxx, component: xxx, parent: xxx },
    //     /home/child1: {path: xxx, component: xxx, parent: xxx }
    // }


    // Return pathList and pathMap
    return {
        pathList,
        pathMap
    }
}

function addRouteRecord(route, pathList, pathMap, parent) {
    const path = parent ? `${parent.path}/${route.path}` : route.path
    const { component, children = null } = route
    const record = {
        path,
        component,
        parent
    }
    if (!pathMap[path]) {
        pathList.push(path)
        pathMap[path] = record
    }
    if (children) {
        // If there are children, execute addRouteRecord recursively
        children.forEach(child => addRouteRecord(child, pathList, pathMap, record))
    }
}

export default createRouteMap

Routing mode

There are three modes of routing

  • 1. hash mode, the most commonly used mode
  • 2. history mode, which requires back-end cooperation

And how to set the mode? It's set like this. It's passed in through the mode field of options

export default new VueRouter({
    mode: 'hash' // Setting mode
    routes
})

If it is not transmitted, the default mode is hash mode, which is also the most commonly used mode in our development, so this chapter only implements hash mode

// src/my-router.js

import HashHistory from "./hashHistory"

class VueRouter {
    constructor(options) {
        
        this.options = options
        
        // If the mode is not transmitted, the default is hash
        this.mode = options.mode || 'hash'

        // What is the judgment mode
        switch (this.mode) {
            case 'hash':
                this.history = new HashHistory(this)
                break
            case 'history':
                // this.history = new HTML5History(this, options.base)
                break
            case 'abstract':

        }
    }
    init(app) { }
}

HashHistory

Create hashHistory.js in the src folder

In fact, the principle of hash mode is to monitor the change of hash value in browser url and switch the corresponding components

class HashHistory {
    constructor(router) {

        // Save the vueroouter instance passed in
        this.router = router

        // If the url does not #, it is automatically populated/#/ 
        ensureSlash()
        
        // Listen for hash changes
        this.setupHashLister()
    }
    // Monitor hash changes
    setupHashLister() {
        window.addEventListener('hashchange', () => {
            // Pass in the hash of the current url and trigger a jump
            this.transitionTo(window.location.hash.slice(1))
        })
    }

    // Function triggered when jumping route
    transitionTo(location) {
        console.log(location) // Each hash change will be triggered. You can modify it in the browser
        // For example http://localhost:8080/#/home/child1  The latest hash is / home / Child1
    }
}

// If there is no #, it will be supplemented automatically/#/
function ensureSlash() {
    if (window.location.hash) {
        return
    }
    window.location.hash = '/'
}

// I won't talk about this first. I'll use it later
function createRoute(record, location) {
    const res = []
    if (record) {
        while (record) {
            res.unshift(record)
            record = record.parent
        }
    }
    return {
        ...location,
        matched: res
    }
}
export default HashHistory

createMmatcher method

As mentioned above, each hash modification can obtain the latest hash value, but this is not our ultimate goal. Our ultimate goal is to render different component pages according to hash changes. What should we do?

Remember the createRouteMap method before? We convert the routes array into a Map data structure. With that Map, we can obtain the corresponding components and render them according to the hash value

 

But is this really OK? In fact, it doesn't work. According to the above method, when the hash is / home/child1, only the home-child1.vue component will be rendered, but this is certainly not possible. When the hash is / home/child1, the home.vue and home-child1.vue components must be rendered

 

So we have to write a method to find which components correspond to the hash. This method is createMmatcher

// src/my-router.js

class VueRouter {
    
    // ... original code

    // Get all corresponding components according to hash changes
    createMathcer(location) {
    
        // Get pathMap
        const { pathMap } = createRouteMap(this.options.routes)

        const record = pathMap[location]
        const local = {
            path: location
        }
        if (record) {
            return createRoute(record, local)
        }
        return createRoute(null, local)
    }
}

// ... original code

function createRoute(record, location) {
    const res = []
    if (record) {
        while (record) {
            res.unshift(record)
            record = record.parent
        }
    }
    return {
        ...location,
        matched: res
    }
}
// src/hashHistory.js

class HashHistory {
    
    // ... original code

    // Function triggered when jumping route
    transitionTo(location) {
        console.log(location)
        
        // Find out all corresponding components. router is an instance of VueRouter and createMathcer is on it
        let route = this.router.createMathcer(location)

        console.log(route)
    }
}

 

This only ensures that all corresponding components can be found when the hash changes, but one thing we ignore is that if we manually refresh the page, the hashchange event will not be triggered, that is, we can't find components. What should we do? Refreshing the page will certainly reinitialize the route. We only need to perform a jump in place at the beginning of the initialization function init.

// src/my-router.js

class VueRouter {

    // ... original code
    
    init(app) {
        // Execute once during initialization to ensure that the refresh can render
        this.history.transitionTo(window.location.hash.slice(1))
    }

    // ... original code
}

Responsive hash change

Above, we have found all the components that need to be rendered according to the hash value, but the final rendering link has not been implemented yet. However, it is not urgent. Before rendering, we have completed one thing, that is, to make the change of hash value a responsive thing. Why? We just got the latest component collection for each hash change, but it's useless. Vue's component re rendering can only be triggered by a responsive change in some data. So we have to make a variable to save this component set, and this variable needs to be responsive. This variable is $route. Pay attention to distinguish it from $route!!! However, this $route needs to be obtained with two mediation variables, current and_ route

There may be a little detour here. Please be patient. I've shown the simplest of complex code.

// src/hashHistory.js

class HashHistory {
    constructor(router) {

        // ... original code

        // Assign an initial value to current at the beginning
        this.current = createRoute(null, {
            path: '/'
        })

    }
    
    // ... original code

    // Function triggered when jumping route
    transitionTo(location) {
        // ... original code

        // Assign a real value to current during hash update
        this.current = route
    }
    // Listening callback
    listen(cb) {
        this.cb = cb
    }
}
// src/my-router.js

class VueRouter {

    // ... original code
    
    init(app) {
        // Pass in the callback to ensure that each current change can be changed incidentally_ route trigger response
        this.history.listen((route) => app._route = route)
        
        // Execute once during initialization to ensure that the refresh can render
        this.history.transitionTo(window.location.hash.slice(1))
    }

    // ... original code
}

VueRouter.install = (Vue) => {
    _Vue = Vue
    // Mix in each component with Vue.mixin
    Vue.mixin({
        // Execute in the beforeCreate life cycle of each component
        beforeCreate() {
            if (this.$options.router) { // If root component

                // ... original code
                
                // Equivalent to existence_ And call the defineReactive method of Vue for responsive processing
                Vue.util.defineReactive(this, '_route', this.$router.history.current)
            } else {
                // ... original code
            }


        }
    })
    
    // Accessing $route is equivalent to accessing_ route
    Object.defineProperty(Vue.prototype, '$route', {
        get() {
            return this._routerRoot._route
        }
    })
}

Router view component rendering

In fact, the key to component rendering is the < router View > component. We can implement a < My View > by ourselves

Create view.js under src. The old rule is to talk about the idea first, and then implement the code

// src/view.js

const myView = {
    functional: true,
    render(h, { parent, data }) {
        const { matched } = parent.$route

        data.routerView = true // Identify this component as router view
        let depth = 0 // Depth index

        while(parent) {
            // If there is a parent component and the parent component is router view, the index needs to be increased by 1
            if (parent.$vnode && parent.$vnode.data.routerView) {
                depth++
            }
            parent = parent.$parent
        }
        const record = matched[depth]

        if (!record) {
            return h()
        }

        const component = record.component

        // Render components using render's h function
        return h(component, data)

    }
}
export default myView

Router link jump

In fact, his essence is just an a label

Create link.js under src

Final effect

Finally, change the introduction in router/index.js

import VueRouter from '../Router-source/index2'
Copy code

Then replace all router views and router links with my view and my link

epilogue

If you think this article is a little helpful to you, give it a compliment and encourage it, ha ha.

/src/my-router.js

import HashHistory from "./hashHistory"
class VueRouter {
    constructor(options) {

        this.options = options

        // If the mode is not transmitted, the default is hash
        this.mode = options.mode || 'hash'

        // What is the judgment mode
        switch (this.mode) {
            case 'hash':
                this.history = new HashHistory(this)
                break
            case 'history':
                // this.history = new HTML5History(this, options.base)
                break
            case 'abstract':

        }
    }
    init(app) {
        this.history.listen((route) => app._route = route)

        // Execute once during initialization to ensure that the refresh can render
        this.history.transitionTo(window.location.hash.slice(1))
    }

    // Get all corresponding components according to hash changes
    createMathcer(location) {
        const { pathMap } = createRouteMap(this.options.routes)

        const record = pathMap[location]
        const local = {
            path: location
        }
        if (record) {
            return createRoute(record, local)
        }
        return createRoute(null, local)
    }
}

let _Vue
VueRouter.install = (Vue) => {
    _Vue = Vue
    // Mix in each component with Vue.mixin
    Vue.mixin({
        // Execute in the beforeCreate life cycle of each component
        beforeCreate() {
            if (this.$options.router) { // If root component
                // this is the root component itself
                this._routerRoot = this

                // this.$options.router is the vueroter instance that is hung on the root component
                this.$router = this.$options.router

                // Execute the init method on the vueroter instance to initialize
                this.$router.init(this)

                // Equivalent to existence_ And call the defineReactive method of Vue for responsive processing
                Vue.util.defineReactive(this, '_route', this.$router.history.current)
            } else {
                // If it is not a root component, the parent component should also be_ routerRoot is saved to itself
                this._routerRoot = this.$parent && this.$parent._routerRoot
                // The sub components should also be hung with $router
                this.$router = this._routerRoot.$router
            }
        }
    })
    Object.defineProperty(Vue.prototype, '$route', {
        get() {
            return this._routerRoot._route
        }
    })
}

function createRouteMap(routes) {

    const pathList = []
    const pathMap = {}

    // Traverse the passed in routes array
    routes.forEach(route => {
        addRouteRecord(route, pathList, pathMap)
    })

    console.log(pathList)
    // ["/home", "/home/child1", "/home/child2", "/hello", "/hello/child1"]
    console.log(pathMap)
    // {
    //     /hello: {path: xxx, component: xxx, parent: xxx },
    //     /hello/child1: {path: xxx, component: xxx, parent: xxx },
    //     /hello/child2: {path: xxx, component: xxx, parent: xxx },
    //     /home: {path: xxx, component: xxx, parent: xxx },
    //     /home/child1: {path: xxx, component: xxx, parent: xxx }
    // }


    // Return pathList and pathMap
    return {
        pathList,
        pathMap
    }
}

function addRouteRecord(route, pathList, pathMap, parent) {
    // Splice path
    const path = parent ? `${parent.path}/${route.path}` : route.path
    const { component, children = null } = route
    const record = {
        path,
        component,
        parent
    }
    if (!pathMap[path]) {
        pathList.push(path)
        pathMap[path] = record
    }
    if (children) {
        // If there are children, execute addRouteRecord recursively
        children.forEach(child => addRouteRecord(child, pathList, pathMap, record))
    }
}

function createRoute(record, location) {
    const res = []
    if (record) {
        while (record) {
            res.unshift(record)
            record = record.parent
        }
    }
    return {
        ...location,
        matched: res
    }
}
export default VueRouter

src/hashHistory.js

class HashHistory {
    constructor(router) {

        // Save the vueroouter instance passed in
        this.router = router

        // Assign an initial value to current at the beginning
        this.current = createRoute(null, {
            path: '/'
        })

        // If the url does not #, it is automatically populated/#/ 
        ensureSlash()

        // Listen for hash changes
        this.setupHashLister()
    }
    // Monitor hash changes
    setupHashLister() {
        window.addEventListener('hashchange', () => {
            // Pass in the hash of the current url
            this.transitionTo(window.location.hash.slice(1))
        })
    }

    // Function triggered when jumping route
    transitionTo(location) {
        console.log(location)
        
        // Find all corresponding components
        let route = this.router.createMathcer(location)

        console.log(route)

        // Assign a real value to current during hash update
        this.current = route
        // Simultaneous update_ route
        this.cb && this.cb(route)
    }
    // Listening callback
    listen(cb) {
        this.cb = cb
    }
}

// If there is no #, it will be supplemented automatically/#/
function ensureSlash() {
    if (window.location.hash) {
        return
    }
    window.location.hash = '/'
}

export function createRoute(record, location) {
    const res = []
    if (record) {
        while (record) {
            res.unshift(record)
            record = record.parent
        }
    }
    return {
        ...location,
        matched: res
    }
}

export default HashHistory

src/view.js

const myView = {
    functional: true,
    render(h, { parent, data }) {
        const { matched } = parent.$route

        data.routerView = true // Identify this component as router view
        let depth = 0 // Depth index

        while(parent) {
            // If there is a parent component and the parent component is router view, the index needs to be increased by 1
            if (parent.$vnode && parent.$vnode.data.routerView) {
                depth++
            }
            parent = parent.$parent
        }
        const record = matched[depth]

        if (!record) {
            return h()
        }

        const component = record.component

        // Render components using render's h function
        return h(component, data)

    }
}
export default myView

src/link.js

const myLink = {
    props: {
        to: {
            type: String,
            required: true,
        },
    },
    // Render
    render(h) {

        // Render using render's h function
        return h(
            // Tag name
            'a',
            // Label properties
            {
                domProps: {
                    href: '#' + this.to,
                },
            },
            // Slot content
            [this.$slots.default]
        )
    },
}

export default myLink

Posted by Hipster on Sun, 03 Oct 2021 17:16:45 -0700