Element UI 组件源码分析之Breadcrumb面包屑

1,460 阅读5分钟

0x00 简介

组件 Breadcrumb 用于显示当前页面的路径,快速返回之前的任意页面。 本文将深入分析源码,剖析其实现原理,耐心读完,相信会对您有所帮助。源码实现详见packages/breadcrumb/src/ 文件夹下  breadcrumb.vue、 breadcrumb-item.vue 等组件实现。 🔗 组件文档 Breadcrumb 🔗 gitee源码

更多组件分析详见 👉 📚 Element UI 源码剖析组件总览

本专栏的 gitbook 版本地址已经发布 anduril.gitbook.io/learning-el… ,内容同步更新中!


面包屑组件有两部分 <breadcrumb><breadcrumb-item>,组件源码都在 packages/breadcrumb/src/ 文件夹下。在项目工程化机制下,每个组件对应各自的文件夹 component-name, 定义导出组件并为其扩展 install 方法,以 commonjs2 规范对每个组件单独打包构建,支持按需引入。

0x01 breadcrumb 组件

breadcrumb.vue 组件是面包屑组件的容器。

template 模板内容

模板创建一个 class 名为.el-breadcrumb<div>元素作为包裹容器,提供了匿名插槽,将提供breadcrumb-item组件引用内容。

<template>
  <div class="el-breadcrumb" aria-label="Breadcrumb" role="navigation">
    <slot></slot>
  </div>
</template>

ARIA 无障碍访问

组件添加了 rolearia-label 属性。 role 表示当前元素的类型。aria-label属性用来给当前元素加上的标签描述。

Accessible Rich Internet Applications  (ARIA)  是能够让残障人士更加便利的访问 Web 内容和使用 Web 应用的一套机制。 更多内容详见 "ARIA",MDN

attributes 属性

组件定义了 2 个 prop : separatorseparatorClass 。 这两个prop用于在<el-breadcrumb>标签中设置分隔符的形式。

props: {
  separator: {
    type: String,
    default: '/'
  },
  separatorClass: {
    type: String,
    default: ''
  }
},

默认使用separator属性,只能是字符串,默认为斜杠/。也可通过设置 separatorClass 使用相应的 iconfont 作为分隔符,格式内容为el-icon-[icon-name],这将使 separator 设置失效。具体逻辑实现详见下文章节 breadcrumb-item 组件#分隔符

provide/inject 依赖注入

使用 依赖注入,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。

provide 选项允许指定想要提供给后代组件的数据/方法。这里将提供组件 breadcrumb 的 vue 实例 。

// breadcrumb\src\breadcrumb.vue
provide() {
  return {
    elBreadcrumb: this
  };
},

在任何后代组件里,使用 inject 选项来指定接收添加在这个实例上的 property。breadcrumb-item组件访问父级组件breadcrumb的实例。

// breadcrumb\src\breadcrumb-item.vue
inject: ['elBreadcrumb'],

mounted()

实例被挂载后,即 mounted 方法中,通过 $el.querySelectorAll('.el-breadcrumb__item') 在面包屑组件的DOM元素中查找<breadcrumb-item>子节点。为最后一个 <breadcrumb-item> 节点添加属性aria-current="page"

// 实例被挂载后
mounted() {
  // $el Vue 实例使用的根 DOM 元素。 
  const items = this.$el.querySelectorAll('.el-breadcrumb__item');
  if (items.length) {
    items[items.length - 1].setAttribute('aria-current', 'page');
  }
}

组件DOM渲染如下,指明<breadcrumb-item>集合中的最后一个代表当前页面。

<div aria-label="Breadcrumb" role="navigation" class="el-breadcrumb">
    <span class="el-breadcrumb__item">...</span>
    <span class="el-breadcrumb__item">...</span>
    <span class="el-breadcrumb__item" aria-current="page">...</span>
</div>

0x02 breadcrumb-item 组件

breadcrumb-item.vue组件用于实现每一个el-breadcrumb-item 子标签。

template 模板内容

模板创建一个 class 名为el-breadcrumb__item<span>元素作为标签容器,包含 2 个子节点 标签inerr分隔符

<template>
  <span class="el-breadcrumb__item">
    <span
      :class="['el-breadcrumb__inner', to ? 'is-link' : '']"
      ref="link"
      role="link">
      <slot></slot>
    </span>
    <i v-if="separatorClass" class="el-breadcrumb__separator" :class="separatorClass"></i>
    <span v-else class="el-breadcrumb__separator" role="presentation">{{separator}}</span>
  </span>
</template>

标签 inner

该节点渲染一个 class 名为el-breadcrumb__inner<span>元素, 根据 prop 传入的to值动态添加链接样式。当prop属性 to 是 truthy 时,添加is-link样式。 同时提供了匿名插槽自定义标签内容。

ref 声明用来给元素注册引用信息。role="link" 声明用于识别创建与应用或外部资源的超链接的元素。

attributes 属性

组件定义了 2 个 prop toreplace用于路由设置。

  • to 路由跳转对象,同 vue-router 的 to
  • replace 在使用 to 进行路由跳转时,启用 replace 将不会向 history 添加新记录。
props: {
  to: {},
  replace: Boolean
},

mounted()

实例被挂载后,根据 toreplace的传入值动态配置路由设置。

  1. 使用$refs获取 标签 inner节点标签元素信息。
  2. 为节点元素添加click监听事件
mounted() { 
  // $refs 只会在组件渲染完成之后生效,并且它们不是响应式的。
  const link = this.$refs.link;
  // span 已声明 role="link" 重复设置
  link.setAttribute('role', 'link1');
  link.addEventListener('click', _ => {
    // 通过在 Vue 根实例的 router 配置传入 router 实例
    const { to, $router } = this;
    // router 实例必须注册 和 路由跳转对象必须设置
    if (!to || !$router) return;
    // $router.replace方法不会向 history 添加新记录- 替换掉当前的 history 记录
    // $router.push方法会向 history 栈添加一个新的记录
    this.replace ? $router.replace(to) : $router.push(to);
  });
}

