一文了解vue-router实现原理

这篇文章主要介绍一下vue-router插件的实现原理。

vue路由的工作流程

首先了解一下vue路由的工作流程,路由分为后端路由和前端路由。

后端路由

  1. 在地址栏中输入url
  2. 该请求会被直接发送到服务器
  3. 服务器解析请求中的路径并返回对应的页面

缺点:发送请求次数过多会对后台服务器造成比较大的压力,所以就出现了前端路由。

前端路由

  1. 在地址栏中输入url
  2. 该地址会被js进行解析
  3. 找到对应的页面后进行渲染

路由模式

前端路由的两种模式:HashHistory

hash模式

  1. #号后面的就是hash值的内容
  2. 可以通过location.hash获取地址栏中的hash值
  3. 可以通过window.onhashchange监听hash值的改变

history模式

  1. 地址栏/后即为正常的路径
  2. 可以通过location.pathname获取地址栏中的路径
  3. 可以通过window.onpopstate监听路由值的改变

vue路由的工作原理

graph TD url改变 --> 触发监听事件 --> 改变vue-router中的currentRoute变量 --> vue监听currentRoute变量 --> 获取到对应的组件 -->render新组件

手写vue-router

列一下我们总共需要了解的知识点:

  • vue.use()注册插件机制
  • vueRouter构造函数
  • 两个全局组件:router-link和router-view
  • main.js中为什么需要注入vueRouter的实例

vue.use()


vue.use方法在注册插件机制时使用的,它接受两个参数,第一个参数是插件本身,第二个参数是要传递给插件的选项。

插件本身可以是一个函数也可以是一个对象,当是一个函数时use方法会立即执行该方法:

js 复制代码
// 可以在main.js文件中进行测试
function a(){
    console.log(100)
}
Vue.use(a) // 100

如果是一个对象的话则对象本身必须有一个install的方法,然后use会直接执行传入对象中的install方法,并且install方法中的第一个参数为vue的构造函数,有了vue的构造函数我们就可以在里面使用vue相关的一些api:

js 复制代码
let obj = {
  install:function(vue){
    console.log(120)
    console.log(vue)
  }
}
Vue.use(obj) // 120 function Vue(options) {...}

// 或者是
function a(){
    console.log(100)
}
a.install = function(120){
    console.log(120)
}
Vue.use(a) // 120

最后如果对同一个插件进行重复注册,那么该插件只会被安装一次,下面来看一下这块儿的源码:

可以直接在node_modules中搜索vue关键字,然后找到dist文件夹下的vue.js文件。

可以看到前三行代码就是用来防止重复注册插件,然后往下可以看到把this直接放在了参数列表中的第一位也就是install方法的第一个参数,再往下就是判断插件本身是否有install属性并且是一个方法并进行执行。

vueRouter构造函数

在router.js中我们可以看到:

js 复制代码
const router = new VueRouter({});

说明Vuerouter是一个构造函数,下面我们来自己实现一个VueRouter的构造函数。

首先新建一个myRouter的文件夹以及它下面的index.js文件并进行路径的修改:

在index.js文件中我们使用类来实现:

js 复制代码
class VueRouter {
    constructor() {

    }
}
// 增加install方法
VueRouter.install = function (_vue) {
    
}
export default VueRouter;

这时页面提示有报错:

也就是缺少了router-link和router-view两个全局组件,下面来实现一下。

实现router-link标签

继续在之前的代码中进行扩展:

js 复制代码
let Vue;
class VueRouter {
    constructor() {

    }
}
// 增加install方法
VueRouter.install = function (_vue) {
    Vue = _vue;
    // 创建两个全局组件
    Vue.component('router-link', {
        // 使用render函数创建 router-link相当于a标签
        render(h) {
            return h('a', 'router-link')
        }
    })
    Vue.component('router-view', {
        // 使用render函数创建 router-view相当于占位符
        render(h) {
            return h('div', 'router-view')
        }
    })
}
export default VueRouter;

先使用Vue.component方法创建两个全局组件,这样就不会再有报错信息了。

