从0搭建Vue3组件库之Icon组件

Icon组件想必大家都不陌生,本篇文章将为大家详细介绍Icon组件是如何实现的。

图标的发展

lnline SVG VS Font Icon :

  • svg完全可以控制,fonticon只能控制字符相关的属性
  • font icon需要下载的字体文件较大,除非自己切割打包。
  • font icon会遇到一些比较奇怪的问题。

Fontawesome

icon组件是一个组件库不可或缺的存在,现阶段也有很多优秀的图标库,所以我们也没有必要去从0到1去开发一款新的组件库,这也不是本组件库的目的,所以本次Icon组件将借助 Font Awesome 进行二次开发。

java 复制代码
// 安装 svg core
npm i--save @fortawesome/fontawesome-svg-core
// 安装图标库
npm i --save @fontawesome/free-solid-svg-icons
// 安装基于 vue3 的包
npm i --save @fortawesome/vue-fontawesome@latest-3

使用:

javascript 复制代码
// main.ts
import './styles/index.css'
import { createApp } from 'vue'
import App from './App.vue'
// library 是 Font Awesome SVG 图标库的一个核心对象,用于管理和存储所有可用的图标
import { library } from '@fortawesome/fontawesome-svg-core'
//fas 是 Font Awesome 提供的一个集合,包含了所有 solid 风格的免费图标
import { fas } from '@fortawesome/free-solid-svg-icons'
// 将 fas 集合中的所有 solid 风格图标添加到 library 对象中,然后就可以使用这些图标了
library.add(fas)

createApp(App).mount('#app')

二次开发

属性:

typescript 复制代码
import type { IconDefinition } from "@fortawesome/fontawesome-svg-core";

// 图标组件的属性。
export interface IconProps {
  // 如果为 true,则显示边框样式。
  border?: boolean;

  // 如果为 true,则使用固定宽度样式。
  fixedWidth?: boolean;

  // 设置图标的翻转方向,可选值为 "horizontal" 水平翻转, "vertical" 垂直翻转, 或 "both" 同时水平和垂直翻转。
  flip?: "horizontal" | "vertical" | "both";

  // 图标的数据,可以是一个对象、字符串数组(包含图标集和图标的名称)、字符串(图标名称)或 IconDefinition 类型。
  icon: object | Array<string> | string | IconDefinition;

  // 作为遮罩层的图标数据,与 'icon' 属性类似。
  mask?: object | Array<string> | string;

  // 作为遮罩层的图标 ID 数据,与 'mask' 属性类似。
  maskId?: object | Array<string> | string;

  // 如果为 true,则将图标渲染为列表项的一部分。
  listItem?: boolean;

  // 设置图标的浮动位置,可选值为 "right" 右浮动 或 "left" 左浮动。
  pull?: "right" | "left";

  // 如果为 true,则使图标以脉冲动画效果显示。
  pulse?: boolean;

  // 设置图标的旋转角度,只能是 90, 180, 270 度,接受数字或字符串形式。
  rotation?: 90 | 180 | 270 | "90" | "180" | "270";

  // 如果为 true,则交换图标的不透明度(通常用于动画效果)。
  swapOpacity?: boolean;

  // 设置图标的尺寸大小,提供了多种预设大小选项。
  size?:
    | "2xs"
    | "xs"
    | "sm"
    | "lg"
    | "xl"
    | "2xl"
    | "1x"
    | "2x"
    | "3x"
    | "4x"
    | "5x"
    | "6x"
    | "7x"
    | "8x"
    | "9x"
    | "10x";

  // 如果为 true,则使图标以旋转动画效果显示。
  spin?: boolean;

  // 图标的变换配置,可以是字符串或者对象,具体取决于库的实现。
  transform?: object | string;

  // 如果为 true,则将图标渲染为 <symbol> 元素;如果提供字符串,则用作 <symbol> 的 id。
  symbol?: boolean | string;

  // 图标的标题文本。
  title?: string;

  // 图标的标题 ID,用于关联 <title> 元素。
  titleId?: string;

  // 如果为 true,则应用反色样式。
  inverse?: boolean;

  // 如果为 true,则使图标以弹跳动画效果显示。
  bounce?: boolean;

  // 如果为 true,则使图标以摇晃动画效果显示。
  shake?: boolean;

  // 如果为 true,则使图标以心跳动画效果显示。
  beat?: boolean;

  // 如果为 true,则使图标以渐隐动画效果显示。
  fade?: boolean;

  // 如果为 true,则使图标以心跳并渐隐的组合动画效果显示。
  beatFade?: boolean;

  // 如果为 true,则使图标以旋转加脉冲的组合动画效果显示。
  spinPulse?: boolean;

