6
头图

Preface

Hello, everyone, I’m Lin Sanxin, uses the most easy-to-understand words to talk about the most difficult knowledge point . I believe everyone must have used Vue-router in the Vue project, which is routing. So in this article I won't explain too much about vue-router , and I will not explain vue-router to you, I will take you from scratch to implement a vue-router ! ! !

Basic usage of routing

We usually use vue-router a lot, basically every project will use it, because Vue is a single page application, you can switch components through routing to achieve the effect of switching pages. We usually use it this way, but it's actually divided into 3 steps

  • 1. Introduce vue-router and use Vue.use(VueRouter)
  • 2. Define the routing array, and pass the array to the VueRouter instance, and expose the instance
  • 3. VueRouter instance into main.js and register it on 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) // 第一步
    
    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 // 第二步
    })
    
    // src/main.js
    import router from './router'
    
    new Vue({
    router,  // 第三步
    render: h => h(App)
    }).$mount('#app')
    

router-view and router-link distribution

// src/App.vue

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

// src/components/home.vue

<template>
    <div style="background: green">
        <div>home的内容哦嘿嘿</div>
        <router-link to="/home/child1">home儿子1</router-link>
        <span style="margin: 0 10px">|</span>
        <router-link to="/home/child2">home儿子2</router-link>
        <router-view></router-view>
    </div>
</template>

// src/components/hello.vue

<template>
    <div style="background: orange">
        <div>hello的内容哦嘿嘿</div>
        <router-link to="/hello/child1">hello儿子1</router-link>
        <span style="margin: 0 10px">|</span>
        <router-link to="/hello/child2">hello儿子2</router-link>
        <router-view></router-view>
    </div>
</template>

// src/components/home-child1.vue 另外三个子组件大同小异,区别在于文本以及背景颜色不一样,就不写出来了
<template>
    <div style="background: yellow">我是home的1儿子home-child1</div>
</template>

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

  • 1. Enter the corresponding path at the URL, and the corresponding components will be displayed
  • 2. You can access $router and $router in any component used, and use the methods or attributes on them
  • 3. You can use the route-link component for path jump
  • 4. You can use the router-view component to display the corresponding content of the routing

截屏2021-09-25 下午3.46.32.png

The following is an animated picture of the achieved effect

router2.gif

Let's do it! ! !

VueRouter class

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

Options parameter VueRouter class, in fact, new VueRouter(options) when passed in this parameter object, and install is a method, and must VueRouter class has this method, why? We will talk about it below.

// src/my-router.js

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

VueRouter.install = (Vue) => {}

export default VueRouter

install method

Why must we define a install and assign it to VueRouter ? In fact, this is related to the Vue.use method. Do you still remember how Vue uses VueRouter?

import VueRouter from 'vue-router'

Vue.use(VueRouter) // 第一步

export default new VueRouter({ // 传入的options
    routes // 第二步
})

import router from './router'

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

In fact, the second and third steps are very clear. It is to instantiate a VueRouter object and attach this VueRouter object to the root component App. Then the question is, what is the use of Vue.use(VueRouter) in the first step? In fact Vue.use(XXX) , is executed XXX on install method, which is Vue.use (VueRouter) === VueRouter.install () , but to this, we know the install will perform, but still do not know install executed is What's the use?

We know that the VueRouter object is linked to the root component App, so the App can directly use the methods on the VueRouter object. However, as we know, we must think every component used in 161b8560e55ef1 can use the methods of VueRouter. For example, 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 ideas first, and then write the code.

截屏2021-09-25 下午10.20.09.png

Knowledge points: Vue.use(XXX) , the install method of XXX will be executed, and the Vue will be used as the parameter to the install method

// src/my-router.js

let _Vue
VueRouter.install = (Vue) => {
    _Vue = Vue
    // 使用Vue.mixin混入每一个组件
    Vue.mixin({
        // 在每一个组件的beforeCreate生命周期去执行
        beforeCreate() {
            if (this.$options.router) { // 如果是根组件
                // this 是 根组件本身
                this._routerRoot = this

                // this.$options.router就是挂在根组件上的VueRouter实例
                this.$router = this.$options.router

                // 执行VueRouter实例上的init方法,初始化
                this.$router.init(this)
            } else {
                // 非根组件,也要把父组件的_routerRoot保存到自身身上
                this._routerRoot = this.$parent && this.$parent._routerRoot
                // 子组件也要挂上$router
                this.$router = this._routerRoot.$router
            }
        }
    })
}

createRouteMap method

What is this method for? As the name implies, it is to convert the incoming routes array into a Map structure. The key is the path and the value is the corresponding component information. As for why the conversion is necessary? We will talk about this below. Let's implement the conversion first.

截屏2021-09-25 下午10.47.42.png

// src/my-router.js

function createRouteMap(routes) {

    const pathList = []
    const pathMap = {}

    // 对传进来的routes数组进行遍历处理
    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 }
    // }


    // 将pathList与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) {
        // 如果有children,则递归执行addRouteRecord
        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, a mode that requires back-end cooperation
  • 3. abstract mode, non-browser environment mode

