本篇文章记录仿写一个el-bread组件细节,从而有助于大家更好理解饿了么ui对应组件具体工作细节。本文是elementui源码学习仿写系列的又一篇文章,后续空闲了会不断更新并仿写其他组件。源码在github上,大家可以拉下来,npm start运行跑起来,结合注释有助于更好的理解。github仓库地址如下:
https://github.com/shuirongsh...

什么是面包屑

直观来说,面包屑其实就相当于一个导航跳转的快捷操作方式。那么为什么这么叫呢?源自格林童话:

面包屑导航(BreadcrumbNavigation)这个概念来自童话故事“汉赛尔和格莱特”,当汉赛尔和格莱特穿过森林时,不小心迷路了,但是他们发现沿途走过的地方都撒下了面包屑,让这些面包屑来帮助他们找到回家的路。所以,面包屑导航的作用是告诉访问者他们在网站中的位置以及如何返回。

源自百度百科:https://baike.baidu.com/item/...

组件需求分析

关于bread面包屑组件,主要是用于展示当前页面所在的层级位置,告知用户在哪里,且能够点击面包屑做路由跳转(返回),我们分析一下面包屑组件的需求,大致有以下:

  • 面包屑分隔内容需求

    • 默认分隔内容,比如饿了么UI的面包屑导航默认就是以 斜杠 / 分隔的
    • 如果我们觉得默认分隔斜杠 / 不好看,也可自己传递分隔内容,比如以 >> 分隔
  • 跳转功能需求

    • 比如push跳转,即:this.$route.push(...)
    • 或者replace跳转,即:this.$route.replace(...)

整理来说,这两个需求都是挺简单的,不过我们再看下方封装的代码之前需要复习一下组件中用到的知识:provide和inject

provide、inject的知识点复习

一言以蔽之:祖先组件provide提供数据,后代组件(包含子组件)inject接收数据

关于provideinject我们可以这样的类比理解:

  • 父传子,是父组件使用冒号:绑定传递,子组件使用props接收数据
  • 而祖先传后代,祖先使用provide绑定传递(提供),后代使用inject接收数据(注入)

只说文字,有点不太直观,所以我们看一下下面这个案例就理解了

小案例

此案例是分为三个组件,分别是one组件、two组件、three组价,one组件是two组件的父组件、two组件是three组件的父组件。即关系为:one、two、three三个组件构成了爷、父、子这样关系的祖先组件和后代组件

one组件中有name和age两个字段的数据,需要提供到two组件和three组件中使用

案例代码图示分析

案例效果图

理解了这个小案例,再看下方的代码会更好明晰思路

为甚要提到provideinject呢?因为在封装面包屑组件的时候,官方是分为两个组件,el-breadcrumbel-breadcrumb-item;在el-breadcrumb组件中使用到了provide提供默认分隔内容斜杠/和分隔内容的图标class类名给后代组件el-breadcrumb-item使用。

接下来我们看一下仿写封装的组件代码

封装代码

封装的效果图

使用组件的代码

类似官方的面包屑组件代码,这里我们也用两个面包屑组件代码,为:my-bread组件和my-bread-item组件(祖先后代关系)

<template>
  <div>
    <my-divider content-position="left">默认颜文字分隔</my-divider>
    <my-bread>
      <my-bread-item>外层</my-bread-item>
      <my-bread-item>中层</my-bread-item>
      <my-bread-item>内层</my-bread-item>
    </my-bread>
    <my-divider content-position="left">自定义分隔内容</my-divider>
    <my-bread customDivide=">">
      <my-bread-item>外层</my-bread-item>
      <my-bread-item>中层</my-bread-item>
      <my-bread-item>内层</my-bread-item>
    </my-bread>
    <my-divider content-position="left">使用饿了么UI的图标做分隔</my-divider>
    <my-bread elementIconClassDivide="el-icon-wind-power">
      <my-bread-item>外层</my-bread-item>
      <my-bread-item>中层</my-bread-item>
      <my-bread-item>内层</my-bread-item>
    </my-bread>
    <my-divider content-position="left">可跳转</my-divider>
    <my-bread elementIconClassDivide="el-icon-location-outline">
      <my-bread-item :to="{ path: '/myTag' }">myTag跳转</my-bread-item>
      <my-bread-item replace :to="{ path: '/myBadge' }">myBadge跳转(replace)</my-bread-item>
      <my-bread-item>当下</my-bread-item>
    </my-bread>
  </div>
