前言

大家好,我是林三心,用最通俗易懂的话,讲最难的知识点,置信大家在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.jsimport 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.jsimport 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.jsclass 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.jslet _VueVueRouter.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.jsfunction 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.jsimport 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.jsclass 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.jsclass HashHistory {        // ...原先代码    // 跳转路由时触发的函数    transitionTo(location) {        console.log(location)                // 找出所有对应组件,router是VueRouter实例,createMathcer在其身上        let route = this.router.createMathcer(location)        console.log(route)    }}

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

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

响应式的hash扭转

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

这里可能会有点绕,还望大家有点急躁。我曾经把简单的代码最简单化展现了。
// src/hashHistory.jsclass HashHistory {    constructor(router) {        // ...原先代码        // 一开始给current赋值初始值        this.current = createRoute(null, {            path: '/'        })    }        // ...原先代码    // 跳转路由时触发的函数    transitionTo(location) {        // ...原先代码        // hash更新时给current赋实在值        this.current = route    }    // 监听回调    listen(cb) {        this.cb = cb    }}
// src/my-router.jsclass 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.jsconst 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 _VueVueRouter.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

结语

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

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

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