likes
comments
collection
share

Vue3中使用各类字体图标的正确姿势:本地SVG、Iconfont、FontAwesome、ElementPlus(Icon篇)

作者站长头像
站长
· 阅读数 6

前言

最近,在项目的开发中,我们规划了一个 Icon 组件,我们希望通过这个组件,能直接同时使用多种图标库的图标(一种语法,实现无限的图标扩展和高度兼容性)并且,实现该 Icon 之后,理应还有一个图标选择器,可以加载出不同图标库的所有可用图标,方便直接选择使用,文末附组件完整代码

由于项目是基于Vite的,所以本文会出现少量只在Vite下才能运行的代码(vite专有api等)。

我们的IconIcon选择器组件,实现了以下常用图标库的支持,如果还有其它你喜欢的图标库,可以参考我们的实现方式,自行加上即可。

  1. 本地SVG图标:直接将svg文件放入指定的文件夹内,实现自动加载该文件夹所有的svg,并利用Icon组件直接使用,无需手动import
  2. ElementPlus的icon,首先使用官方提供的方法全局注册,然后和Icon组件整合,实现语法的兼容性。
  3. Iconfont(阿里巴巴矢量图标库),实现了自动载入Font clas(css链接,载入后即可通过class来使用对应的字体图标),实现Icon组件的语法兼容性,然后自动解析出Font class内的所有图标名称,以供图标选择器使用。
  4. FontAwesome,这是一款很常用的图标库,包含了675个图标,Icon组件实现了自动加载,语法兼容;并且自动解析所有图标名称,以供图标选择器使用。

使用四种图标的语法

<!-- 本地图标 -->
<Icon name="local-图标文件名" size="18px" color="#000000" />

<!-- Element Plus图标 -->
<Icon name="el-icon-图标名" size="18px" color="#000000" />

<!-- FontAwesome图标 -->
<Icon name="fa fa-图标名" size="18px" color="#000000" />

<!-- Iconfont图标 -->
<Icon name="iconfont 图标名" size="18px" color="#000000" />

具体实现

目录结构

BuildAdmin
├─src
│  │  App.vue
│  │  main.ts
|  |  vite.config.ts
│  ├─assets
│  │  └─icons 存放本地SVG文件的文件夹
│  ├─components
│  │  ├─icon
│  │  ├─svg
│  │  │  ├─index.ts 加载本地SVG文件的实现
│  │  │  ├─index.vue svg显示组件的实现
│  │  ├─index.vue Icon 组件的实现
│  │  └─selector.vue 图标选择器组件的的实现
|  ├─utils
│  │  └─common.ts公共辅助函数库

本地SVG的实现

我们准备了一个目录/src/assets/icons,将svg文件放入其中,实现自动加载全部文件(不依赖第三方包),并直接以:<Icon name="local-图标文件名" />的语法使用图标。

第一步:SVG文件读取准备

文件:/src/components/icon/svg/index.ts

// 从fs库导入读取文件和读取文件夹的函数
// fs库是node自带的,无需通过npm进行install
import { readFileSync, readdirSync } from 'fs'

// 定义一个变量以保存所有的icon文件名称
let iconNames: string[] = []

众所周知,svg文件,是以XML的语法定义的矢量图片文件,我们需要对svg的XML内容进行一些特殊处理,比如清理掉width、height属性等,我们定义了一些正则表达式,读取到svg文件内容之后,将这些正则匹配结果进行处理:

const svgTitle = /<svg([^>+].*?)>/
const clearHeightWidth = /(width|height)="([^>+].*?)"/g
const hasViewBox = /(viewBox="[^>+].*?")/g
const clearReturn = /(\r)|(\n)/g
const clearFill = /(fill="[^>+].*?")/g

接下来,我们首先实现一个查找SVG文件的函数(递归),将svg文件内容构建为symbol元素

