0x00 简介
组件 Breadcrumb
用于显示当前页面的路径,快速返回之前的任意页面。 本文将深入分析源码,剖析其实现原理,耐心读完,相信会对您有所帮助。源码实现详见packages/breadcrumb/src/
文件夹下 breadcrumb.vue
、 breadcrumb-item.vue
等组件实现。 🔗 组件文档 Breadcrumb 🔗 gitee源码
更多组件剖析详见 👉 📚 Element 2 源码剖析组件总览 。
面包屑组件有两部分 <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 无障碍访问
组件添加了 role
和 aria-label
属性。 role
表示当前元素的类型。aria-label
属性用来给当前元素加上的标签描述。
Accessible Rich Internet Applications
(ARIA) 是能够让残障人士更加便利的访问 Web 内容和使用 Web 应用的一套机制。 更多内容详见 "ARIA",MDN
attributes 属性
组件定义了 2 个 prop : separator
、separatorClass
。 这两个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 to
、replace
用于路由设置。
to
路由跳转对象,同 vue-router
的 to
。
replace
在使用 to 进行路由跳转时,启用 replace 将不会向 history 添加新记录。
props: {
to: {},
replace: Boolean
},
mounted()
实例被挂载后,根据 to
、replace
的传入值动态配置路由设置。
使用$refs
获取 标签 inner
节点标签元素信息。
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 进行路由跳转时使用不同编程式的导航 。
声明式 | 编程式 | 描述 |
---|
| | 向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,则回到之前的 URL。 |
<router-link :to="..." 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>
separator
、separatorClass
属性值通过依赖注入从父组件获取实例,在 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
使用混合指令 b
、e
、utils-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; }
0x04 📚参考
"依赖注入",vuejs.org
“组件注入”,vue-router
“Using ARIA: Roles, states, and properties”,MDN
“Breadcrumb Example”,w3.org
"ref",vuejs.org