关于前端:7张图从零实现一个简易版VueRouter太通俗易懂了

9次阅读

共计 13334 个字符,预计需要花费 34 分钟才能阅读完成。

前言

大家好,我是林三心,用最通俗易懂的话,讲最难的知识点 ,置信大家在 Vue 我的项目中必定都用过Vue-router,也就是路由。所以本文章我就不过多解说vue-router 的根本解说了,我也不给你们解说 vue-router 的源码,我就带大家 从零开始,实现一个 vue-router吧!!!

路由根本应用办法

平时咱们 vue-router 其实都用很多了,根本每个我的项目都会用它,因为 Vue 是单页面利用,能够通过路由来实现切换组件,达到切换页面的成果。咱们平时都是这么用的,其实分为 3 步

  • 1、引入vue-router,并应用Vue.use(VueRouter)
  • 2、定义路由数组,并将数组传入VueRouter 实例,并将实例裸露进来
  • 3、将 VueRouter 实例引入到 main.js,并注册到根 Vue 实例上

    // 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 和 router-link的散布

// 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>

通过下面这 3 步,咱们能实现什么成果呢?

  • 1、在网址处输出对应 path,就会展现对应组件
  • 2、能够在任何用到的组件里拜访到$router 和 $router,并应用其身上的办法或属性
  • 3、能够应用 route-link 组件进行门路跳转
  • 4、能够应用 router-view 组件进行路由对应内容展现

以下是达到的成果动图

开搞!!!

VueRouter 类

在 src 文件夹中,创立一个my-router.js

VueRouter 类的 options 参数,其实就是 new VueRouter(options) 时传入的这个参数对象,而 install 是一个办法,并且必须使 VueRouter 类 领有这个办法,为什么呢?咱们上面会讲的。

// src/my-router.js

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

VueRouter.install = (Vue) => {}

export default VueRouter

install 办法

为什么必须定义一个 install 办法,并且把他赋予 VueRouter 呢?其实这跟 Vue.use 办法无关,大家还记得 Vue 是怎么应用 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')

其实第二步和第三步很分明,就是实例一个 VueRouter 对象,并且将这个 VueRouter 对象挂到根组件 App 上,那问题来了,第一步的 Vue.use(VueRouter)是干什么用的呢?其实 Vue.use(XXX),就是执行XXX 上的 install 办法,也就是 Vue.use(VueRouter) === VueRouter.install(),然而到了这,咱们是晓得了install 会执行,然而还是不晓得 install 执行了是干嘛的,有什么用?

咱们晓得 VueRouter 对象是被挂到根组件 App 上了,所以 App 是能间接应用 VueRouter 对象上的办法的,然而,咱们晓得,咱们必定是想 每一个用到的组件 都能应用 VueRouter 的办法,比方 this.$router.push,然而当初只有 App 能用这些办法,咋办呢?咋能力每个组件都能应用呢?这时install 办法派上用场了,咱们先说说实现思路,再写代码哈。

知识点:Vue.use(XXX)时,会执行 XXX 的 install 办法,并将 Vue 当做 参数 传入 install 办法

// 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 办法

这个办法是干嘛的呢?顾名思义,就是将传进来的 routes 数组 转成一个 Map 构造 的数据结构,key 是 path,value 是对应的组件信息,至于为什么要转换呢?这个咱们上面会讲。咱们先实现转换。

// 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

路由模式

路由有三种模式

  • 1、hash 模式,最罕用的模式
  • 2、history 模式,须要后端配合的模式
  • 3、abstract 模式,非浏览器环境的模式

而且模式怎么设置呢?是这么设置的,通过 options 的 mode 字段传进去

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

而如果不传的话,默认是hash 模式,也是咱们平时开发中用的最多的模式,所以本章节就只实现hash 模式

// 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

在 src 文件夹下创立hashHistory.js

其实 hash 模式的原理就是,监听浏览器 url 中 hash 值的变动,并切换对应的组件

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 办法

下面讲了,每次 hash 批改,都能获取到最新的 hash 值,然而这不是咱们的最终目标,咱们最终目标是依据 hash 变动渲染不同的组件页面,那怎么办呢?

还记得之前 createRouteMap 办法吗?咱们将 routes 数组 转成了 Map 数据结构,有了那个 Map,咱们就能够依据 hash 值去获取对应的组件并进行渲染

然而这样真的能够吗?其实是不行的,如果依照下面的办法,当 hash 为 /home/child1 时,只会渲染 home-child1.vue 这一个组件,但这样必定是不行的,当 hash 为 /home/child1 时,必定是渲染 home.vuehome-child1.vue这两个组件

所以咱们得写一个办法,来查找 hash 对应哪些组件,这个办法就是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)
    }
}

这只是保障了 hash 变动 的时候能找出对应的所有组件来,然而有一点咱们疏忽了,那就是咱们如果手动刷新页面的话,是不会触发 hashchange 事件的,也就是找不出组件来,那咋办呢?刷新页面必定会使路由从新初始化,咱们只须要在 初始化函数 init上一开始执行一次原地跳转就行。

// src/my-router.js

class VueRouter {

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

    // ... 原先代码
}

响应式的 hash 扭转

下面咱们实现了依据 hash 值 找出所有须要渲染的组件,但最初的渲染环节却还没实现,不过不急,实现渲染之前,咱们先把一件事给实现了,那就是要让 hash 值扭转 这件事变成一件 响应式的事 ,为什么呢?咱们刚刚每次 hash 变动是能拿到最新的 组件合集 ,然而没用啊,Vue 的组件从新渲染只能通过某个数据的响应式变动来触发。所以咱们得搞个变量来保留这个 组件合集 ,并且这个变量须要是响应式的才行,这个变量就是$route,留神要跟$router 区别开来哦!!!然而这个 $route 须要用两个中介变量来获取,别离是current 和_route

这里可能会有点绕,还望大家有点急躁。我曾经把简单的代码最简单化展现了。

// 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 组件渲染

其实组件渲染关键在于 <router-view> 组件,咱们能够本人实现一个<my-view>

src 下创立view.js,老规矩,先说说思路,再实现代码

// 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 跳转

其实他的实质就是个 a 标签而已

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

最终成果

最初把 router/index.js 里的引入改一下

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

而后把所有 router-view 和 router-link 全都替换成my-view 和 my-link

成果

结语

如果你感觉此文对你有一丁点帮忙,点个赞,激励一下林三心哈哈。或者能够退出我的摸鱼群
想进学习群,摸鱼群,请点击这里[摸鱼](
https://juejin.cn/pin/6969565…),我会定时直播模仿面试,答疑解惑

残缺代码

/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

结语

有人可能感觉没必要,然而严格要求本人其实是很有必要的,平时严格要求本人,能力做到每到一个公司都能更好的做到向下兼容难度。

如果你感觉此文对你有一丁点帮忙,点个赞,激励一下林三心哈哈。

如果你想一起学习前端或者摸鱼,那你能够加我,退出我的摸鱼学习群

正文完
 0