Vue-Ui 手写实现以下breadcrumb面包屑组件(初级难度)

简介

我们开始只关注组件的功能实现,不考虑 css 分装、webpack 配置、整体结构设计、单元测试等等,因为在后面会一步一步完善。让大家一起进步,实现一套简单的组件库。

在日常我们开发 PC 页面时经常会用到一个面包屑导航的功能,其实这个功能算是比较简单的功能,基本上用过Vue这个框架的人都能自己写出来。但是既然要写一个通用的可能就不是那么容易实现,我们首先要了解breadcrumb它都有什么功能。下面我们就先分析它都有什么功能,可以参考element/iview这种流行的Ui框架

  1. 分析breadcrumb组件功能
  2. 构思代码、编写代码
  3. 测试组件效果,(编写单元测试)

按照上面的三步一步一步的实现自己一个自己breadcrumb组件,废话不多说直接开干。

分析组件功能

我们可以去element/iview官方网去阅读一下他们的文档,在去github中看一下他们的源码。首先看一下他们是怎么使用,下面以element为例。
示例

1
2
3
4
5
6
<el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item><a href="/">活动管理</a></el-breadcrumb-item>
<el-breadcrumb-item>活动列表</el-breadcrumb-item>
<el-breadcrumb-item>活动详情</el-breadcrumb-item>
</el-breadcrumb>

效果图:
breadcrumb

根据上面和代码我们可以看出breadcrumb有两个组件,分别为:

breadcrumb组件,并且它接受两个props属性:

  • sparator(props): 它是用来替换默认/分隔符的,并且它的类型为String类型。默认/
  • sparatorClass(props): 它是用来给填充iconfont这种的图标分隔符,并且它的类型为String类型。没有默认值

breadcrumb-item组件,它是被breadcrumb包裹的组件,它也接受两个props属性:

  • to(props): 路由跳转对象,同 vue-routerto, 并且它的类型为String/Object类型。没有默认值
  • replace(props): 在使用 to 进行路由跳转时,启用 replace 将不会向 history 添加新记录, 类型是Boolean。默认false

我们大致知道了有两个组件,组件之间有嵌套关系,并且分别都支持两个props参数。并且有的props还有默认参数。下面我们就来一步一步实现自己已经知道的功能和配置。

实现组件

这里面有两个比较重要的知识点:

  • breadcrumb-item可以通过slot传入breadcrumb,同时breadcrumb-item中的内容也通过slot传入内容
  • breadcrumb中接受的两个props怎么传入breadcrumb-item。 当然可以通过props一层一层传入,但是我们要写的好一点,这个里可以通过两种方式实现。provide/injectthis.$children来实现,这两种实现方式分别是elementiview实现方式,个人认为provide/inject更简洁一点。

第一步

一个简单的组件**breadcrumb**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<template>
<div class="breadcrumb">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'breadcrumb',
props: {
// 分隔符
separator: {
type: String,
default: '/'
},
// 分隔符
separatorClass: {
type: String,
default: ''
}
},
/**
* TODO: 通过provide注入当前组件实例
* @return {Object} 返回一个对象
*/
provide() {
return {
breadcrumbEl: this
};
},
mounted() {}
};
</script>
<style lang="scss" scoped>
.clearfix {
&::after,
&::before {
content: '';
display: table;
}
&::after {
clear: both;
}
}
.breadcrumb {
font-size: 14px;
line-height: 1;
@extend .clearfix;
}
</style>

