将 iconfont 图标转换成element-plus也能使用的图标组件

在做项目时发现,element-plus的图标组件,不能像文档示例中那样使用 iconfont 的图标。经过研究发现,element-plus的图标封装成了vue组件,组件内容是一个svg,然后以组件的方式引入和调用图标。根据这个思路,我写了一个脚本,将iconfont图标,转换成vue组件,这样就可以完美地统一项目中图标的使用方式了。

ElementPlus 图标的两种使用方式

先来回顾一下 element-plus 使用图标的两种方式

  1. 第一种是直接以组件调用的方式使用
<el-icon><Plus /></el-icon>
  1. 第二种是作为组件的props使用
<el-button type="danger" :icon="Delete" circle />

import { Delete } from '@element-plus/icons-vue'

读取 iconfont 图标内容

iconfont 能够生成svg图标,生成的内容在 iconfont.js 文件中
在这里插入图片描述
从图中可以看到,svg内容是一个字符串,只需要读取这个字符串,然后将每一个图标取出,就可以用来生成vue组件了。使用工具jsdom将svg字符串转换为dom结构,这样就可以使用dom操作读取svg相关信息了。

核心代码:

import fs from 'fs'
import { JSDOM } from 'jsdom'
import { normalizePath } from 'vite'

let file = ''
try {
  // 读取 iconfont.js 文件内容
  file = fs.readFileSync(normalizePath(sourcePath), 'utf-8')
} catch (e) {
  writeIconfontJsFile()
  modifyImportFile()
  return
}
// 提取svg字符串
const svgReg = /<svg>[\s\S]*?<\/svg>/g
const [svgStr] = file.match(svgReg)
// 解析svg字符串为dom对象
const dom = new JSDOM(svgStr)

const document = dom.window.document
// 获取所有的symbol元素,每个symbol代表一个图标
const symbols = document.querySelectorAll('symbol')
if (!symbols.length) {
  return
}

const iconsNames = []

let str = `import { defineComponent, h } from 'vue'\n\n`

symbols.forEach((symbol) => {
  // 获取图标名称,并转换为驼峰命名
  const iconName = toCamelCase(symbol.getAttribute('id')).replace(/_$/, '').replace(/-$/, '')
  iconsNames.push(iconName)
  // 获取viewBox和path,并生成对应的vue组件代码
  const viewBox = symbol.getAttribute('viewBox')
  const path = symbol.querySelectorAll('path')

  const pathArr = []
  path.forEach((p) => {
    const d = p.getAttribute('d')
    pathArr.push(`h('path', { d: '${d}' })`)
  })

  str += `const ${iconName} = defineComponent({ name: '${iconName}', render() { return h('svg', { viewBox: '${viewBox}' }, [${pathArr}]) }})\n\n`
})

str += `export {\n\t${iconsNames.join(',\n\t')}\n}`
// 写入文件
writeIconfontJsFile(str)
modifyImportFile(iconsNames)

生成的结果如下

import { defineComponent, h } from 'vue'

const IconChehuisekuai = defineComponent({ name: 'IconChehuisekuai', render() { return h('svg', { viewBox: '0 0 1024 1024' }, [h('path', { d: 'M64 347.552L320 128v448z' }),h('path', { d: 'M265.472 896v-112h377.824a200 200 0 1 0 0-400H240V272h403.296c172.32 0 312 139.68 312 312S815.616 896 643.296 896H265.472z' })]) }})

export {
	IconChehuisekuai,
}

完整代码

以 vite 插件的方式使用

/**
 * 将iconfont转换为vue组件的插件
 * 在vite.config.js中使用
 * 如果引入新的iconfont后没有变化,重启试一下
 * @example
 * import transformIconfontToComponent from './script/vite-plugin/transform-iconfont-to-component.js'
 * plugins: [transformIconfontToComponent({ sourcePath: 'src/assets/iconfont/iconfont.js', targetPath: 'src/assets/vue-icons/iconfont.js', importPath: 'src/assets/vue-icons/index.js' })]
 *
 * importPath文件中需要添加占位注释,如下:
 * 导入位置
 * /* import iconfont start *\/
 * /* import iconfont end *\/
 * 全局注册位置
 * /* register iconfont start *\/
 * /* register iconfont end *\/
 * 导出位置
 * /* export iconfont start *\/
 * /* export iconfont end *\/
 *
 * 使用以上注释时,\ 需要删除掉
 */