</template>

my-bread组件代码

<template>
  <div class="breadWrap">
    <slot></slot>
  </div>
</template>

<script>
export default {
  name: "myBread",
  props: {
    // 使用饿了么UI的图标icon类名进行分隔
    elementIconClassDivide: {
      type: String,
      default: "",
    },
    // 自定义分隔内容,用户填写什么,就以什么为分隔
    customDivide: {
      type: String,
      default: "→_→", // 如这里,默认以颜文字为默认分隔。饿了么是斜杠默认分隔/
    },
  },
  /**
   * 父组件provide注入一个自身实例this给到子组件myBreadItem,方便子组件访问
   * 父组件的分隔内容变量defaultDivide或customDivide或elementIconClassDivide
   * 因为子组件需要去渲染出对应的分隔内容是啥
   *
   * 其实这里不使用provide注入的方式也是可以的。子组件myBreadItem访问父组件myBread
   * 也可以使用this.$parent.defaultDivide这样的形式去访问父组件的数据内容。不过
   * 这里只是做一个分隔内容的展示,不牵涉到响应式数据处理,所以使用provide更优雅
   * */
  provide() {
    return {
      fatherInstance: this, // 提供自身实例,方便子组件使用,子组件需inject声明注入接收使用
    };
  },
};
</script>

<style lang="less" scoped>
.breadWrap {
  font-size: 14px;
  // 第一个面包屑的文字加粗
  /deep/ .breadItem:first-child .breadItemWords {
    font-weight: 700;
  }
  // 最后一个面包屑的小图标隐藏
  /deep/ .breadItem:last-child .breadItemDivide {
    display: none;
  }
}
</style>

my-bread-item组件代码

<template>
  <div class="breadItem">
    <!-- 面包屑文字部分(点击文字跳转) -->
    <span ref="link" :class="['breadItemWords', to ? 'isLink' : '']">
      <slot></slot>
    </span>
    <!-- 面包屑图标部分 -->
    <!-- 使用饿了么UI的图标做分隔 -->
    <i
      v-if="elementIconClassDivide"
      class="breadItemDivide"
      :class="elementIconClassDivide"
    ></i>
    <!-- 自定义分隔 二者只留一个即可 -->
    <span v-else class="breadItemDivide">{{ customDivide }}</span>
  </div>
</template>

<script>
export default {
  name: "myBreadItem",
  inject: ["fatherInstance"], // 要声明接受以后才能使用,注意名字要和父组件provide的保持一致
  props: {
    // 跳转的path
    to: {},
    // 默认不做replace跳转
    replace: {
      type: Boolean,
      default: false,
    },
  },
  data() {
    return {
      elementIconClassDivide: "", // 使用饿了么图标做分隔的类名
      customDivide: "", // 自定义分隔内容
    };
  },
  mounted() {
    /**
     * 先祖组件provide提供数据,后代组件inject注入接受数据(可以inject多个实例,故为数组)
     * 这里是以父子组件为例,子组件访问父组件的值(也可考虑使用this.$parent.xxx方式)
     *
     * 渲染分隔符
     * */
    // console.log(
    //   "父组件提供数据,子组件注入自身以后便可访问",
    //   this.fatherInstance
    // );
    // 访问并再存一份去使用,从而渲染分隔内容
    this.elementIconClassDivide = this.fatherInstance.elementIconClassDivide;
    this.customDivide = this.fatherInstance.customDivide;
    /**
     * 跳转功能
     * */
    // 获取组件实例
    const link = this.$refs.link;
    // 绑定监听点击,点击跳转
    link.addEventListener("click", (_) => {
      // 没有传递to就不做跳转
      if (!this.to) return;
      // 当replace为true的时候,才做replace跳转(默认还是push跳转)
      this.replace ? this.$router.replace(this.to) : this.$router.push(this.to);
    });
  },
};
</script>

<style lang="less" scoped>
.breadItem {
  display: inline-block;
  .breadItemWords { font-weight: 400; }
  .isLink { font-weight: 700; }
  .isLink:hover { color: #409eff; cursor: pointer; }
  .breadItemDivide { margin: 0 8px; color: #999; }
}
</style>

总结

个人愚见provideinject主要使用的场景还是组件封装这一块,貌似在业务代码中使用的少


水冗水孚
1.1k 声望585 粉丝

每一个不曾起舞的日子,都是对生命的辜负