Icon组件想必大家都不陌生,本篇文章将为大家详细介绍Icon组件是如何实现的。
图标的发展
- 雪碧图CSSsprint
- Font Icon
- lnline SVG
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
时,你可以访问这些属性,并且可以选择性地将它们应用到子组件或元素上。这在创建高阶组件或封装其他组件时特别有用,因为它允许你透传属性,而不必手动定义每一个。
$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
对象包含了除组件所声明的 props
和 emits
之外的所有其他 attribute,例如 class
,style
,v-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>
我们想要所有像 class
和 v-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 应用在子组件的根元素上。当撰写包裹一个目标元素或另一个组件的组件时,这可能不会总是符合预期行为。通过设置
inheritAttrs
为false
,这些默认行为将会被去掉。而通过$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