import fs from 'fs'
import { JSDOM } from 'jsdom'
import { normalizePath } from 'vite'

/**
 * 驼峰化, 支持:下划线(_)、小数点(.)、空格( )、冒号(:)、中横线(-)、斜杠(\) 等多种连续分隔符混合使用场景
 * @param {String} name 需要转换的名称
 * @param {Boolean} isFirstUppercase 是否首字母大写,默认否
 * @returns {String} 转换后的名称
 */
function toCamelCase(name, isFirstUppercase = true) {
  if (!name) return name

  const SPECIAL_CHARS_REGEXP = /([:\-_. /]+(.))/g // special chars regexp
  const result = name.replace(SPECIAL_CHARS_REGEXP, function (_, separator, letter, offset) {
    return offset ? letter.toUpperCase() : letter
  })
  // 大驼峰
  if (isFirstUppercase) {
    return result.replace(/^\w/, (p1) => p1.toUpperCase())
  }
  return result
}

export default function (opt) {
  /**
   * @param {String} sourcePath 图标字体文件路径
   * @param {String} targetPath 生成的组件文件路径
   * @param {String} importPath 导入targetPath的路径
   */
  const { sourcePath, targetPath, importPath } = opt

  function writeIconfontJsFile(str = '') {
    // 写入文件
    fs.writeFileSync(normalizePath(targetPath), str)
  }

  function modifyImportFile(iconsNames = []) {
    // 修改注入文件,注册组件
    let indexFileStr = fs.readFileSync(normalizePath(importPath), 'utf-8')

    // 找到注入位置的标志符号,然后插入导入语句
    const importIconfontStartFlag = '/* import iconfont start */'
    const importIconfontEndFlag = '/* import iconfont end */'
    const index1 = indexFileStr.indexOf(importIconfontStartFlag)
    const index2 = indexFileStr.indexOf(importIconfontEndFlag)

    const importIconfontStr = iconsNames?.length
      ? `\nimport {\n\t${iconsNames.join(',\n\t')}\n} from '${normalizePath(targetPath).replace(
          /^src/,
          '@'
        )}'\n`
      : '\n'

    indexFileStr =
      indexFileStr.slice(0, index1 + importIconfontStartFlag.length) +
      importIconfontStr +
      indexFileStr.slice(index2)

    // 找到注册位置的标志符号,然后插入注册语句
    const registerIconfontStartFlag = '/* register iconfont start */'
    const registerIconfontEndFlag = '/* register iconfont end */'
    const index3 = indexFileStr.indexOf(registerIconfontStartFlag)
    const index4 = indexFileStr.indexOf(registerIconfontEndFlag)

    const registerIconfontStr = iconsNames?.length
      ? `\n${iconsNames.map((name) => `app.component('${name}', ${name})`).join('\n')}\n`
      : '\n'

    indexFileStr =
      indexFileStr.slice(0, index3 + registerIconfontStartFlag.length) +
      registerIconfontStr +
      indexFileStr.slice(index4)

    // 找到导出位置的标志符号,然后插入导出语句
    const exportIconfontStartFlag = '/* export iconfont start */'
    const exportIconfontEndFlag = '/* export iconfont end */'
    const index5 = indexFileStr.indexOf(exportIconfontStartFlag)
    const index6 = indexFileStr.indexOf(exportIconfontEndFlag)

    const exportIconfontStr = iconsNames?.length
      ? `\nexport {\n\t${iconsNames.join(',\n\t')}\n}\n`
      : '\n'

    indexFileStr =
      indexFileStr.slice(0, index5 + exportIconfontStartFlag.length) +
      exportIconfontStr +
      indexFileStr.slice(index6)

    fs.writeFileSync(normalizePath(importPath), indexFileStr)
  }

  return {
    name: 'transform-iconfont-to-component',
    buildStart() {
      let file = ''
      try {
        // 读取 iconfont.js 文件内容
        file = fs.readFileSync(normalizePath(sourcePath), 'utf-8')
      } catch (e) {
        writeIconfontJsFile()
        modifyImportFile()
        return
      }
      // 提取svg字符串
      const svgReg = /<svg>[\s\S]*?<\/svg>/g
      const [svgStr] = file.match(svgReg)
      // 解析svg字符串为dom对象
      const dom = new JSDOM(svgStr)

      const document = dom.window.document
      // 获取所有的symbol元素,每个symbol代表一个图标
      const symbols = document.querySelectorAll('symbol')
      if (!symbols.length) {
        return
      }

      const iconsNames = []

      let str = `import { defineComponent, h } from 'vue'\n\n`

      symbols.forEach((symbol) => {
        // 获取图标名称,并转换为驼峰命名
        const iconName = toCamelCase(symbol.getAttribute('id')).replace(/_$/, '').replace(/-$/, '')
        iconsNames.push(iconName)
        // 获取viewBox和path,并生成对应的vue组件代码
        const viewBox = symbol.getAttribute('viewBox')
        const path = symbol.querySelectorAll('path')

        const pathArr = []
        path.forEach((p) => {
          const d = p.getAttribute('d')
          pathArr.push(`h('path', { d: '${d}' })`)
        })

        str += `const ${iconName} = defineComponent({ name: '${iconName}', render() { return h('svg', { viewBox: '${viewBox}' }, [${pathArr}]) }})\n\n`
      })

      str += `export {\n\t${iconsNames.join(',\n\t')}\n}`
      // 写入文件
      writeIconfontJsFile(str)
      modifyImportFile(iconsNames)
    }
  }
}