另一个组件**breadcrumb-item**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<template>
<span class="breadcrumb-item">
<span :class="['breadcrumb-inner']">
<slot />
</span>
<span class="breadcrumb-separator">
{{ separator }}
</span>
</span>
</template>
<script>
export default {
name: 'breadcrumbItem',
props: {
// 跳转路径
// eslint-disable-next-line vue/require-default-prop
to: [String, Object],
// 是否使用repalce替换push跳转
replace: Boolean
},
data: function () {
return {
// TODO: 接受父组件传入的 分隔符
separator: '',
// TODO: 接受父组件传入的 分隔符 class
separatorClass: ''
};
},
// TODO: 通过inject接受父组件注入的对象
inject: ['breadcrumbEl'],
mounted() {
this.separator = this.breadcrumbEl.separator;
this.separatorClass = this.breadcrumbEl.separatorClass;
console.log('this.breadcrumbEl: ', this.breadcrumbEl);
}
};
</script>
<style lang="scss" scoped>
.breadcrumb {
@at-root #{&}-item {
float: left;
}
@at-root #{&}-separator {
margin: 0 9px;
font-weight: 700;
color: #c0c4cc;
}
}
</style>

到此我们至少实现了基本的功能,如下图所示:
breadcrumb

第二步

但是我们观察这个图片可以看到,我们还是有一部分功能没有实现,如下几点:

  • 组件的后面多了一个/
  • 当前的组件不能跳转,增加跳转
  • 添加属性无障碍阅读

我们在breadcrumb组件中添加如下代码:

1
2
3
4
5
6
7
8
9
10
// 新增代码
mounted () {
// 获取所有的面包屑子项
const items = this.$el.querySelectorAll('.breadcrumb-item')
// 判断子节点的长度
if (items.length) {
// 如果最后一个添加aria 属性
items[items.length - 1].setAttribute('aria-current', 'page')
}
}

我们在breadcrumb-item组件中添加如下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<template>
<span class="breadcrumb-item">
<span ref="link" :class="['breadcrumb-inner', to ? 'is-link': '']">
<slot />
</span>
<i
v-if="separatorClass"
class="breadcrumb-separator"
:class="separatorClass"
/>
<span v-else class="breadcrumb-separator" role="presentation">
{{ separator }}
</span>
</span>
</template>
<script>
export default {
name: 'breadcrumbItem',
props: {
// 跳转路径
to: [String, Object],
// 是否使用repalce替换push跳转
replace: Boolean
},
data() {
return {
// TODO: 接受父组件传入的 分隔符
separator: '',
// TODO: 接受父组件传入的 分隔符 class
separatorClass: ''
};
},
// TODO: 通过inject接受父组件注入的对象
inject: ['breadcrumbEl'],
mounted() {
this.separator = this.breadcrumbEl.separator;
this.separatorClass = this.breadcrumbEl.separatorClass;
// 获取当前Link实例
let linkEl = this.$refs.link;
linkEl.setAttribute('role', 'link');
linkEl.addEventListener('click', (event) => {
//
let { to, replace, $router } = this;
// 判断是否传入to 是否存在$router不存在直接返回
if (!to || !$router) {
return false;
}
// 根据replace的值,调用push or replace
replace ? $router.replace(to) : $router.push(to);
});
console.log('this.breadcrumbEl: ', this.breadcrumbEl);
}
};
</script>
<style lang="scss" scoped>
.breadcrumb {
@at-root #{&}-item {
float: left;
}

@at-root #{&}-separator {
margin: 0 9px;
font-weight: 700;
color: #c0c4cc;
}
@at-root #{&}-inner {
&.is-link:hover,
& a:hover {
color: #409eff;
cursor: pointer;
}
}
& .breadcrumb-item:last-child {
.breadcrumb-separator {
display: none;
}
}
}
</style>

我们通过breadcrumb-item:last-child把最后一个/隐藏掉。
我们通过获取this.$refs.link实例,设置无障碍阅读role属性,设置无障碍阅读。并且绑定跳转事件,根据传入的repalce属性判断跳转方式。
到此我们就实现了一个自己可用的Breadcrum组件。

在线代码:

总结

在本篇文章中即实现了自己的 ui 组件breadcrumb,又学习了两个比较常用的Vue知识点。如果不了解solt可以去看vue官方文档。另一个provide/inject它类似于react中的context,如果想了解vue中其他好玩的属性内置组件修饰符可以关注我。