function findSvgFile(dir: string, perfix: string = 'local'): string[] {
    const svgRes = [] // 一个目录下所有的svg文件资源

    const dirents = readdirSync(dir, {
        withFileTypes: true,
    })
    for (const dirent of dirents) {
        
        // 替换掉.svg文件后缀,然后存入预设的图标名称数组内
        iconNames.push(`${perfix}-${dirent.name.replace('.svg', '')}`)

        if (dirent.isDirectory()) {
            svgRes.push(...findSvgFile(dir + dirent.name + '/'))
        } else {

            // 读取svg文件内容,并对内容进行处理,组装为一个 symbol 元素,该元素的id属性是文件名称
            const svg = readFileSync(dir + dirent.name)
                .toString()
                .replace(clearReturn, '')
                .replace(clearFill, 'fill=""')
                .replace(svgTitle, ($1, $2) => {
                    let width = 0
                    let height = 0
                    let content = $2.replace(clearHeightWidth, (s1: string, s2: string, s3: number) => {
                        if (s2 === 'width') {
                            width = s3
                        } else if (s2 === 'height') {
                            height = s3
                        }
                        return ''
                    })
                    if (!hasViewBox.test($2)) {
                        content += `viewBox="0 0 ${width} ${height}"`
                    }
                    return `<symbol id="${perfix}-${dirent.name.replace('.svg', '')}" ${content}>`
                })
                .replace('</svg>', '</symbol>')
            svgRes.push(svg)
        }
    }
    return svgRes
}

继续定义一个构建svg元素的函数,在页面body标签内,插入所有svg文件的内容,本函数将在运行Vite build/dev时执行,修改Vite生成的html代码。

export const svgBuilder = (path: string, perfix: string = 'local') => {
    if (path === '') return
    
    // 使用以上定义的查找svg函数,获得所有的.svg文件内容
    const res = findSvgFile(path, perfix)
    return {
        name: 'svg-transform',
        transformIndexHtml(html: string) {
            return html.replace(
                '<body>',
                `
                <body>
                <svg id="local-icon" data-icon-name="${iconNames.join(
                    ','
                )}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position: absolute; width: 0; height: 0">
                ${res.join('')}
                </svg>
                `
            )
        },
    }
}

第二步:使用读取好的SVG文件

我们在vite.config.ts文件中,导入我们刚刚写好的函数:

import type { UserConfig } from 'vite'
import { svgBuilder } from '/@/components/icon/svg/index'

随后,在该文件的viteConfigplugins中,添加svg构建函数的使用,使得Vite构建时,可以自动加载好svg文件:

// 示例代码,请注意如果有其他的 plugin 请自行添加
const viteConfig = (): UserConfig => {
    // ...
    return {
        plugins: [svgBuilder('./src/assets/icons/')],
    }
}

export default viteConfig

第三步:SVG的显示组件实现

这个就非常简单了,使用svg标签,利用上面构建好的的svg内容的symbol元素的id属性,直接就可以显示,代码位置:/src/components/icon/svg/index.vue

<template>
    <svg class="svg-icon icon" :style="iconStyle">
        <use :href="iconName" />
    </svg>
</template>

<script setup lang="ts">
import { computed, CSSProperties } from 'vue'

interface Props {
    // 图标文件名
    name: string
    // 图标大小
    size: string
    // 图标颜色
    color: string
}

const props = withDefaults(defineProps<Props>(), {
    name: '',
    size: '18px',
    color: '#000000',
})

const s = `${props.size.replace('px', '')}px`
const iconName = computed(() => `#${props.name}`)
const iconStyle = computed((): CSSProperties => {
    return {
        color: props.color,
        fontSize: s,
    }
})
const urlIconStyle = computed(() => {
    return {
        width: s,
        height: s,
        mask: `url(${props.name}) no-repeat 50% 50%`,
        '-webkit-mask': `url(${props.name}) no-repeat 50% 50%`,
    }
})
</script>

<style scoped>
.svg-icon {
    width: 1em;
    height: 1em;
    fill: currentColor;
    overflow: hidden;
}
</style>

此时,本地svg图标的准备工作就已经完成了,但还不能以<Icon name="local-图标文件名" />的语法使用图标,因为Icon组件还没有实现,它的实现,我们放在了文章结尾。