上面提到一个 importPath 的导入文件,这个文件将生成的图标组件集中导入,然后进行全局注册,内容如下:

/**
 * 自定义图标、iconfont图标、element-plus图标的注册文件
 *
 * 注意:
 * iconfont 图标名称由iconfont名称驼峰化,后面不加_。如 icon-name => IconName
 *
 * 示例:
 *
 * 引入自定义图标:
 * import { SunnyFill } from '@/assets/vue-icons'
 *
 * element-plus图标:
 * import { Search } from '@element-plus/icons-vue'
 */
/* eslint-disable */
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

import MonthFill from './components/MonthFill.vue'

// 下面这行注释不能删除,vite自定义插件会在这里插入内容
/* import iconfont start */
import {
	IconChehuisekuai,
} from '@/assets/vue-icons/iconfont.js'
/* import iconfont end */

export default {
  install(app) {
    // 注册element-plus图标
    for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
      app.component(key, component)
    }

    // 注册自定义图标
    app.component('MonthFill', MonthFill)

    // 下面这行注释不能删除,vite自定义插件会在这里插入内容
    /* register iconfont start */
	app.component('IconChehuisekuai', IconChehuisekuai)
	/* register iconfont end */
  }
}

export { MonthFill }

// 下面这行注释不能删除,vite自定义插件会在这里插入内容
/* export iconfont start */
export {
	IconChehuisekuai,
}
/* export iconfont end */

vite插件引入

// vite.config.js

// ....
import transformIconfontToComponent from './script/vite-plugin/transform-iconfont-to-component'

export default defineConfig(() => {
	plugins: [
      // ...其他插件
      // 自定义插件,将iconfont转换为vue组件,使其能够通过 el-icon 组件使用。
      // 重启后生效,每次重启后都会重新生成。
      transformIconfontToComponent({
        sourcePath: 'src/assets/iconfont/iconfont.js', // iconfont 原始文件,从图标库下载
        targetPath: 'src/assets/vue-icons/iconfont.js', // 经过插件转换后的结果文件
        importPath: 'src/assets/vue-icons/index.js' // 导入结果文件,与element-plus及自定义的图标一起,集中管理
      })
    ],
    // ...其他vite配置
})
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值