五分钟手写简版VueRouter(秒理解)

五分钟手写简版VueRouter(秒理解)

前言

Vue Router 是 Vue.js 官⽅的路由管理器。它和 Vue.js 的核⼼深度集成,让构建单⻚⾯应⽤变得很简单。

很多同学在面试的时候只能说出Router内部是依赖hash以及history这两种模式实现,但是内部原理就知其然而不知所以然了。本文通过手写简版Hash模式的VueRouter,快速拆分理解Router内部机制。

1.使用回顾

开始前,我们先来快速回顾VueRouter的使用

1.安装: vue add router

2.router.js

引入vue-router并使用
import Router from 'vue-router'
Vue.use(Router)
创建router实例并导出
const routes=[{...}]
export default new Router({routes})
复制代码

3.main.js中

引入router
import router from './router'
并在根组件上添加实例
new Vue({
router,
}).$mount("#app");
复制代码

4.步骤四:App.vue中添加路由视图及对应的代码编写

<router-view></router-view>
<router-link to="/">Home</router-link> <router-link to="/other">other</router-link>
this.$router.push('/')
this.$router.push('/other')
复制代码

2.问题

回顾完Router的使用,我们开始正文。但是在这之前,我们要先思考如下几个问题。

2.1 我们要通过VueRouter解决什么问题?

答:实现在单⻚⾯应⽤程序中,url地址发⽣变化的时候,页面不能刷新,但是显示与url地址对应的内容

2.2 那么根据上面的需求,我们有什么实现的办法,任务如何做拆分?

答:

  • url地址发生变化,页面不刷新:hash\history Api
  • 根据url地址显示对应内容:监听路由,拿到当前地址,从用户的配置项中找到对应componet,并在router-view中渲染

2.3 我们要实现一个VueRouter,我们有具体哪些工作要做呢?

答:

首先肯定是实现一个VueRouter类,在类的内部要做4件事情。

  • 保存用户在生成router实例时候的配置选项。
  • 监听当前的hash地址,并把该地址处理成响应式数据
  • 实现插件的install方法

    vueRouter是一个插件,插件在Vue中使用的时候是通过Vue.use()方法来实现注册,而Vue.use方法其实就是调用插件的install方法,并把Vue实例以参数形式向install方法传递,所以我们第一步要实现一个插件的install方法,方法内部就是我们要在挂载的时候的一些具体实现。

    • router实例的挂载

    • 注册两个router-link和router-view

      routerlink本质上就是渲染成一个a标签而router-view的功能就是根据当前的hash,在用户配置的路由选项中找到对应的componet组件,并渲染出来

3.代码实现

好,回答完上述问题呢,我们开始手撸一个简版的Vue-Router,同时各位也可以从VueRouter源码中去看真实的具体实现。

3.1 实现一个VueRouter类

  • 在constructor中保存配置项
  • 对当前路径current(hash)的初始化和响应式处理,供后面的的router-view组件寻找路由组件使用
class vueRouter {
  constructor(options) {
    // 选项中包含路由配置信息,保存
    this.$options = options;
    // 需要将current属性声明为响应式的
    //Vue.util.defineReactive是Vue提供的响应式方法
    Vue.util.defineReactive(
      this,
      "current",
      window.location.hash.slice(1) || "/"
    );
    // 2.监听hashchang事件,并且在变化的时候响应
    window.addEventListener("hashchange", () => {
      // hash: #/about
      this.current = window.location.hash.slice(1);
    });
  }
}
复制代码

3.2 实现vueRouter类的install方法,该方法会在vue.use时执行,包含2个功能,

  • 挂载实例router
let Vue
vueRouter.install=function(_vue){
  //保存vue.use时候传入的Vue构造函数,供后续使用
  Vue=_vue
  //由于在Vue.use(VueRouter)时,router实例并未生成(详见最开始的使用回顾),因此通过在use的时候通过Vue.mixin声明,在Vue实例化的时候才会去挂载router,也就是mian.js中的new Vue({router,render: h => h(App)}).$mount('#app')
  Vue.mixin({
    beforeCreate(){
      if(this.option.router) Vue.prototype.$router=this.option.router
    }
  })
}
复制代码
  • 注册router-link及router-view全局组件
