4

前言

在使用vue全家桶进行开发时候,其中一个重要的插件就是vue-router,vue-router功能强大,并且使用方便,是我们构建项目时候不可或缺的一部分,那么vue-router到底是怎么实现的呢?那我们从源码出发,手撸一个基础的vue-router

基础用法

首先我们先看一下在项目中我们是如何使用router的

  • router文件
    在项目中我们通常都会有一个router文件夹在存放我们的router文件

        // src/router/index.js
        import Vue from'vue';
        import VueRouter from 'vue-router';
        import Page1 from'@/pages/detail1';
        import Page2 from'@/pages/detail2';
        
        // 创建VurRouter的插件
        Vue.use(VueRouter);
        
        // 配置router
        let router = [
            {
                path: '/',
                components: Page1,
            },
            {
                path: '/page2',
                components: Page2,
            }
        ]
        
        // 实例化VurRouter,并将配置注入,导出实例
        const Router = new VueRouter(router);
        
        export default Router;
  • vue根实例文件
    在实例化和挂载vue根实例的时候,我们会将vue-router引入到配置项

        // src/main.js
        
        import Vue from 'vue';
        import App from './App.vue';
        import router from './router';
        
        new Vue({
            router,
            render: h => h(App),
        }).$mount('#app')
  • 使用路由
    利用路由提供的组件,我们可以按照我们的需求展示和跳转路由

    <template>
    
        <div id="app">
    
            <div>
    
                <router-link to="/">page1</ router-link>
    
                <span>|</ span>
    
                <router-link to="/page2">page2</ router-link>
    
            </div>
    
            <router-view></ router-view>
    
        </ div>
    
    </ template>

手写vue-router

