头图

前言

本篇文章主要讲解如何来封装一个面包屑组件,充当基本的导航角色

本文也是《通俗易懂的中后台系统建设指南》系列的第七篇文章,该系列旨在告诉你如何来构建一个优秀的中后台管理系统

在本文中,你可以了解到面包屑概念、应用场景等知识点,学到面包屑的的封装思路及实践应用

文章最后也会给到本文中示例的全部源码

什么是面包屑

面包屑导航(Breadcrumb Navigation)这个概念来自童话故事“汉赛尔和格莱特”,当汉赛尔和格莱特穿过森林时,不小心迷路了,但是他们发现沿途走过的地方都撒下了面包屑,让这些面包屑来帮助他们找到回家的路

面包屑的作用如何定义?

面包屑导航被当作一种有效的视觉救援,指引用户在网站层级中所处的位置,你需要了解以下三种信息

  • 我在哪儿? 面包屑导航提醒浏览者他当前处于整个网站层级中具体位置
  • 我能去到哪里? 面包屑导航能够提高用户对网站的章节和页面的搜寻能力,比起只是放置一个菜单,放置一排面包屑导航更容易让人理解网站的结构
  • 我将去哪里? 面包屑导航能将内容进行关联,井且促进浏览(比如电商网站的用户可能进到一个商品详情页之后,发现这个商品并不是自己想要的,这个时候用户可能有意愿浏览同类的商品),同时,又反过来降低了整个网站的跳出率

面包屑的应用场景

面包屑组件在 B 端产品是比较常见的元素,它扮演着重要的导航角色,主要应用场景包括:

  1. 层级导航清晰展示

    • 快速展示用户当前所处页面的路径
    • 帮助用户理解系统的层级结构
  2. 快速回溯

    • 提供快捷的上级页面跳转入口
    • 减少用户多次点击返回的操作成本
  3. 提升用户体验

    • 增强系统的可用性和导航性
    • 降低用户在复杂系统中的迷失感

Ant Design面包屑设计板块中,有这么一段话:Breadcrumb 的本质是了解当前所处页面的位置,并能向上导航

面包屑的封装思路及目标

在本文中,我们会按照以下思路进行逐步分析实现:

  1. 满足导航数据的基本层级展现
  2. 满足 ElBreadcrumb 的原有配置属性
  3. 增强面包屑组件配置项,比如支持图标的隐现、可配置路由跳转方式
  4. ElBreadcrumb 的样式美化
  5. 提供流畅的路由切换动画、保证用户体验的连贯性

本文默认使用 Element Plus 的 ElBreadcrumb 作为二次封装的基础组件,你可以先了解一下 ElBreadcrumb 组件及 Api

本文开发环境是: Vue3 + TS + Tailwindcss + scss

面包屑的基本导航功能

首先,先要来了解一个属性,Vue Router 中的 router.matched,它是一个数组,表示当前路由对象中与当前路径匹配的所有路由记录(RouteRecord),简单一点来说,router.matched 用于存储匹配的路由记录

<script setup lang="ts">
import { useRoute } from 'vue-router';

const currentRoute = useRoute();
console.log(currentRoute);
</script>

image

利用 matched,可以动态生成页面的面包屑,展示从父级到子级的层级关系

我们新建一个 breadcrumb.vue 文件,表示这个文件用于二次封装 ElBreadcrumb,然后写入以下内容:

<script setup lang="ts">
import { ElBreadcrumb, ElBreadcrumbItem } from 'element-plus';
import { useRoute } from 'vue-router';
import type { RouteLocationMatched } from 'vue-router';

import { computed } from 'vue';

defineOptions({
  name: 'Breadcrumb',
});

const currentRoute = useRoute();

/** 获取路由路径 */
const getPath = (item: RouteLocationMatched): string | Object => {
  if (!item) return '';
  if (item.meta?.isReadonlyBreadcrumb) return '';
  return { path: item.redirect ? item.redirect : item.path };
};

/** 面包屑列表 */
const breadcrumbList = computed(() =>
  currentRoute.matched.filter((item) => !item.meta.isHideBreadcrumb),
);
</script>

<template>
  <ElBreadcrumb>
    <ElBreadcrumbItem v-for="item in breadcrumbList" :key="item.path" :to="getPath(item)">
        <span>{{ item.meta.title }}</span>
    </ElBreadcrumbItem>
  </ElBreadcrumb>