let Vue
vueRouter.install=function(_vue){
  Vue=_vue  
  Vue.mixin({
    beforeCreate(){
      if(this.option.router) Vue.prototype.$router=this.option.router
    }
  })
  
 //注册router-link及router-view全局组件
  Vue.component("router-link", {
    //接受to参数,也就是我们写的 <router-link to="/about"></router-link>
    props: {
      to: {
        type: String,
        required: true,
      },
    },
    //component第二个参数可以通过render函数返回虚拟dom,
    render(h) {
      // h是createElement, 返回vnode
      // <router-link to="/about"></router-link>
      // <a href="#/about"></a>
      // return <a href={'#' + this.to}>{this.$slots.default}</a>
      //这里h函数表达的是渲染成一个 <a href="#/other"></a>的形式
      return h(
        "a",
        {
          attrs: {
            href: "#" + this.to,
          },
        },
        this.$slots.default
      );
    },
  });
  
  
  Vue.component("router-view", {
    render(h) {
      // 数据响应式:数据变化可侦听,使用这些数据组件就会和响应式数据产生依赖关系
      // 将来如果响应式数据发生变化,这些组件会重新渲染
      let component = null;
      //这里从this.$router中获取用户的配置项(也就是我们常写的路由表)中找到和当前路由实例路径current匹配的组件,把该组件通过h函数渲染出来
      //注意:这个current是响应式的,是window.location.hash截取出来的,只要这个current发生变化,那么这个组件就会重新渲染,达到路由切换的目的
      const route = this.$router.$options.routes.find(
        (route) => route.path === this.$router.current
      );
      if (route) {
        component = route.component;
      }
      // 1.获取hash部分,获取path
      // 2.根据path,从路由表中获取组件
      return h(component);
    },
  });
};
}
复制代码

至此,就实现了一个依赖Vue的响应式处理,对不同hash值匹配不同组件并在routerView组件中渲染的简易路由。当然真实源码考虑的情况要比现在的情况复杂的多,后续会补上嵌套路由的分析及具体实现。

4.完整代码

vue-router.js

let Vue;
// vue插件形式:
// 实现一个install方法,该方法会在use的时候被调用
class vueRouter {
  constructor(options) {
    // 选项中包含路由配置信息,保存
    this.$options = options;
    // 需要将current属性声明为响应式的
    Vue.util.defineReactive(
      this,
      "current",
      window.location.hash.slice(1) || "/"
    );
    // 2.监听hashchang事件,并且在变化的时候响应
    window.addEventListener("hashchange", () => {
      this.current = window.location.hash.slice(1);
    });
  }
}
​
// 形参1是Vue构造函数
VueRouter.install = function(_Vue) {
  // 传入构造函数,我们可以修改它的原型,起到扩展的作用
  Vue = _Vue;
​
  // install中this是KVueRouter
  // 1.注册$router
  // 延迟执行接下来代码,等到router实例创建之后
  // 全局混入:Vue.mixn
  Vue.mixin({
    beforeCreate() {
      // 此处this指的是组件实例
      if (this.$options.router) {
        Vue.prototype.$router = this.$options.router;
      }
    },
  });
​
  // 2.注册router-link和router-view全局组件
  Vue.component("router-link", {
    props: {
      to: {
        type: String,
        required: true,
      },
    },
    render(h) {
      // h是createElement, 返回vnode
      // 获取插槽内容
      // <router-link to="/about"></router-link>
      // <a href="#/ohter"></a>
      // return <a href={'#' + this.to}>{this.$slots.default}</a>
      return h(
        "a",
        {
          attrs: {
            href: "#" + this.to,
          },
        },
        this.$slots.default
      );
    },
  });
  Vue.component("router-view", {
    render(h) {
      // 数据响应式:数据变化可侦听,使用这些数据组件就会和响应式数据产生依赖关系
      // 将来如果响应式数据发生变化,这些组件会重新渲染
      // 0.获取router实例
      // console.log(this.$router.$options, this.$router.current);
      let component = null;
      const route = this.$router.$options.routes.find(
        (route) => route.path === this.$router.current
      );
      if (route) {
        component = route.component;
      }
      // 1.获取hash部分,获取path
      // 2.根据path,从路由表中获取组件
      return h(component);
    },
  });
};
​
export default VueRouter;
​
复制代码

おすすめ

転載: juejin.im/post/7049736617749118983