标签监听事件主要用于,点击标签时,判断是否 注册 router 实例 和 设置路由跳转对象(prop 的 to),若都满足,则根据 replace 在使用 to 进行路由跳转时使用不同编程式的导航 。

声明式编程式描述
<router-link :to="...">router.push(...)向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,则回到之前的 URL。
<router-link :to="..." replace>router.replace(...)不会向 history 添加新记录,替换掉当前的 history 记录。

分隔符

当属性 separatorClass 是 truthy 时,使用相应的 iconfont 作为分隔符。

否则使用separator属性,默认为斜杠/,此时渲染的 <span> 元素, 其role="presentation" 属性声明用于从元素及其子元素中删除语义含义。

<i v-if="separatorClass" class="el-breadcrumb__separator" :class="separatorClass"></i>
<span v-else class="el-breadcrumb__separator" role="presentation">{{separator}}</span>

separatorseparatorClass属性值通过依赖注入从父组件获取实例,在 mounted 方法中获取父组件的prop中的属性值进行赋值。

data() {
  return {
    separator: '',
    separatorClass: ''
  };
},
// 接收父组件提供的实例
inject: ['elBreadcrumb'],
// 实例被挂载后
mounted() {
  // 分割线的设置,获取父级组件的prop属性值
  this.separator = this.elBreadcrumb.separator;
  this.separatorClass = this.elBreadcrumb.separatorClass; 
}

0x03 组件样式

src/breadcrumb.scss

组件样式源码 packages\theme-chalk\src\breadcrumb.scss 使用混合指令 beutils-clearfix嵌套生成组件样式。

混合指令utils-clearfix 用于清除浮动。

@mixin utils-clearfix {
  $selector: &;

  @at-root {
    #{$selector}::before,
    #{$selector}::after {
      display: table;
      content: "";
    }
    #{$selector}::after {
      clear: both
    }
  }
}

breadcrumb.scss 生成逻辑如下。

// 生成 .el-breadcrumb
@include b(breadcrumb) {
  //...
  
  // 生成 .el-breadcrumb::before,.el-breadcrumb::after { /*...*/ } 
  // 生成 .el-breadcrumb::after { /*...*/ }
  @include utils-clearfix;
  
  // 生成 .el-breadcrumb__separator
  @include e(separator) {
    //...
    
    // 生成 .el-breadcrumb__separator[class*=icon]
    &[class*=icon] {
      //...
    }
  }
  // 生成 .el-breadcrumb__item
  @include e(item) {
    //...
    
    // 生成 .el-breadcrumb__inner
    @include e(inner) {
      //...
      
      // 生成  .el-breadcrumb__inner.is-link, .el-breadcrumb__inner a
      &.is-link, & a {
        //...
        
        // 生成  .el-breadcrumb__inner.is-link:hover, .el-breadcrumb__inner a:hover
        &:hover {
          //...
        }
      }
    }
    

    /*  生成
    .el-breadcrumb__item:last-child .el-breadcrumb__inner, 
    .el-breadcrumb__item:last-child .el-breadcrumb__inner a, 
    .el-breadcrumb__item:last-child .el-breadcrumb__inner a:hover, 
    .el-breadcrumb__item:last-child .el-breadcrumb__inner:hover */
    &:last-child {
      .el-breadcrumb__inner,
      .el-breadcrumb__inner a {
        &, &:hover {
          //...
        }
      }
      
      // 生成  .el-breadcrumb__item:last-child .el-breadcrumb__separator { display: none; }
      .el-breadcrumb__separator {
        display: none;
      }
    }
  }
}

虽然定义了 breadcrumb-item.scss样式文件,但是所有的样式逻辑都在breadcrumb.scss中实现。

lib/breadcrumb.scss

前文可知使用 gulpfile.js编译 scss 文件转换为CSS,经过浏览器兼容、格式压缩,最后生成 packages\theme-chalk\lib\breadcrumb.scss,内容格式如下。

.el-breadcrumb { /*...*/ }
.el-breadcrumb::after,.el-breadcrumb::before { /*...*/ }
.el-breadcrumb::after { /*...*/ }
.el-breadcrumb__separator { /*...*/ }
.el-breadcrumb__separator[class*="icon"] { /*...*/ }
.el-breadcrumb__item { /*...*/ }
.el-breadcrumb__inner { /*...*/ }
.el-breadcrumb__inner a,.el-breadcrumb__inner.is-link { /*...*/ }
.el-breadcrumb__inner a:hover,.el-breadcrumb__inner.is-link:hover { /*...*/ }

.el-breadcrumb__item:last-child .el-breadcrumb__inner,
.el-breadcrumb__item:last-child .el-breadcrumb__inner a,
.el-breadcrumb__item:last-child .el-breadcrumb__inner a:hover,
.el-breadcrumb__item:last-child .el-breadcrumb__inner:hover {
  /*...*/
}
/* 最后标签的分隔符不显示 */ 
.el-breadcrumb__item:last-child .el-breadcrumb__separator { display: none; }

📚参考&关联阅读

"依赖注入",vuejs.org
“组件注入”,vue-router
“Using ARIA: Roles, states, and properties”,MDN
“Breadcrumb Example”,w3.org
"ref",vuejs.org

关注专栏

如果本文对您有所帮助请关注➕、 点赞👍、 收藏⭐!您的认可就是对我的最大支持!

此文章已收录到专栏中 👇,可以直接关注。