前端架构学习笔记(一)简单实现 VueRouter

学习笔记(一)简单实现 VueRouter

本系列笔记来源,开课吧前端架构课程

这个练习非常简单,只考虑 history 模式下路由和嵌套路由的实现。主要目的是理解路由的核心思想。至于更完善,更深入的内容,可以在学习完这些内容之后,阅读源码。

如果有问题,或者有更好的解决方案,欢迎大家分享和指教。

*最终效果

我们在这个练习中需要实现的效果如下:

  • 考虑 hash 模式
  • 实现 router-link 组件通过 to 属性切换路由
  • 实现 router-view 渲染路由对应的组件
  • 实现 在任意组件内通过 this.$router.push(path) 的方式,跳转路由
  • 实现嵌套路由

目录

在 src 下新建以下文件和文件夹

my-router
  my-router.js  # 封装 VueRouter 类
  my-router-view.js # 封装 router-view
  index.js # 配置路由

main.js 中配置如下:

import Vue from 'vue'
import App from './App.vue'
import router from './my-router/index'

Vue.config.productionTip = false

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

my-router/index.js

import Vue from 'vue'
import Router from './my-router'
import Home from '../views/Home.vue'

Vue.use(Router)

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    {
      path: '/about',
      name: 'about',
      component: () => import('../views/About.vue'),
      children: [
        {
          path: '/about/about-1',
          name: 'about1',
          component: () => import('../views/about/About-1')
        }, {
          path: '/about/about-2',
          name: 'about2',
          component: () => import('../views/about/About-2')
        }
      ]
    }
  ]
})

一、暂不考虑嵌套路由

不考虑嵌套路由

// 1. 插件
// 2. 两个组件 router-link router-view


/* vue 插件
*  function or object
*
*  要求有一个 install 方法,将来会被 Vue.use 调用
* */
let Vue; // 保存 Vue 的 构造函数,插件中要使用

export default class VueRouter {
  constructor(props) {
    this.$options = props;

    /* 将 current 作为响应式数据,router-view 的函数能够再次执行 */
    const hash = window.location.hash.slice(1);
    const initial = hash || '/'
    Vue.util.defineReactive(this, 'current', initial);

    // 监听 hash 的变化
    window.addEventListener('hashchange', () => {
      this.current = window.location.hash.slice(1);
    });
  }
  /* *
    任务一: 保证组件内 $router 能正常使用
  * install 函数,为 VueRouter 的静态方法
  * 此处 Vue 不使用 import 原因如下:
  *  1. Vue.use() 调用插件 install 函数时,会将 Vue 的构造函数传入
  *  2. 如果要单独打包该插件,会将 import 的 Vue 也打包在一起,会让插件的体积变大
  * */
  static install(_Vue) {
    Vue = _Vue;
    /* 全局混入
    *  目的:延迟逻辑,保证使用 router 时,router 已经创建
    * */
    Vue.mixin({
      /* 此钩子在每个组件创建实例时都会调用 */
      beforeCreate() {
        /* 因为 router 被挂载在根实例上,所以根实例会有 router */
        const { router } = this.$options;
        if(router) {
          /*
          *  将挂载在根实例上的 router 挂在 Vue 原型上,也是为了延迟执行,当该方法执行的时候,router 已经被创建了
          * */
          Vue.prototype.$router = router;
        }
      }
    });

    /* 任务二:注册 router-view 和 router-link 组件 */
    Vue.component(
      'router-link',
      {
        props: {
          to: {
            type: String,
            required: true
          }
        },
        render(h){
          /* 方式一(不推荐,兼容性比较差) */
          /* 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) {
        /* 获取并渲染当前路由对应的组件 */
        let component = null;
        const route = this.$router.$options.routes.find(
          route => route.path === this.$router.current
        );
        if (route) {
          component = route.component;
        }
        return h(component);
      }
    });
  }
  push(path) {
    window.location.hash = path;
  }
}

二、嵌套路由

上面的写法会在出现嵌套路由时报错,要考虑嵌套循环,做以下改进:

  • 声明一个新响应式数据:matched 数组,当其发生改变时,就会执行对应操作
  • 使用 match 方法将要渲染的一级、二级…路由匹配成一个 mathced 数组
  • 为每个要被 router-view 渲染的组件打上 routerView 的标记,便于遍历时确定层级
  • 用 depth 标记当前被路由组件的路由层级,便于在 mathced 数组中直接定位并防止循环渲染 router-view