And how to set the mode? It is set like this, and passed in through the mode field of options

export default new VueRouter({
    mode: 'hash' // 设置模式
    routes
})

And if you don’t pass it, the default is hash mode, which is also the most used mode in our usual development, so this chapter only implements hash mode

// src/my-router.js

import HashHistory from "./hashHistory"

class VueRouter {
    constructor(options) {
        
        this.options = options
        
        // 如果不传mode,默认为hash
        this.mode = options.mode || 'hash'

        // 判断模式是哪种
        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

hashHistory.js under the src folder

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

class HashHistory {
    constructor(router) {

        // 将传进来的VueRouter实例保存
        this.router = router

        // 如果url没有 # ,自动填充 /#/ 
        ensureSlash()
        
        // 监听hash变化
        this.setupHashLister()
    }
    // 监听hash的变化
    setupHashLister() {
        window.addEventListener('hashchange', () => {
            // 传入当前url的hash,并触发跳转
            this.transitionTo(window.location.hash.slice(1))
        })
    }

    // 跳转路由时触发的函数
    transitionTo(location) {
        console.log(location) // 每次hash变化都会触发,可以自己在浏览器修改试试
        // 比如 http://localhost:8080/#/home/child1 最新hash就是 /home/child1
    }
}

// 如果浏览器url上没有#,则自动补充/#/
function ensureSlash() {
    if (window.location.hash) {
        return
    }
    window.location.hash = '/'
}

// 这个先不讲,后面会用到
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, every time the hash is modified, the latest hash value can be obtained, but this is not our ultimate goal. Our ultimate goal is to render different component pages according to the hash changes. What should we do?

Remember the previous createRouteMap method? We converted the routes array into the Map data structure. With that Map, we can get the corresponding components based on the hash value and perform rendering

截屏2021-09-26 下午9.26.44.png

But is this really okay? In fact, it doesn’t work. If you follow the above method, when the hash is /home/child1 , only the home-child1.vue will be rendered, but this is definitely not working. When the hash is /home/child1 , the two components home.vue and home-child1.vue must be rendered.

截屏2021-09-26 下午9.30.57.png

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

// src/my-router.js

class VueRouter {
    
    // ....原先代码

    // 根据hash变化获取对应的所有组件
    createMathcer(location) {
    
        // 获取 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)
    }
}

// ...原先代码

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 {
    
    // ...原先代码

    // 跳转路由时触发的函数
    transitionTo(location) {
        console.log(location)
        
        // 找出所有对应组件,router是VueRouter实例,createMathcer在其身上
        let route = this.router.createMathcer(location)

        console.log(route)
    }
}

截屏2021-09-26 下午9.51.01.png

This just ensures that all the corresponding components can be found when the hash changes, but one thing we overlooked is that if we refresh the page manually, the hashchange event will not be triggered, that is, the component cannot be found. What should I do? Refreshing the page will definitely re-initialize the routing. We only need to perform an in-place jump initialization function init.

// src/my-router.js

class VueRouter {

    // ...原先代码
    
    init(app) {
        // 初始化时执行一次,保证刷新能渲染
        this.history.transitionTo(window.location.hash.slice(1))
    }

    // ...原先代码
}

Responsive hash change

Above, we have realized that all the components that need to be rendered are found based on the hash value of , but the final rendering link has not been implemented yet, but no hurry. Before rendering, let’s complete one thing first, that is, let hash The value change thing becomes a responsive thing, why? We just got the latest component collection every time the hash changes, but it's useless. The re-rendering of Vue components can only be triggered by a responsive change of certain data. So we have to make a variable to save this component collection, and this variable needs to be responsive, this variable is $route , pay attention to distinguish it from $router ! ! But this $route needs to be obtained with two intermediate variables, namely current and _route

It may be a little bit convoluted here, but I hope everyone will be patient. I have shown the most simple complex code.
// src/hashHistory.js

class HashHistory {
    constructor(router) {

        // ...原先代码

        // 一开始给current赋值初始值
        this.current = createRoute(null, {
            path: '/'
        })

    }
    
    // ...原先代码

    // 跳转路由时触发的函数
    transitionTo(location) {
        // ...原先代码

        // hash更新时给current赋真实值
        this.current = route
    }
    // 监听回调
    listen(cb) {
        this.cb = cb
    }
}
// src/my-router.js

class VueRouter {

    // ...原先代码
    
    init(app) {
        // 把回调传进去,确保每次current更改都能顺便更改_route触发响应式
        this.history.listen((route) => app._route = route)
        
        // 初始化时执行一次,保证刷新能渲染
        this.history.transitionTo(window.location.hash.slice(1))
    }

    // ...原先代码
}

VueRouter.install = (Vue) => {
    _Vue = Vue
    // 使用Vue.mixin混入每一个组件
    Vue.mixin({
        // 在每一个组件的beforeCreate生命周期去执行
        beforeCreate() {
            if (this.$options.router) { // 如果是根组件

                // ...原先代码
                
                // 相当于存在_routerRoot上,并且调用Vue的defineReactive方法进行响应式处理
                Vue.util.defineReactive(this, '_route', this.$router.history.current)
            } else {
                // ...原先代码
            }


        }
    })
    
    // 访问$route相当于访问_route
    Object.defineProperty(Vue.prototype, '$route', {
        get() {
            return this._routerRoot._route
        }
    })
}

router-view component rendering

In fact, the key to component rendering lies in the <router-view> component. We can implement a <my-view>

Create src view.js , the old rules, first talk about ideas, and then implement the code

截屏2021-09-26 下午11.07.10.png

// src/view.js

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

        data.routerView = true // 标识此组件为router-view
        let depth = 0 // 深度索引

        while(parent) {
            // 如果有父组件且父组件为router-view 说明索引需要加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的h函数进行渲染组件
        return h(component, data)

    }
}
export default myView

router-link jump

In fact, his essence is just an a tag

In src created under link.js

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