  // 如果为 true,则使图标以逆向旋转的动画效果显示。
  spinReverse?: boolean;

  // 设置图标的类型,通常用于设置主题颜色,如 "primary", "success", "info", "warning", "danger"。
  type?: "primary" | "success" | "info" | "warning" | "danger";

  // 设置图标的颜色,接受任何有效的 CSS 颜色值。
  color?: string;
}

Icon.vue:

xml 复制代码
<template>
<!-- 因为已经禁用了透传,所以这里要使用 $attrs 来接收非props的属性或者事件 -->
<!-- 使用 v-bind="$attrs" 将父级组件所有的未被 props 所接收的特性绑定到 i 组件上。 -->
  <i
    class="jd-icon"
    :class="{ [`jd-icon--${type}`]: type }"
    :style="customStyles"
    v-bind="$attrs"
  >
    <font-awesome-icon v-bind="filteredProps" />
  </i>
</template>

<script setup lang="ts">
import { computed } from "vue";
import type { IconProps } from "./types";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { omit } from "lodash-es";

// 定义组件名称
defineOptions({
  name: "JdIcon",
  // 禁止透传,简单来说,把`inheritAttrs`设置为`false`,避免给Icon组件设置的属性被添加到myIcon组件的根元素div上。
  inheritAttrs: false, 
});

// 定义组件属性
const props = defineProps<IconProps>();
// 根据一个响应式对象,完成一个xx计算,记得用computed,这样子props变化,filteredProps也会变化
// 这里使用omit方法将type和color属性过滤掉 
// 因为 <font-awesome-icon /> 本身是没有这两个属性,所以最好过滤掉 
// 这两个属性是要添加到 <font-awesome-icon /> 的父元素
const filteredProps = computed(() => omit(props, ["type", "color"]));
// 根据传递进来的属性确定图标的样式
const customStyles = computed(() => {
  return props.color ? { color: props.color } : {};
});
</script>

组件样式

css 复制代码
.jd-icon {
  --jd-icon-color: inherit;
  display: inline-flex;
  justify-content: center;
  align-items: center;
  position: relative;
  fill: currentColor;
  color: var(--jd-icon-color);
  font-size: inherit;
}

@each $val in primary, info, success, warning, danger {
  .jd-icon--$(val) {
    --jd-icon-color: var(--jd-color-$(val));
  }
}

拓展

$attrs 是 Vue 3 中的一个特殊属性,它包含了所有传递给组件的、未被声明为 props 的属性(包括事件监听器)。当你在组件中使用 $attrs 时,你可以访问这些属性,并且可以选择性地将它们应用到子组件或元素上。这在创建高阶组件或封装其他组件时特别有用,因为它允许你透传属性,而不必手动定义每一个。

参考:juejin.cn/post/734269...

$attrs 的作用

  • 捕获未声明的属性 :当组件接收到一些属性,但这些属性并没有通过 props 明确声明时,Vue 会将这些属性存储在 $attrs 对象中。
  • 传递属性 :你可以选择将 $attrs 中的属性绑定到子组件或任意 HTML 元素上,通常使用 v-bind="$attrs" 语法。
  • 避免污染根元素 :如果你设置了 inheritAttrs: false,那么未声明的属性将不会自动应用到组件的根元素上,而是保留在 $attrs 中供你处理。

禁用 Attributes 继承

如果你不想要 一个组件自动地继承 attribute,你可以在组件选项中设置 inheritAttrs: false

从 3.3 开始你也可以直接在 <script setup> 中使用 defineOptions

xml 复制代码
<script setup>
defineOptions({
  inheritAttrs: false
})
// ...setup 逻辑
</script>

最常见的需要禁用 attribute 继承的场景就是 attribute 需要应用在根节点以外的其他元素上。通过设置 inheritAttrs 选项为 false,你可以完全控制透传进来的 attribute 被如何使用。

这些透传进来的 attribute 可以在模板的表达式中直接用 $attrs 访问到。

css 复制代码
<span>Fallthrough attribute: {{ $attrs }}</span>

这个 $attrs 对象包含了除组件所声明的 propsemits 之外的所有其他 attribute,例如 classstylev-on 监听器等等。

有几点需要注意:

  • 和 props 有所不同,透传 attributes 在 JavaScript 中保留了它们原始的大小写,所以像 foo-bar 这样的一个 attribute 需要通过 $attrs['foo-bar'] 来访问。
  • @click 这样的一个 v-on 事件监听器将在此对象下被暴露为一个函数 $attrs.onClick

现在我们要再次使用一下之前小节中的 <MyButton> 组件例子。有时候我们可能为了样式,需要在 <button> 元素外包装一层 <div>