</template>

上面内容中实现了一个基本的面包屑:

  • breadcrumbList:面包屑列表,主要是通过 router.matched Api 实现
  • getPath:获取路由路径的函数

满足原有配置属性

满足原有配置属性比较简单,利用 $attrs 即可实现,具有的参数配置,参阅 Breadcrumb 面包屑

  <ElBreadcrumb v-bind="$attrs">
  //...
  </ElBreadcrumb>

面包屑个性化(图标隐现、路由跳转方式)

面包屑除了基本的导航文本展示外,可以有更多丰富的内容,比如每个路由的元信息中可能会存储一个 icon 图标文本,再比如点击路由时的跳转方式,下面我们会来丰富这些配置:

创建一个 typing.ts 文件,表示类型文件,定义一个类型 BreadcrumbProps

export interface BreadcrumbProps {
  /**
   * 如果设置该属性为 true, 导航将不会留下历史记录
   * @default false
   * @see https://element-plus.org/zh-CN/component/breadcrumb.html#breadcrumbitem-attributes
   */
  replace?: boolean;

  /**
   * 是否显示面包屑图标
   * @default true
   */
  isShowIcon?: boolean;
}

基本实现:

<script setup lang="ts">
import { ElBreadcrumb, ElBreadcrumbItem } from 'element-plus';
import { useRoute } from 'vue-router';
import type { RouteLocationMatched } from 'vue-router';

import type { BreadcrumbProps } from './typing';
import { computed, h } from 'vue';
import { AppIcon } from '@/components/common/app-icon';

defineOptions({
  name: 'Breadcrumb',
});
const currentRoute = useRoute();

const props = withDefaults(defineProps<BreadcrumbProps>(), {
  replace: false,
  isHideIcon: false,
});

//...

/** 渲染图标 */
const renderIcon = (item: RouteLocationMatched) => {
  if (!props.isShowIcon || !item.meta.icon) return null;
  return h(AppIcon, { icon: item.meta.icon });
};

</script>

<template>
  <ElBreadcrumb v-bind="$attrs" :class="breadcrumbClassName">
      <ElBreadcrumbItem
        v-for="item in breadcrumbList"
        :key="item.path"
        :to="getPath(item)"
        :replace
      >
        <div class="space-x-1">
          <Component :is="renderIcon(item)" />
          <span>{{ item.meta.title }}</span>
        </div>
      </ElBreadcrumbItem>
  </ElBreadcrumb>
</template>

注意,请确保你的路由 meta 信息中拥有一个 icon 属性,否则图标相关的内容将不起效

上面代码中,实现了图标的显隐配置、ElBreadcrumbItemreplace 属性配置等

需要注意的是文中的 AppIcon 组件是内部实现的组件,你可以在这里找到它。当然,你也可以替换成 ElIcon

面包屑的样式美化及切换动画

样式美化

Element Plus 中面包屑组件的样式变化不多,算是文本形式,顶多配置一下图标分隔符,我们下面来对基本的面包屑进行样式美化

Pasted image 20241225222201

还记得我们上面定义了一个 BreadcrumbProps 嘛,在这基础上,我们新添一个 styleType,它接受一个联合类型:

  • default:默认面包屑文本样式
  • arrow:箭头面包屑样式
  • parallelogram:平行四边形面包屑样式
type BreadcrumbStyleType = 'default' | 'arrow' | 'parallelogram';

export type BreadcrumbStyleObj = {
  [key in BreadcrumbStyleType]: string;
};

export interface BreadcrumbProps {
  //...
  
  /**
   * 面包屑样式
   * @default default
   */
  type?: BreadcrumbStyleType;
}
<script setup lang="ts">
import type { BreadcrumbEmits, BreadcrumbProps, BreadcrumbStyleObj } from './typing';

//...

const props = withDefaults(defineProps<BreadcrumbProps>(), {
  replace: false,
  isHideIcon: false,
  styleType: 'default',// 默认为文本样式
});

/** 获取面包屑Class样式 */
const breadcrumbClassName = computed(() => {
  const className: BreadcrumbStyleObj = {
    arrow: 'breadcrumb-arrow',
    default: 'breadcrumb-default',
    parallelogram: 'breadcrumb-parallelogram',
  };
  return className[props.styleType];
});
</script>