ElementPlus的Icon

首先使用 ElementPlus 官方提供的方法全局注册所有图标组件,然后和Icon组件整合,实现直接以:<Icon name="el-icon-图标名" />的语法使用图标。

全局注册所有图标-element官方文档

第一步:安装图标库

npm install @element-plus/icons-vue

第二步:准备注册图标的函数

我们在/src/utils/common.ts文件中,提前准备了注册所有图标的函数,以供main.ts中直接使用。

import * as elIcons from '@element-plus/icons-vue'

/*
* 全局注册element Plus的icon
*/
export function registerIcons(app: App) {
    const icons = elIcons as any
    for (const i in icons) {
        app.component(`el-icon-${icons[i].name}`, icons[i])
    }
}

第三步:在main.ts中注册图标

import { createApp } from 'vue'
const app = createApp(App)
registerIcons(app)
// ...
app.mount('#app')

此时,element Plus图标的准备工作就已经完成了,但还不能以<Icon name="el-icon-图标名" />的语法使用图标,因为Icon组件还没有实现,它的实现,我们放在了文章结尾。

Iconfont 和 FontAwesome 图标

我们在/src/utils/common.ts文件中,提前准备了加载css文件的函数。

/* 
 * 加载网络css文件
 */
export function loadCss(url: string): void {
    const link = document.createElement('link')
    link.rel = 'stylesheet'
    link.href = url
    link.crossOrigin = 'anonymous'
    document.getElementsByTagName('head')[0].appendChild(link)
}

本函数实现向页面动态插入link标签,将传递的url参数的资源,加载到网页内,Iconfont和FontAwesome的资源,可以直接通过此函数载入到网页,示例如下:

/src/App.vue文件

<script setup lang="ts">
import { onMounted } from 'vue'
import { loadCss } from '/@/utils/common'

onMounted(() => {
    // 加载 FontAwesome 所有图标,Url由官网提供
    loadCss('//netdna.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css')
})
</script>

你可以通过以上函数,实现FontAwesome、Iconfont的图标资源(Font class)加载。

Icon组件的实现

通过上面的准备工作,我们终于将四种图标全部都引入到了我们的环境内,接下来,只需要通过Icon组件,统一显示图标的语法即可。

实现非常简单,判断传递的name属性,确定是何种类的的图标,然后创建VNode渲染即可。

<script lang="ts">
import { createVNode, resolveComponent, defineComponent, computed, CSSProperties } from 'vue'
import svg from '/@/components/icon/svg/index.vue'
export default defineComponent({
    name: 'Icon',
    props: {
        name: {
            type: String,
            required: true,
        },
        size: {
            type: String,
            default: '18px',
        },
        color: {
            type: String,
            default: '#000000',
        },
    },
    setup(props) {
        const iconStyle = computed((): CSSProperties => {
            const { size, color } = props
            let s = `${size.replace('px', '')}px`
            return {
                fontSize: s,
                color: color,
            }
        })

        if (props.name.indexOf('el-icon-') === 0) {
            return () => createVNode('el-icon', { class: 'icon el-icon', style: iconStyle.value }, [createVNode(resolveComponent(props.name))])
        } else if (props.name.indexOf('local-') === 0) {
            return () => createVNode(svg, { name: props.name, size: props.size, color: props.color })
        } else {
            return () => createVNode('i', { class: [props.name, 'icon'], style: iconStyle.value })
        }
    },
})
</script>

图标选择器

可以加载出以上四种图标库的所有可用图标,方便直接选择使用,我们的加载方式是通过解析css文件内容获取图标名称(非手动列出图标名称等),此文篇幅有限,我们将在下一篇文章,继续详细讲解图标选择器的实现。

完整代码

支持四种图标的 Icon 组件和 图标选择器组件 ,是为BuildAdmin实现的,所以可以直接在仓库中找到组件的完整代码,另外我们还实现了CRUD代码生成,内置WEB终端,基于ThinkPHP6+Vue3(setup)+Vite+Pinia+等,免费开源,无需授权即可商用,欢迎大家体验和提出意见建议。