ini 复制代码
<div class="btn-wrapper">
  <button class="btn">Click Me</button>
</div>

我们想要所有像 classv-on 监听器这样的透传 attribute 都应用在内部的 <button> 上而不是外层的 <div> 上。我们可以通过设定 inheritAttrs: false 和使用 v-bind="$attrs" 来实现:

ini 复制代码
<div class="btn-wrapper">
  <button class="btn" v-bind="$attrs">Click Me</button>
</div>

小提示:没有参数的 v-bind 会将一个对象的所有属性都作为 attribute 应用到目标元素上。

使用第三方组件的属性

封装一个elementUI的el-input输入框组件称为myInput,若要在myInput组件上添加一个disabled属性来禁用输入框,要如何实现呢?一般同学会这么做

xml 复制代码
//myInput.vue
<template>
  <div>
    <el-input v-model="inputVal" :disabled="disabled"></el-input>
  </div>
</template>
<script>
export default {
  props: {
    value: {
      type: String,
      default: '',
    },
    disabled: {
      type: Boolean,
      default: false
    }
  },
  computed: {
    inputVal: {
      get() {
        return this.value;
      },
      set(val) {
        this.$emit('input', val);
      }
    }
  }
}
</script>

过一段时间后又要在myInput组件上添加el-input组件的其它属性,el-input组件总共有27个多属性,那该怎么呢,难道一个个用prop传进去,这样不仅繁琐而且可读性差,可以用$attrs一步到位,先来看一下attrs的官方定义。

$attrs: 包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。当一个组件没有声明任何prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件

xml 复制代码
//myInput.vue
<template>
  <div>
    <el-input v-model="input" v-bind="$attrs"></el-input>
  </div>
</template>

这还不够,还得把inheritAttrs选项设置为false,为什么呢,来看一下inheritAttrs选项的官方定义就明白了。

默认情况下父作用域的不被认作 props 的 attribute 绑定 (attribute bindings) 将会"回退"且作为普通的 HTML attribute 应用在子组件的根元素上。当撰写包裹一个目标元素或另一个组件的组件时,这可能不会总是符合预期行为。通过设置 inheritAttrsfalse,这些默认行为将会被去掉。而通过 $attrs 可以让这些 attribute 生效,且可以通过 v-bind 显性的绑定到非根元素上。注意:这个选项不影响 class 和 style 绑定。

简单来说,把inheritAttrs设置为false,避免给myInput组件设置的属性被添加到myInput组件的根元素div上。

xml 复制代码
//myInput.vue
<template>
  <div>
    <el-input v-model="input" v-bind="$attrs"></el-input>
  </div>
</template>
<script>
export default {
  inheritAttrs: false,
  props: {
    value: {
      type: String,
      default: '',
    },
  },
  computed: {
    inputVal: {
      get() {
        return this.value;
      },
      set(val) {
        this.$emit('input', val);
      }
    }
  }
}
</script>

这样设置后,在myInput组件上就可以直接使用el-input组件的属性,不管后续el-input组件再增加了多少个属性。

总结

本文主要结合Fontawesome对Icon组件进行了二次开发,但是在开发过程中要注意以下几点:

  • 使用inheritAttrs: false 禁用透传,目的是手动将icon原本的属性进行手动绑定

  • 解决禁用透传以后一些默认属性失效的问题,比如新增class无法传递到Icon,那么这个时候就要使用$attrs来解决

  • inheritAttrs: false不继承属性

  • 使用$props访问所有属性

  • 要注意不继承以后一些默认属性失效的间题$attrs

相关推荐
Excel_VBA表格จุ๊บ2 小时前
wps宏js接入AI功能和接入翻译功能
javascript·wps·js宏
豪宇刘3 小时前
JavaScript 延迟加载的方法
开发语言·javascript
顾尘眠4 小时前
http常用状态码(204,304, 404, 504,502)含义
前端
摇光934 小时前
js迭代器模式
开发语言·javascript·迭代器模式
王先生技术栈5 小时前
思维导图,Android版本实现
java·前端
悠悠:)6 小时前
前端 动图方案
前端
anyup_前端梦工厂6 小时前
了解 ES6 的变量特性:Var、Let、Const
开发语言·javascript·ecmascript
星陈~6 小时前
检测electron打包文件 app.asar
前端·vue.js·electron
Aatroox6 小时前
基于 Nuxt3 + Obsidian 搭建个人博客
前端·node.js
每天都要进步哦6 小时前
Node.js中的fs模块:文件与目录操作(写入、读取、复制、移动、删除、重命名等)
前端·javascript·node.js