router-link作为一个a标签应该具有超链接的属性,所以我们可以使用h函数的第二个参数为其设置属性(不了解h函数的我就没办法喽h函数

js 复制代码
// 以下为关键代码,其余的同上面保持一致
 Vue.component('router-link', {
        render(h) {
            return h('a', {
                attrs: { href: '#' }
            }, 'router-link')
        }
    })

如图所示:

解决完这个问题后接下来就是跳转的问题了,在vue中router-link的跳转是通过给组件传递一个to属性来实现跳转:

js 复制代码
    <router-link to="/">home</router-link> | 
    <router-link to="/about">about</router-link>

那我们可以在自己创建的全局组件中获取到该属性并实现地址栏路由的改变:

js 复制代码
   Vue.component('router-link', {
        // 在当前组件中使用props接受传递的值
        props:{
            to:{
                required:true,
                type:String,
            }
        },
        render(h) {
            return h('a', {
                // this指向的是当前router-link组件的实例
                attrs: { href: '#'+this.to }
            }, 'router-link')
        }
    })

这时点击就可以在地址栏中看到地址栏路由的变化。

那如何在页面中显示我们自定义的名称呢?

那自然而然应该想到插槽的概念了。

js 复制代码
 Vue.component('router-link', {
        props: {
            to: {
                required: true,
                type: String,
            }
        },
        render(h) {
            return h('a', {
                attrs: { href: '#' + this.to }
            }, this.$slots.default) // 使用默认插槽
        }
    })

router-link标签到这里就结束了,下面再来实现router-view标签。

实现router-view标签

实现router-view主要是通过监听路由的改变,使用当前的路由获取到对应的组件并进行渲染。

js 复制代码
class VueRouter {
    constructor(options) {
        // 当前的路由
        this.currentRoute = '';
        // 用户定义的路由规则
        this.routes = options.routes;
        // 路由模式
        this.mode = options.mode || 'hash';
        // 监听路由变化
        this.init()
    }
    init() {
        if (this.mode == 'hash') {
            window.addEventListener("hashchange", () => {
                this.currentRoute = location.hash.slice(1); // 去除前面的#号与用户配置的路由规则表里保持一致
                console.log(this.currentRoute);
            })
        }
    }
}

这时候已经完成了路由的监听,在页面中切换会发现地址栏路由会发生变化并且可以获取到当前的路由值,接下来就是根据当前路由获取对应的组件。

那么现在的问题就是如何在router-view全局组件中获取路由信息,这时候我们可以使用Vue.mixin方法进行全局混入,通过全局混入的方法可以在里面给每个组件设置路由的相关信息。在Vue中我们会在main.js文件中把router实例传递给Vue构造函数:

js 复制代码
new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

通过this.$options可以获取vue中所有实例组件的信息

js 复制代码
VueRouter.install = function (_vue) {
    Vue = _vue;
    Vue.mixin({
        // 在每个组件的beforeCreate钩子函数中设置
        beforeCreate() {
            console.log(this.$options.name)
        }
    })
    Vue.component('router-view', {
        render(h) {
            return('div','router-view');
        }
    })
}

打印结果自上而下分别为main.js文件、App.vue、router-link、router-view组件,因为我们可以获取到main.js文件中的配置项,所以也就能获取到用户配置的路由信息,之后再通过mixin方法共享给其他所有的组件,这就是为什么需要在main.js文件中注入vueRouter的实例

js 复制代码
VueRouter.install = function (_vue) {
    Vue = _vue;
    Vue.mixin({
        // 在每个组件的beforeCreate钩子函数中设置
        beforeCreate() {
              if (this.$options.router) { // 判断是否是根实例
                Vue.prototype.$router = this.$options.router // 让每个组件实例都可以访问路由信息
            }
        }
    })
    Vue.component('router-view', {
        render(h) {
            const currentRoute = this.$router.currentRoute;
            const routes = this.$router.routes;
            // 获取到当前的路由信息与所有的路由信息进行匹配
            const obj = routes.find(item => {
                return item.path == currentRoute
            });
            return h(obj.component)
        }
    })
}

这时候组件已经渲染成功了,但是切换的时候组件并没有更新,这是因为currentRoute这个变量不是响应式的,所以我们需要把它变为响应式数据,可以直接使用Vue中提供的api:

js 复制代码
let Vue;
class VueRouter {
    constructor(options) {
        // this.currentRoute = '/';
        Vue.util.defineReactive(this, 'currentRoute', '/') // 使用defineReactive方法定义currentRoute变量
        // 用户定义的路由规则
        this.routes = options.routes;
        // 路由模式
        this.mode = options.mode || 'hash';
        // 监听路由变化
        this.init()
    }
  }

完整代码

js 复制代码
let Vue;
class VueRouter {
    constructor(options) {
        // 当前的路由
        // this.currentRoute = '/';
        Vue.util.defineReactive(this, 'currentRoute', '/')
        // 用户定义的路由规则
        this.routes = options.routes;
        // 路由模式
        this.mode = options.mode || 'hash';
        // 监听路由变化
        this.init()
    }
    init() {
        if (this.mode == 'hash') {
            window.addEventListener("hashchange", () => {
                this.currentRoute = location.hash.slice(1); // 去除前面的#号与用户配置的路由规则表里保持一致
            })
        }
    }
}
VueRouter.install = function (_vue) {
    Vue = _vue;
    Vue.mixin({
        beforeCreate() {
            if (this.$options.router) {
                Vue.prototype.$router = this.$options.router
            }
        }
    })
    Vue.component('router-link', {
        props: {
            to: {
                required: true,
                type: String,
            }
        },
        render(h) {
            return h('a', {
                attrs: { href: '#' + this.to }
            }, this.$slots.default)
        }
    })
    Vue.component('router-view', {
        render(h) {
            const currentRoute = this.$router.currentRoute;
            const routes = this.$router.routes;
            const obj = routes.find(item => {
                return item.path == currentRoute
            });
            return h(obj.component)
        }
    })
}
export default VueRouter;

上述代码只是讲解了以下vue-router比较核心的一些东西,想了解更多的知识可以查看vue-router的源码。

相关推荐
ziyue75754 分钟前
vue修改element-ui的默认的class
前端·vue.js·ui
树叶会结冰25 分钟前
HTML语义化:当网页会说话
前端·html
冰万森30 分钟前
解决 React 项目初始化(npx create-react-app)速度慢的 7 个实用方案
前端·react.js·前端框架
牧羊人_myr43 分钟前
Ajax 技术详解
前端
浩男孩1 小时前
🍀封装个 Button 组件,使用 vitest 来测试一下
前端
蓝银草同学1 小时前
阿里 Iconfont 项目丢失?手把手教你将已引用的 SVG 图标下载到本地
前端·icon
布列瑟农的星空1 小时前
重学React —— React事件机制 vs 浏览器事件机制
前端
程序定小飞2 小时前
基于springboot的在线商城系统设计与开发
java·数据库·vue.js·spring boot·后端
一小池勺2 小时前
CommonJS
前端·面试