【源码解析】解密RouterLink组件的实现原理,其实就是一个a标签

1,593 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第23天,点击查看活动详情

前言

大家好,上篇文章(路由前置守卫beforeEach)[xxx]中我们分享的路由前置守卫beforeEach的使用方法、使用场景及云源码解读。前面的几篇关于VueRouter的分享都是与路由初始化和配置相关的,那么除了这些初始化和配置操作外,还有路由的具体使用。在前面我们分享VueRouter的install方法时曾有提到在install的最后会注册两个全局组件RouterLink和RouterView,这也是在组将中使用路由的两个非常常用的组件。RouterLinek主要实现路由的切换,RouterView则负责页面的加载。下面我们先来分析一下RouterLink。

RouterLink的使用

RouterLinke的使用也非常简单,跟其它组件一样RouterLink也是一个组件,并且还是一个全局组件,所以我们可以在各个组件的模板中直接使用即可。RouterLink定义了11个自定义属性(props)其中to是最常用也是必须要传递的一个属性。下面我们把几个常用的属性简单介绍一下:

to

  • 类型:to是一个routelocationrawstring类型
  • 默认值:to是必填项,无默认值
  • 用途:用来表示目标路由的链接,当RouterLink被点击后,它的值会被传递给router.push从而实现路由的切换,因此to的值还可以是一个字符串。
<!-- 字符串 -->
<router-link to="/home">Home</router-link>

<!-- 使用 v-bind 的 JS 表达式 -->
<router-link :to="{ path: '/home' }">Home</router-link>

<!-- 命名的路由 -->
<router-link :to="{ name: 'user', params: { userId: '123' }}">User</router-link>

<!-- 带查询参数,下面的结果为 `/register?plan=private` -->
<router-link :to="{ path: '/register', query: { plan: 'private' }}">
  Register
</router-link>

replace

  • 类型:Boolean
  • 默认值:false
  • 用途:上面说到to会把值传递给router.push实现路由切换,而这里如果指定了replace属性为true时则会将to的值传递个router.replace而不是router.push,因此导航不会留下历史记录
<router-link to="/home" replace>home</router-link>

active-class

  • 类型:string
  • 默认值:router-link-active
  • 用途:用于链接激活时,应用于渲染组件的class。
<router-link to="/home" active-class="xxx">home</router-link>

以上就是RouterLink的基本用法和用途介绍,其实最终RouterLink会被渲染成一个a标签,那么既然最终用的也是a标签,为什么不一开始就使用a标签而是要用个自定义的RouterLink组件呢?这是因为:

如果直接使用a标签,在点击链接实现路由切换时整个页面都会刷新一下,而VueRouter之所以为我们封装了自定义组件RouterLink,目的就是为了实现在不重新加载页面的情况下更改 URL,处理 URL 的生成以及编码。如下图

<router-link to="/">RouterLink</router-link>
<a href="/">a标签</a>

caoshenhuinan (2).gif

源码解读

了解了RouterLink的用法及用途,下面我们再来看下它的源码是如何实现的,为什么它可以在不重新加载页面的情况下实现url切换的呢?

  var Link = {
    name: 'RouterLink',
    props: {
      to: {
        type: toTypes,
        required: true
      },
      tag: {
        type: String,
        default: 'a'
      },
    ...省略部分props
    },
    render: function render (h) {
      var this$1 = this;
      var router = this.$router;
      var current = this.$route;
      var ref = router.resolve(
        this.to,
        current,
        this.append
      );

      var classes = {};
      var activeClassFallback =
        globalActiveClass == null ? 'router-link-active' : globalActiveClass;
      var exactActiveClassFallback =
        globalExactActiveClass == null
          ? 'router-link-exact-active'
          : globalExactActiveClass;
      ...
      var handler = function (e) {
        if (guardEvent(e)) {
          if (this$1.replace) {
            router.replace(location, noop);
          } else {
            router.push(location, noop);
          }
        }
      };

      var on = { click: guardEvent };
      if (Array.isArray(this.event)) {
        this.event.forEach(function (e) {
          on[e] = handler;
        });
      } else {
        on[this.event] = handler;
      }

      if (scopedSlot) {
        
      }

      if (this.tag === 'a') {
        data.on = on;
        data.attrs = { href: href, 'aria-current': ariaCurrentValue };
      } else {
        // find the first <a> child and apply listener and href
        var a = findAnchor(this.$slots.default);
        if (a) {
           ...
        } else {
          // doesn't have <a> child, apply listener to self
          data.on = on;
        }
      }

      return h(this.tag, data, this.$slots.default)
    }
  };

首先我们看到这里的RouterLink组件跟其它vue组件的定义方式有所不同,这里的RouterLink是一个包含了三个属性(name、props和render)的Link对象,实际上这是一种函数式的组件声明,其中name和props的定义方式与我们在vue文件中自定义组件的方式是一样的,唯一不同的也是该组件的核心所在就是这个render函数,下面我们详细解读一下:

  • name属性的值为“RouterLink”,这个就是组件的名称
  • props,前面我们提到RouterLink组件有11个自定义属性,其实就是来自这个props,其中to属性是一个必填项就是通过这里定义的。另外还有个字符串类型的tag属性,这个属性就是用来指示最终渲染在页面上的标签,默认值为a,我们还可以手动指定为div,span等等任何想要的标签。
  • render函数是该组件实现的核心所在
    • render函数接收一个形参h,实际上h也是一个函数,是用来将虚拟dom转换成真是dom的createElement函数
    • 在render内部,首先会声明并定义一些变量,如路由实例router、当前路由信息current等等
    • 然后处理自定义属性,比如上面定义的activeClass和exactActiveClass,其中默认值router-link-activerouter-link-exact-active就是在这里设置的
    • 接着下面定义了一个handler方法,当在组件中点击RouterLink时就会触发这个handler方法实现路由的切换,我们可以看到在这个方法中最核心的就是调用了router.replacerouter.push从而实现在不重新加载页面的情况下实现url的切换
    • 下面定义的on对象中包含一个click属性,其实对应的就是RouterLink的点击事件,默认值是guardEvent,最终会被替换为handler函数
    • data对象存储的是标签的样式列表,最终会被应用到标签元素上
    • scopedSlot是用于处理RouterLink的作用域插槽的,在前面使用时没有提到,其实RouterLink还可以通过v-slot指令来暴露底层的定制能力,这是一个更高阶的 API,主要面向库作者,但也可以为开发者提供便利,大多数情况下用在一个类似 NavLink 这样的组件里。关于作用域插槽就是通过scopedSlot相关的代码进行处理的
    • 接下来会判断自定义属性tag的值是否是a,也就是a标签,如果是则添加对应的click事件,href和aria-current属性
    • 如果不是则查找它的子元素中是否有a标签,如果有则进行相关的绑定操作,如果仍然没有则直接将click事件绑定到当前组件本身
    • 最后也是重点就是调用h函数,上面说到h函数其实就是createElement,目的将上将虚拟dom转换成真实dom

总结

本次分享我们介绍了vuerouter中RouterLink组件的用法用途及源码解读,简单总结如下:

RouterLink通过to属性接收路由信息,当用户点击RouterLink时会触发一个handler的函数,在handler中会将路由路径传递给router.push或router.replace从而实现在不重新加载页面的情况下实现url的切换。然后通过render函数对自定义属性进行处理最后通过调用createElement函数将虚拟dom(js模拟的dom对象)转换成真实dom进行渲染

好了关于RouterLink组件的分享就到这里了。欢迎大佬的指点!!!