衍生需求:按钮集成图标组件 & 图标选择器
本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
专栏上篇文章传送门:Web 中的字体和 SVG 图标,你了解多少?
本节涉及的内容源码可在vue-pro-components c4 分支找到,欢迎 star 支持!
前言本文是基于Vite+AntDesignVue打造业务组件库专栏第 5 篇文章【衍生需求:按钮集成图标组件 & 图标选择器】,聊聊实际业务中与图标组件相关的一些衍生需求,例如:
怎么通过一个简单的icon属性就能在a-button中用上我们的图标组件?怎么实现一个可视化的图标选择器?按钮集成图标组件背景介绍按钮中搭配图标一起用,是再常见不过的场景了。ant-design-vue 的 Button 组件具备自定义图标的能力,具体是通过icon插槽实现的。
image.png虽然能实现,但是感觉写起来也挺复杂的,代码量不少,那么能不能简化成这样呢?只要通过一个icon属性(而不是插槽)就能把图标展示出来呢?最理想的状态是还能同时支持我们自己的业务图标。
//比较理想的用法
//既支持
a-buttontype="primary"icon="SearchOutlined"Search/a-button
a-buttontype="primary"icon="location"Search/a-button
事实上,ant-design-vue没有支持这种能力。
首先,从字符串到组件,是需要一个解析的过程,这对应resolveComponent,简单看下源码,resolveComponent内部会调用resolveAssets,我们发现,这需要将组件注册好,不管是注册到局部还是全局,都可以。
image.png而 ant-design-vue 是一个通用组件库,它提供的图标都是一个个独立的组件,这些组件都在@ant-design/icons-vue这个包里。如果要实现字符串到组件的解析能力,就要求把图标组件都提前注册好,这就违背了按需加载的初衷。
另外, ant-design-vue 也要考虑用户自定义图标的场景,所以综合来看留个插槽算是比较合理的做法。
然而,对业务方来说,通常考虑的是:
大而全:能力丰富,既要有原始组件本身的能力,还能增加一些定制的能力;用起来方便:提供最简单的用法;性能过得去:没有明显的性能负担即可。那么,我们自己来尝试实现一下这些能力。
封装按钮组件a-button我们也不能改,所以,需要先做一个vp-button,它既有a-button的全部能力,还能支持使用各种图标组件,这样才不至于说封装了一个组件,却牺牲了底层的能力。
image.png我们首先要考虑的是:AButton 本身有很多属性,那么我们怎么让 VpButton 同样支持这些属性呢?
有两条路子可供选择:
利用v-bind="$attrs"透传属性,但是出于性能考虑,通过$attrs透传的这部分 attributes 不像 props 那样具备响应式特性。将 AButton 支持的 props,都列入 VpButton 的 props 中,然后 VpButton 再原样通过属性绑定传递给 AButton,这样就能保证这些 props 的响应式依然有效。路子1虽然是最简单的,但缺失响应式这一缺点有时候会很致命。
路子2是比较靠谱的,但是使用起来很繁琐,需要将 AButton 支持的属性重复定义在 VpButton 中。此外,一旦粗心就可能会遗漏一些属性,这就会导致功能是有缺失的,那么怎么解决这些问题呢?
我的思路是:
想办法把 AButton 的 props 定义取出来,与我们要额外扩展的属性做一个合并,统一作为 VpButton 的 props 定义。这样一来,从外部调用者的视角来看,VpButton 支持的属性就是完整的,给人的直观感觉就是:VpButton 是 AButton 的加强版,我可以放心使用。在 VpButton 内部需要封装 AButton,同时要从所有 props 中将属于 AButton 的那部分 props 挑选出来,传递给 AButton,这样对 AButton 来说就是无感的,因为我们传给 AButton 的属性是完全符合要求的。只要我们封装的 VButton 满足了上面这两点,这个组件就是趋近完美的,它向上对调用者提供了更强大的能力,同时向下又包容了 AButton 的能力。
我们来试试看,大致查阅 ant-desigin-vue 的 Button 组件源码,我们可以发现,AButton 的属性是由这些代码构造出来的。
image.pngimage.png我们新建一个button/props.ts文件,尝试一下下面的代码,看看能不能拿到预期的 AButton 属性定义,如果能成功,那就意味着我们就不必一个一个属性地重复定义了,同时也意味着我们得到了一种扩展属性的基本方法。
importbuttonPropsfrom'ant-design-vue/es/button/buttonTypes'
import{initDefaultProps}from'ant-design-vue/es/_util/props-util'
const_buttonProps=initDefaultProps(buttonProps(),{
type:'default',
})
console.log(_buttonProps)
打印出来发现,这就是我们需要的 AButton 的属性定义:
image.png接着我们用一个enhancedProps来定义需要扩展的属性,这里先给出以下几个属性:
exportconstenhancedProps={
//对应自定义图标的名称
ico:{
type:String,
},
//图标的大小
icoSize:{
type:Number,
},
//图标颜色
icoColor:{
type:String,
},
//按钮主体颜色,影响边框颜色,背景色
primaryColor:{
type:String,
},
}
用ico接收图标名称,是为了避免与AButton的icon插槽冲突。
然后我们把_buttonProps和enhancedProps这两部分组成一个完整的props。
exportconstinnerKeys=Object.keys(_buttonProps)
exportconstenhancedKeys=Object.keys(enhancedProps)
exportconstprops={
..._buttonProps,
...enhancedProps,
}
exporttypeVpButtonProps=ExtractPropTypestypeofprops
image.png可以发现属性很完整了,其中框起来的部分是我们扩展的属性,剩下的都是 AButton 支持的属性。
接下来就是看怎么使用这些属性了,直接上组件主体代码,这里用了 tsx 实现。
import { defineComponent } from 'vue'
import { Button } from 'ant-design-vue'
import IconSvg from '../icon-svg'
import { innerKeys, props as buttonProps } from './props'
import { usePickedProps } from '../hooks/props'
export default defineComponent({
name: 'VpButton',
props: buttonProps,
setup(props, { slots }) {
// 把属于 AButton 的属性挑选出来,再绑定到 AButton 上
const innerProps = usePickedProps(props, innerKeys)
return () = (
Button
{...innerProps.value}
class="vp-button"
style={{ backgroundColor: props.primaryColor, borderColor: props.primaryColor }}
v-slots={{
...slots,
default: () = (
{props.ico && !props.loading ? IconSvg icon={props.ico} size={props.icoSize} color={props.icoColor} / : null}
{slots?.default?.()}
),
}}
/Button
)
},
})
这里用到了一个usePickedProps方法,将 AButton 支持的属性全部挑选出来,然后绑定到 AButton 上(因为 props 中有我们扩展的属性,而这部分不需要传给 AButton)。
usePickedProps的逻辑也不复杂,主要是基于lodash-es的pick方法进行属性挑选,然后用computed计算属性返回结果,这样才能保证得到的innerProps是具备响应式特性的。
image.png而在图标这块的处理,除了支持通过ico属性直接展示IconSvg图标,我们依然支持通过icon插槽进行自定义的图标展示,这与 AButton 的默认行为是一致的。
当我们在 packageplayground引入这个 VpButton 组件使用时,会发现报了一个错误Uncaught ReferenceError: React is not defined。
image.png这是因为我们的当前环境还不支持jsx,需要引入一个@vitejs/plugin-vue-jsx插件。
//安装一下jsx插件
lernaadd@vitejs/plugin-vue-jsx--scope=playground--dev
vite.config.ts增加 jsx 相关配置:
image.png由于 VpButton 内部用到了 AButton 和 IconSvg 这两个组件,而这两个组件也是有定义样式的,所以我们在button/style/index.less中引入一下相关的样式依赖。
image.png接着我们测试一下基本效果,基本上可以满足常见使用场景:
image.pngico属性支持多种图标源可行吗?那么有没有可能实现上面说的:用一个ico属性,既能支持 ant-design 的内置图标,又能支持由 IconSvg 组件实现的业务图标呢?我们可以尝试做一下看看。
如上文所述,首先需要有一个字符串到组件的解析过程,这需要用到resolveComponent,这部分逻辑可以内置到 VpButton 组件中。与此同时,还需要将 ant-design 的图标注册到组件上下文中,这部分操作放在业务调用方比较合适(这可以支持按需加载),因为我们不可能把所有 ant-design 的图标都注册到 VpButton 组件中,这会让 VpButton 组件变成一个巨型组件。
好,思路清楚后,我们首先实现 VpButton 内部的逻辑。为了减少判断逻辑,我们通过一个icoSource标识图标的来源,默认为"biz",表示展示 IconSvg 支持的业务图标,同时支持"antd",表示展示 ant-design 的图标。
image.png当icoSource的值为"antd"时,我们利用 Vue 提供的resolveComponent和h进行组件解析和渲染,否则逻辑照旧。
image.png接着我们在App.vue调用一下。
引入PlusOutlined图标组件:
image.png尝试通过icoSource和ico属性渲染出图标:
image.png结果发现,resolveComponent还是找不到 PlusOutlined 组件。
[Vue warn]: Failed to resolve component: PlusOutlined
image.png回头看了一下源码resolveComponent的流程,发现它只会在当前组件实例和应用实例中去寻找组件,而resolveComponent是在 Button 组件中使用的,即便我们在App.vue中引入了 PlusOutlined 也是解析不到的,所以只能在应用实例全局注册 PlusOutlined,类似这样:
image.png效果就出来了:
image.png但是这样用起来也是相当繁琐,虽然实现了功能,但还不如直接用icon插槽简单呢,所以这条路基本上可以选择放弃了。
这部分代码可以见这个版本,相关分支上就不保留这部分代码了。
如果与unplugin-vue-components配套使用,其提供的AntDesignVueResolver也支持自动识别并导入@ant-design/icons-vue中的图标,用起来也算方便。
image.png图标选择器在中后台或者一些低码搭建场景中,很多地方需要动态配置图标,最常见的可能就是给菜单配图标。比较简单的实现方式就是直接用一个文本输入框配置图标名,但是这样并不直观,也容易出错,因为你不确定你输入的图标名是不是对应一个有效的图标。而且这要求操作人员熟知图标的名称,显然不是很方便。
image.png那么能不能提供一个图标选择器进行可视化的配置呢?我们可以来试一试!
要进行图标的选择,首先必须知道有哪些图标,也就是需要有一个图标清单。那么具体怎么做呢?
一个简单粗暴的方法是:项目中维护一个数组,把 icon 名称全部都手动录入。但是这样显得很繁琐,每个业务项目都要手动录,太容易出错了。
另一个方法是:从 iconfont 图标库中寻找有用的信息,基于这些信息编写脚本自动生成一个图标清单。
那么 iconfont 中有哪些我们可以利用的信息呢?
我最开始想的是检查 iconfont 项目调用的接口,从接口中把信息抓出来。确实找到了一个detail.json请求,这里有相关的 icons 数组。
image.png记得前些时间还检查过,iconfont 还没有提供这个 icons 字段,可能最近优化了。
虽然请求是找到了,但是还要考虑调这个请求是不是要验证 token 等身份信息。果不其然,需要验证!
image.png这也就意味着,如果我们想用这个能力,需要打通登录流程,先调登录接口,再调这个 detail.json 的请求。
image.png只要模拟一下这个登录请求即可,看着不复杂,其实做起来不简单,首先要搞清楚 password 的加密策略,还有两个 bx- 开头的字段是怎么得来的,这需要研究一下 iconfont 相关的 js 代码。
而且,我们需要把账号密码存在某个配置文件中,不是很安全,所以也不建议这样做。
js 链接 + 正则取得图标清单我们换个思路,既然不想登录,但是又要获得图标清单,那就只能从一些公开的资源上去做文章了。
还好,iconfont 提供的 js 链接是公开的,而且这里面也包含了图标信息。
image.png我们发现,这里面有一些特征可以捕捉到,只要把符合vp-icon-前缀的内容提取出来,就能得到图标清单。
话不多说,直接上代码,主要是一些正则的逻辑:
importfsfrom"fs"
importaxiosfrom"axios"
constSVG_ICON_SCRIPT_URL="https://at.alicdn.com/t/c/font_3736402_d50r1yq40hw.js"
constSVG_ICON_PREFIX="vp-icon-"
functiongetIcons(str){
constreg=newRegExp(`id="${SVG_ICON_PREFIX}([^"]+)"`);
returnstr.match(/id="([^"]*)"/g).map((item)=item.replace(reg,"$1"));
}
exportasyncfunctiongenIconListJson(){
try{
constres=awaitaxios.get(SVG_ICON_SCRIPT_URL);
console.log(res)
if(res.status===200){
consticonList=getIcons(res.data);
console.log(iconList);
fs.writeFile(newURL("../src/assets/json/icons.json",import.meta.url),JSON.stringify(iconList,null,2),function(err){
if(err){
returnconsole.error(err);
}
console.log("图标清单写入成功!");
}else{
console.error(res.status,res.statusText);
}
}catch(err){
console.error(err);
}
}
genIconListJson();
执行脚本后,就能得到一个 json 文件了,这里有全部的图标名称。我们特意去掉了图标前缀,因为 IconSvg 组件的 icon 属性只需要简单的名称即可,其内部会与前缀拼接。
image.png根据图标清单实现选择器拿到了图标清单,剩下的工作就比较简单了,无非是把图标循环渲染出来,让用户选择即可。同时提供一个搜索功能,方便在图标数量很大时能够通过名字检索。
代码并不复杂,感兴趣的可以 fork 源码看一下。
结语本文以实际业务中与图标组件相关的衍生需求为背景,介绍了如何封装一个基础组件,以及如何在封装组件时既能在基础组件之上做扩展,同时又不牺牲掉基础组件的原有能力。总的来说,这不仅仅是在讲解如何开发一个组件,更多的是介绍一种通用的上层组件封装思想,希望对大家有所帮助。如果您对我的专栏感兴趣,欢迎您订阅关注本专栏[1],接下来可以一同探讨和交流组件库开发过程中遇到的问题。
[1]https://juejin.cn/column/7140103979697963045:https://juejin.cn/column/7140103979697963045
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线