        // 使用render的h函数渲染
        return h(
            // 标签名
            'a',
            // 标签属性
            {
                domProps: {
                    href: '#' + this.to,
                },
            },
            // 插槽内容
            [this.$slots.default]
        )
    },
}

export default myLink

final effect

Finally, change the introduction in router/index.js

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

Then replace all router-view and router-link with my-view and my-link

Effect

router2.gif

Concluding remarks

If you think this article is of little help to you, please give me a thumbs up and encourage Lin Sanxin haha. Or you can join my fish school
If you want to join the learning group and catch the fish, please click here [moyu](
https://juejin.cn/pin/6969565162885873701), I will broadcast the mock interview regularly to answer questions

image.png

Complete code

/src/my-router.js

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

        this.options = options

        // 如果不传mode,默认为hash
        this.mode = options.mode || 'hash'

        // 判断模式是哪种
        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)

        // 初始化时执行一次,保证刷新能渲染
        this.history.transitionTo(window.location.hash.slice(1))
    }

    // 根据hash变化获取对应的所有组件
    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
    // 使用Vue.mixin混入每一个组件
    Vue.mixin({
        // 在每一个组件的beforeCreate生命周期去执行
        beforeCreate() {
            if (this.$options.router) { // 如果是根组件
                // this 是 根组件本身
                this._routerRoot = this

                // this.$options.router就是挂在根组件上的VueRouter实例
                this.$router = this.$options.router

                // 执行VueRouter实例上的init方法,初始化
                this.$router.init(this)

                // 相当于存在_routerRoot上,并且调用Vue的defineReactive方法进行响应式处理
                Vue.util.defineReactive(this, '_route', this.$router.history.current)
            } else {
                // 非根组件,也要把父组件的_routerRoot保存到自身身上
                this._routerRoot = this.$parent && this.$parent._routerRoot
                // 子组件也要挂上$router
                this.$router = this._routerRoot.$router
            }
        }
    })
    Object.defineProperty(Vue.prototype, '$route', {
        get() {
            return this._routerRoot._route
        }
    })
}

function createRouteMap(routes) {

    const pathList = []
    const pathMap = {}

    // 对传进来的routes数组进行遍历处理
    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 }
    // }


    // 将pathList与pathMap返回
    return {
        pathList,
        pathMap
    }
}

function addRouteRecord(route, pathList, pathMap, parent) {
    // 拼接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) {
        // 如果有children,则递归执行addRouteRecord
        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) {

        // 将传进来的VueRouter实例保存
        this.router = router

        // 一开始给current赋值初始值
        this.current = createRoute(null, {
            path: '/'
        })

        // 如果url没有 # ,自动填充 /#/ 
        ensureSlash()

        // 监听hash变化
        this.setupHashLister()
    }
    // 监听hash的变化
    setupHashLister() {
        window.addEventListener('hashchange', () => {
            // 传入当前url的hash
            this.transitionTo(window.location.hash.slice(1))
        })
    }

    // 跳转路由时触发的函数
    transitionTo(location) {
        console.log(location)
        
        // 找出所有对应组件
        let route = this.router.createMathcer(location)

        console.log(route)

        // hash更新时给current赋真实值
        this.current = route
        // 同时更新_route
        this.cb && this.cb(route)
    }
    // 监听回调
    listen(cb) {
        this.cb = cb
    }
}

// 如果浏览器url上没有#,则自动补充/#/
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 // 标识此组件为router-view
        let depth = 0 // 深度索引

        while(parent) {
            // 如果有父组件且父组件为router-view 说明索引需要加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的h函数进行渲染组件
        return h(component, data)

    }
}
export default myView

src/link.js

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

        // 使用render的h函数渲染
        return h(
            // 标签名
            'a',
            // 标签属性
            {
                domProps: {
                    href: '#' + this.to,
                },
            },
            // 插槽内容
            [this.$slots.default]
        )
    },
}

export default myLink

Concluding remarks

Some people may think it is unnecessary, but it is actually necessary to strictly demand oneself. Only by strict requirements of oneself at ordinary times can each company be better able to achieve backward compatibility.

If you think this article is of little help to you, please give me a thumbs up and encourage Lin Sanxin haha.

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

image.png


Sunshine_Lin
2.1k 声望7.1k 粉丝