在做项目时发现,element-plus的图标组件,不能像文档示例中那样使用 iconfont 的图标。经过研究发现,element-plus的图标封装成了vue组件,组件内容是一个svg,然后以组件的方式引入和调用图标。根据这个思路,我写了一个脚本,将iconfont图标,转换成vue组件,这样就可以完美地统一项目中图标的使用方式了。
ElementPlus 图标的两种使用方式
先来回顾一下 element-plus 使用图标的两种方式
- 第一种是直接以组件调用的方式使用
<el-icon><Plus /></el-icon>
- 第二种是作为组件的props使用
<el-button type="danger" :icon="Delete" circle />
import { Delete } from '@element-plus/icons-vue'
读取 iconfont 图标内容
iconfont 能够生成svg图标,生成的内容在 iconfont.js
文件中
从图中可以看到,svg内容是一个字符串,只需要读取这个字符串,然后将每一个图标取出,就可以用来生成vue组件了。
核心代码:
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 */