分析如下

假设有一个符合项目路由配置的 hash 为: /about/about-1/about-1-1

那么在配置路由时,routes 一定包含这样一个路由配置:
routes: [
  ...
  {
    path: '/about',
    component: AboutComponet,
    children: [
      {
        path: '/about/about-1',
        component: About1Component,
        chilren: [
          {
            path: '/about/about-1/about1-1',
            component: About11Component
          }
        ]
      }, {
        path: '/about/about-2'
        component: About2Component
      }
    ]
  }
  ...
]

当路由改变为: /about/about-1/about-1-1 时,希望 match 函数能在 routes 数组中做匹配,匹配之后,matched 数组包含以下内容:

[
  {
    path: '/about',
    component: AboutComponet,
    children: [...]
  }, {
    path: '/about/about-1',
    component: About1Component,
    chilren: [...]
  }, {
    path: '/about/about-1/about1-1',
    component: About11Component
  }
]

当一级路由 '/about' 的 router-view 渲染时,会有一个 depth,值为 0,代表该router-view 要渲染 mathced[0].component

当二级路由 '/about/about-1' 的 router-view 渲染时,会有一个 depth,值为 1,代表该router-view 要渲染 mathced[1].component

当三级路由 '/about/about-1-1' 的 router-view 渲染时,会有一个 depth,值为 2,代表该router-view 要渲染 mathced[2].component

按照以上规律,我们可以将 router-view 的配置和渲染单独封装,并且改变 VueRouter 类的部分内容:

import RouterView from './my-router-view'

export default class VueRouter {
  static install(_Vue) {
    Vue = _Vue;
    Vue.mixin({
      beforeCreate() {
        const { router } = this.$options;
        if(router) {
          Vue.prototype.$router = router;
        }
      }
    });

    Vue.component(
      'router-link',
      {
        props: {
          to: {
            type: String,
            required: true
          }
        },
        render(h){
          return h('a', {
            attrs: {
              href: '#' + this.to
            }
          }, this.$slots.default)
        }
      }
    );

    Vue.component('router-view', RouterView);
  }

  matched = [];
  constructor(props) {
    this.$options = props;
    this.current = window.location.hash.slice(1) || '/';
    Vue.util.defineReactive(this, 'matched', []);

    // match 方法可以递归的遍历路由表,获得匹配关系数组
    this.match();

    // 监听 hash 的变化
    window.addEventListener('hashchange', this.onHashChange.bind(this));
  }

  onHashChange() {
    this.current = window.location.hash.slice(1);
    this.matched = []; // 清空 matched 数组,重新匹配
    this.match();
  }

  match(routes) {
    routes = routes || this.$options.routes;

    // 递归遍历
    for(const route of routes) {
      if (route.path === '/' && this.current === '/') {
        this.matched.push(route);
        return;
      }
      if(route.path !== '/' && this.current.indexOf(route.path) !== -1) {
        this.matched.push(route);
        // 如果存在 children,继续判断
        if(route.children) {
          this.match(route.children);
          /* 递归遍历完成时,matched 数组中就会承接此次跳转的所有路由列表 */
        }
        return;
      }
    }
  }
  push(path) {
    window.location.hash = path;
  }
}

view封装

export default {
  render(h) {
    /* 在当前 router-view  的虚拟节点上,标记其深度 */
    this.$vnode.data.routerView = true;

    /* 深度值,一级路由为 0 */
    let depth = 0;
    /* 当前 router-view 所在的父级 vNode  */
    let parent = this.$parent;

    while(parent) {
      const vnodeData = parent.$vnode && parent.$vnode.data;
      if(vnodeData && vnodeData.routerView) {
        /*
          每次 router-view 被 render 时,都会在其 data 上添加 routerView 属性,
          所以如果这里的routerView存在,即证明该 parent 为一个 routerView,
          那么当前 router-view 的深度就应该 +1
        */
        depth++;
      }
      // 当前 parent 判断完毕,再获取上层 parent,继续判断,直至无 parent,遍历完毕
      parent = parent.$parent;
    }

    /* 获取并渲染当前路由对应的组件 */
    let component = null;
    const route = this.$router.matched[depth];
    if(route) {
      component = route.component;
    }
    return h(component);
  }
}

扫码拉你进入前端技术交流群

在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

锋利的二丫

如果对您有帮助,请博主喝杯奶茶

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值