<template>
  <ElBreadcrumb v-bind="$attrs" :class="breadcrumbClassName">
      <ElBreadcrumbItem
        v-for="item in breadcrumbList"
        :key="item.path"
        :to="getPath(item)"
        :replace
      >
        <div class="space-x-1">
          <Component :is="renderIcon(item)" />
          <span>{{ item.meta.title }}</span>
        </div>
      </ElBreadcrumbItem>
  </ElBreadcrumb>
</template>

在上面这一步,我们主要做的事,是给 ElBreadcrumb 加上不同的类名来应用样式

既然类名加上了,我们需要给到各类名对应的样式,复制如下样式即可

注意,这里默认你使用了 Scss
<style scoped lang="scss">
$height: 24px;

@mixin breadcrumb__inner($padding: 0 4px 0 16px, $bgColor: var(--el-fill-color-light)) {
  position: relative;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  height: $height;
  padding: $padding;
  text-decoration: none;
  background-color: $bgColor;
}

.breadcrumb {
  //箭头样式
  &-arrow {
    :deep(.el-breadcrumb__item) {
      position: relative;
      margin-right: 12px;

      .el-breadcrumb__inner {
        @include breadcrumb__inner();

        &::before,
        &::after {
          position: absolute;
          top: 0;
          z-index: -1;
          content: '';
          border: calc($height/2) solid transparent;
        }

        &::before {
          left: -1px;
          border-left-color: var(--el-bg-color);
        }

        &::after {
          right: -23px;
          border-left-color: var(--el-fill-color-light);
        }

        &:hover {
          background: var(--el-fill-color);

          &::after {
            border-left-color: var(--el-fill-color);
          }
        }
      }
    }

    :deep(.el-breadcrumb__separator) {
      display: none;
    }
  }

  //平行四边形样式
  &-parallelogram {
    :deep(.el-breadcrumb__item) {
      position: relative;
      margin-right: 8px;

      .el-breadcrumb__inner {
        @include breadcrumb__inner(4px 10px, transparent);

        &::before {
          position: absolute;
          top: 0;
          left: 0;
          z-index: -1;
          width: 100%;
          height: 100%;
          content: '';
          background-color: var(--el-fill-color-light);
          transform: skew(-20deg);
        }
      }
    }

    :deep(.el-breadcrumb__separator) {
      display: none;
    }
  }
}
</style>

然后,根据属性 styleType 传入不同的值,即可得到默认文本、箭头、平行四边形的面包屑样式

默认面包屑:

Pasted image 20241225222710

箭头面包屑:

Pasted image 20241225222630

平行四边形面包屑:

Pasted image 20241225222616

切换动画

面包屑根据你的路由、地址会进行不断变化,我们给它一个平滑的动画效果来更符合视觉感受

这里需要用到 Vue 的内置组件 <TransitionGroup> ,你可以在 Vue 官网的 TransitionGroup 章节找到更多信息

写入以下动画

/* 面包屑切换动画 */
.breadcrumb-basic-enter-active {
  transition: all 0.25s;
}

.breadcrumb-basic-enter-from,
.breadcrumb-basic-leave-active {
  opacity: 0;
  transform: translateX(20px) skewX(-20deg);
}

然后在封装的组件中使用 <TransitionGroup> 包裹

<template>
  <ElBreadcrumb v-bind="$attrs" :class="breadcrumbClassName">
    <TransitionGroup name="breadcrumb-basic">
      //...
    </TransitionGroup>
  </ElBreadcrumb>
</template>

这个动画的最终效果是这样的:

Kapture 2024-12-25 at 22 47 42

参考资料

源码

本文中的所有实例源码,你可以在这里找到

了解更多

系列专栏地址:GitHub 博客 | 掘金专栏 | 思否专栏

实战项目:vue-clean-admin

专栏往期回顾:

  1. 收下这份 Vue + TS + Vite 中后台系统搭建指南,从此不再害怕建项目
  2. 中后台开发必修课:Vue 项目中 Pinia 与 Router 完全攻略
  3. 用了这些 Vite 配置技巧,同事都以为我开挂了
  4. 受够了团队代码风格不统一?7千字教你从零搭建代码规范体系
  5. 开发者必看!在团队中我是这样实现 Git 提交规范化的
  6. 告别繁琐!Vue3 组合式函数解锁 Echarts 封装新姿势

交流讨论

文章如有错误或需要改进之处,欢迎指正


十五
10 声望3 粉丝