闲话说了不少,下面我们就开始手撸vue-router,let‘go!

  • 实现router的思路
    首先要实现router,我们必须清楚我们这次到底要做什么东西,先把他们列出来:

    1. 实现并导出一个Router实例,供外部调用
    2. 实现一个公共组件RouterView,用来加载路由视图
    3. 实现一个RouterLink,用来跳转路由
  • 整理一下每个组件的需求

    1. Router实例,需要接收router传入的options,将所有路由以及嵌套路由处理和收集起来,并监听url的变化。并且他作为插件存在,应该有一个install方法
    2. RouterView组件,根据自己的路由嵌套层级,找出对应的路由里面的组件,并展示出来
    3. RouterLink组件,创建一个a标签,插入写入的文本,并根据props中to的值进行跳转
  • 开始写代码

    1. 分析完需求,我们首先来写一下vue-router实例

      class VueRouter {
          constructor(options) {
              // 将传入路由配置保存在属性$options中,方便之后调用
              this.$options = options;
          }
      }

      创建了实例方法,我们开始写install方法

      // 将install中的参数Vue缓存起来,这么做原因有两个:
      // 1.这里的Vue实例在上面的VueRouter实例中也会用到,缓存起来可以直接使用
      // 2.因为插件是独立的,直接引入Vue会导致打包时候把Vue打包到插件中进去,增加代码打包后体积
      var Vue;
      
      VueRouter.install = function(_Vue) {
          Vue = _Vue;
          
          //使用mixin和生命周期beforeCreate做全局混入, 拿到实例后的相关属性并挂载到prototype上
          Vue.mixin({
              beforeCreate() {
                  if (this.$options.router) {
                      // 这里可以看出我们为何可以使用this.$router拿到router实例
                      // 也可以看出为何要在main.js中的根实例中将router实例作为options配置进去
                      Vue.prototype.$router = this.$options.router;
                  }
              }
          })
      }

      接下来我们接着完善router实例

      class VueRouter {
          constructor(options) {
              // 将传入路由配置保存在属性$options中,方便之后调用
              this.$options = options;
              
              // 这里我们利用hash的方法来构造路由地址,即我们常看到的http://localhost/#/xxx
              // 截取url,去掉#号
              this.current = window.location.hash.slice(1) ||'/';
              
              // 利用Vue中的defineReactive方法响应式的创建一个数组matched,存放当前路由以及其嵌套路由 
              Vue.util.defineReactive(this, 'matched', []);
              
              // 处理路由以及嵌套路由
              this.matchs(this.$options);
              
              // 监听拿到地址并修改地址
              window.addEventListener('hashchange', this.currentHash.bind(this));
              
              // 页面首次加载的时候也需要监听并修改地址
              window.addEventListener('load', this.currentHash.bind(this));
          }
          
          currentHash() {
              this.current = window.location.hash.slice(1);
              this.matched = [];
              this.matchs(this.$options);
          }
          
          matchs(routers) {
              // 递归处理路由
              // 输出结果一般是['parent', 'child1', 'child1-child1']
              routers.forEach(router => {
                  // 一般来说/路径下不会存放嵌套路由
                  if (this.current ==='/' && router.path === '/') {
                      this.matched.push(router);
                  } else if (this.current.includes(router.path) && router.path !== '/') {
                      this.matched.push(router);
                      if (router.children) {
                          this.matchs(router.children);
                      }
                  }
              }
          }
      }

      到这里,对照需求,router的实例基本完成。

    2. 我们接下来来写RouterView组件
      RouterView组件在实现的过程中,我们需要思考一个问题,就是怎么能找到对应的RouterView视图并匹配到对应的路由组件。这里我们根据matched存入的方式,引入一个概念,路由深度,表示路由的层级。

      // 由于是一个全局组件,我们选择写在install中,在配置插件时候一起注册
      
      VueRouter.install = function(_Vue) {
          Vue = _Vue;
          
          // 挂载router-view组件
          Vue.component('RouterView', {
              render(h) {
                  // 上面提到的深度标记,父组件深度为0
                  let deep = 0;
                  
                  // 将组件标记
                  this.routerView = true;
                  
                  // 循环找出所有RouterView的父元素 并标记深度
                  // 利用父子通信的中的$parent
                  let parent = this.$parent;
                  
                  // 循环查找父组件,以此标记当前组件的深度,当parent不存在时候,说明已经到了顶层组件,退出循环
                  while(parent) {
                      //  父组件存在且是routerView
                      if (parent && parent.routerView) {
                          deep+=1;
                      }
                      parent = parent.$parent;
                  }
                  
                  // 找到匹配深度层级,找出component
                  let matched = this.$router.matched;
                  let _component = null;
                  // 如果能在matched中找出,为_component赋值
                  if (matched[deep]) {
                      _component =  matched[deep].components;
                  }
                  
                  // 渲染_component
                  return h(_component);
              }
          })
      }
      

      到这一步RouterView也已经完成了,我们只剩下一个组件了

    3. 最后我们来实现一下RouterLink组件
      从需求中看,这个组件比起RouterView很简单了,他只需要渲染一个a标签,并展示对应文本并跳转,和RouterView一样,我们在install函数中写

      VueRouter.install = function(_Vue) {
          Vue = _Vue;
          
          // 挂载router-link组件
          Vue.component('RouterLink', {
              // 声明props,to并设置为必填
              props: {
                  to: {
                      type: String,
                      required: true
                  }
              },
              render(h) {
                  'a',
                  {
                      attrs: {
                          href: `#${this.to}`
                      }
                  },
                  // 利用匿名插槽将组件中文本写入
                  [
                      this.$slots.default
                  ]
              }
          })
      }

      RouterLink组件功能也完成了,是不是很简单呢?

完整代码

var Vue;

class VueRouter {
    constructor(options) {
        // 将传入路由配置保存在属性$options中,方便之后调用
        this.$options = options;

        // 这里我们利用hash的方法来构造路由地址,即我们常看到的http://localhost/#/xxx
        // 截取url,去掉#号
        this.current = window.location.hash.slice(1) ||'/';

        // 利用Vue中的defineReactive方法响应式的创建一个数组matched,存放当前路由以及其嵌套路由 
        Vue.util.defineReactive(this, 'matched', []);

        // 处理路由以及嵌套路由
        this.matchs(this.$options);

        // 监听拿到地址并修改地址
        window.addEventListener('hashchange', this.currentHash.bind(this));

        // 页面首次加载的时候也需要监听并修改地址
        window.addEventListener('load', this.currentHash.bind(this));
    }

    currentHash() {
        this.current = window.location.hash.slice(1);
        this.matched = [];
        this.matchs(this.$options);
    }

    matchs(routers) {
        // 递归处理路由
        // 输出结果一般是['parent', 'child1', 'child1-child1']
        routers.forEach(router => {
            // 一般来说/路径下不会存放嵌套路由
            if (this.current ==='/' && router.path === '/') {
                this.matched.push(router);
            } else if (this.current.includes(router.path) && router.path !== '/') {
                this.matched.push(router);
                if (router.children) {
                    this.matchs(router.children);
                }
            }
        }
    }
}


VueRouter.install = function(_Vue) {
    Vue = _Vue;

    //使用mixin和生命周期beforeCreate做全局混入, 拿到实例后的相关属性并挂载到prototype上
    Vue.mixin({
        beforeCreate() {
            if (this.$options.router) {
                // 这里可以看出我们为何可以使用this.$router拿到router实例
                // 也可以看出为何要在main.js中的根实例中将router实例作为options配置进去
                Vue.prototype.$router = this.$options.router;
            }
        }
    })
    
    // 挂载router-view组件
    Vue.component('RouterView', {
        render(h) {
            // 上面提到的深度标记,父组件深度为0
            let deep = 0;

            // 将组件标记
            this.routerView = true;

            // 循环找出所有RouterView的父元素 并标记深度
            // 利用父子通信的中的$parent
            let parent = this.$parent;

            // 循环查找父组件,以此标记当前组件的深度,当parent不存在时候,说明已经到了顶层组件,退出循环
            while(parent) {
                //  父组件存在且是routerView
                if (parent && parent.routerView) {
                    deep+=1;
                }
                parent = parent.$parent;
            }

            // 找到匹配深度层级,找出component
            let matched = this.$router.matched;
            let _component = null;
            // 如果能在matched中找出,为_component赋值
            if (matched[deep]) {
                _component =  matched[deep].components;
            }

            // 渲染_component
            return h(_component);
        }
    })
    
    // 挂载router-link组件
    Vue.component('RouterLink', {
        // 声明props,to并设置为必填
        props: {
            to: {
                type: String,
                required: true
            }
        },
        render(h) {
            'a',
            {
                attrs: {
                    href: `#${this.to}`
                }
            },
            // 利用匿名插槽将组件中文本写入
            [
                this.$slots.default
            ]
        }
    })
}

export default VueRouter;

写在最后

  • 到这里,这次的vue-router介绍就基本结束了。其实这里实现的只是一个最简单,最基础的vue-router,也是我在学习过程中的一个总结,旨在为大家做一个拓展和介绍。其中里面的重定向,路由懒加载以及路由守卫等都没有体现,大家可以按照这里分析的思路去看和学习源码。本人不才,文笔也一般,不敢说授人以渔,哪怕是给大家一个提示和启发也是极好的。如果不明白的地方,可以大家一起讨论,有错误的话欢迎大家指正,一起学习,一起进步。
  • 下一篇可能会写一篇关于vuex的实现,喜欢的同学们给个赞也是棒棒的,hhhhh。
  • 最后的最后,源码在我的github上,觉得有帮助的同学们可以给个小小的star。

Ekko_
130 声望11 粉丝

一个前端菜鸟,级别为前端中的负级