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

Posted by shazam on Thu, 09 Dec 2021 09:58:59 +0100

preface

Hello, I'm Lin Sanxin. I'm talking about the most difficult knowledge points in the most easy to understand words. I believe everyone must have used Vue router, that is, routing, in Vue project. 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 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. The other three sub components of Vue are similar. The difference is that the text and background colors are different, so they are not written out
<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

The following is the dynamic diagram of the effect achieved

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? Actually, it's similar to 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 to the root component App. That's the problem. Vue in the first step What is use (Vue router) for? Actually, Vue Use (xxx) is to execute the install method on XXX, that is, Vue use(VueRouter) === VueRouter. Install (), but when we get here, we know that install will execute, but we still don't know what it is for. What's the use?

We know that the VueRouter object is attached to the root component App, so the App can directly use the methods on the VueRouter object. However, we know that we definitely want every component used to use the VueRouter methods, such as this$ router. Push, but now only App can use these methods. What should I 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 points: Vue When use (xxx), the install method of XXX will be executed and Vue will be passed into the install method as a parameter

// src/my-router.js

let _Vue
VueRouter.install = (Vue) => {
    _Vue = Vue
    // Using Vue Mix in every component
    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 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
  • 3. abstract mode, mode in non browser environment

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. In the src folder js

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 can't. If you follow the above method, when the hash is / home/child1, only home-child1 will be rendered Vue is a component, but this is definitely not possible. When the hash is / home/child1, it must render home Vue and home Child1 Vue these two components

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
    // Using Vue Mix in every component
    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 a view under src JS, the old rule, first talk about the idea, and then realize 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. Under src 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

Final effect

Finally, route / index JS in the introduction of change

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

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

effect

epilogue

If you think this article is of little help to you, give a praise and encourage Lin Sanxin, ha ha. Or you can join my fishing group
If you want to join the learning group and fish, please click here [fish](
https://juejin.cn/pin/6969565... ), I will broadcast the mock interview regularly to answer questions and dispel doubts

Complete code

/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
    // Using Vue Mix in every component
    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 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

epilogue

Some people may think it's unnecessary, but it's actually necessary to be strict with yourself. Only by being strict with yourself at ordinary times can we achieve better downward compatibility in every company.

If you think this article is of little help to you, give a praise and encourage Lin Sanxin, ha ha.

If you want to learn front-end or fishing together, you can add me and join my fishing learning group

Topics: Javascript Front-end Vue.js Interview