这里先说说我对iconfont使用过程中遇到的痛点
- 下载iconfont到本地文件夹步骤繁多
- 多人开发下很容易出现冲突
- 无法做到按需引入
然后在说说为啥对我来说是痛点
对于第一点: 下载iconfont所需要进行多步骤
- 需要打开网址找到对应项目
- 下载
- 解压到指定目录
- (可选)删除多余文件
对于第二点: 因为我所在公司的项目刚启动,且有10多个小伙伴在并行开发同一个前端仓库,所以造成合代码时经常遇到冲突,且大部分冲突基本都有iconfont的影子。 解决一次可能还好,但是解决一次又一次就会变得很痛苦。
对于第三点: iconfont作为使用最多的图标库,也广泛分布在我们的代码里面。它大大简化了我们对图标的使用,但是它也带来了一个问题,就是iconfont本身体积过大,甚至是比我们的代码还大。我们在首屏可能也就仅仅使用了几个图标,却要承担整个iconfont的大小。无法做到按需引入。
为了解决以上痛点,便有了做一个插件的想法
针对以上痛点,该插件需要满足以下功能:
- 自动下载iconfont
- iconfont不作为git提交的一部分,而是由我们自己来管理
- 能做到按需引入
对于自动下载来说,其实node脚本和vscode插件很容易做到,但是对于后面两个功能,就很难做到了。 思来想去,最终还是unocss给了我灵感。 那就是做成vite插件
- 自己写逻辑来处理iconfont下载,满足功能1
- 利用vite插件的虚拟文件 功能来满足功能2,不导入iconfont实体文件,而是vite运行或编译时才动态的由逻辑生成。(具体可见官网插件 API | Vite (vitejs.net))
- 结合unplugin-icons/vite 将iconfont注册成不同svg组件,在通过unplugin-auto-import/vite来在使用的地方按需引入该组件。
大家对第三点可能有疑问,iconfont和svg有什么关系。当我去看了iconfont的接口后,发现有将全部字体图标返回成svg格式的接口。 当然如果大家不想使用svg的形式,而是正常的使用iconfont,那么也没问题,毕竟功能三也是有代价的
有了思路后,其实具体实现也只是时间问题了
下面贴一下主要代码,最后也会放上github链接
1.对于自动下载关键的逻辑如下
ts
// 获取所有字体的svg格式
export const getIconJson = retryPromiseFunctionGenerator<IconfontJson['data'], VitePluginConfig>(async (config) => {
console.log('\x1B[32m%s\x1B[0m', '[vite-plugin-unocss-iconfont]:' + 'start get iconfont json')
const DOWNLOAD_URL = 'https://www.iconfont.cn/api/project/detail.json'
const data = await axios.get(DOWNLOAD_URL, {
headers: {
cookie: config.cookie,
},
timeout: 10 * 1000,
params: {
pid: config.pid,
ctoken: config.ctoken,
},
})
return data.data.data
})
// 获取iconfont字体
const fetchIonfontZip = retryPromiseFunctionGenerator<any, VitePluginConfig>((config) => {
console.log('\x1B[32m%s\x1B[0m', '[vite-plugin-unocss-iconfont]:' + 'start download iconfont zip')
const DOWNLOAD_URL = 'http://www.iconfont.cn/api/project/download.zip'
return axios.get(DOWNLOAD_URL, {
responseType: 'stream',
headers: {
cookie: config.cookie,
},
timeout: 15 * 1000,
params: {
pid: config.pid,
ctoken: config.ctoken,
},
})
})
// 下载过程处理
export async function getIconFiles(config: VitePluginConfig): Promise<any> {
try {
let res = null
try {
res = await fetchIonfontZip(config) // 下载iconfont
}
catch (error) {
console.error('[vite-plugin-unocss-iconfont]:%c请检查cookie, ctoken, pid是否正确:', 'color: red;')
console.error(error)
process.exit()
}
const tempPath = fse.mkdtempSync('temp-')
await zip.uncompress(res.data, tempPath) // 解压文件到指定目录并返回目录名
const zipPath = await listDir(tempPath)
const filePath = path.join(tempPath, zipPath[0])
const files = await Promise.all(['iconfont.woff', 'iconfont.woff2', 'iconfont.ttf'].map(async (item) => {
const data = await fs.readFile(path.join(filePath, item))
return [item, data]
}))
await fse.rm(tempPath, {
force: true,
maxRetries: 3,
retryDelay: 200,
recursive: true,
})
return files
}
catch (error) {
console.error('[vite-plugin-unocss-iconfont]:', error)
process.exit()
}
}
虚拟文件功能 开发模式下,虚拟文件使用的是iconfont的在线链接(下载压缩包耗时多1秒以上,所以为了开发方便,目前采用了这种方案) 生产模式下,虚拟文件使用的是下载到本地的iconfont压缩包
ts
mport { type Plugin } from 'vite'
import type { CustomIconLoader, VitePluginConfig } from './type'
import { getIconFiles, getIconJson } from './download'
import { generateCss } from './build'
let iconMap: Map<string, string>
const virtualModuleId = 'virtual:iconfont'
const resolvedVirtualCss = 'virtual-iconfont.css'
// 将下载的svg.json 提供给 unplugin-icons/vite 注册为svg组件
export function FileSystemIconLoader(transform?: (svg: string) => string): CustomIconLoader {
return (name) => {
return transform ? transform(iconMap.get(name)) : iconMap.get(name)
}
}
export default function (config: VitePluginConfig): Plugin {
const options: VitePluginConfig = Object.assign({
devmodel: 'link',
model: 'file',
fontFamily: 'iconfont',
}, config)
let viteConfig
let model = ''
let iconfontJson
let iconSrc = ''
return {
name: 'unocss-iconfont',
enforce: 'pre',
async configResolved(_viteConfig) {
model = _viteConfig.command === 'build' ? options.model : options.devmodel
viteConfig = _viteConfig
},
async buildStart() {
// 下载svg.json 用于按需用
iconfontJson = (await getIconJson(options))
iconfontJson.updateTime = new Date(iconfontJson.project.updated_at).getTime()
iconMap = new Map(iconfontJson.icons.map((icon) => {
return [icon.name, icon.show_svg]
}))
},
resolveId(id) {
// 虚拟文件用法
if (id === virtualModuleId) {
return resolvedVirtualCss
}
},
async load(id) {
// 虚拟文件处理
if (id === resolvedVirtualCss) {
if (model === 'file') { // 如果是压缩包,打包的时候就将解压后的文件发送到打包目录下
const iconFiles = await getIconFiles(options)
//
iconFiles.forEach(([name, code]) => {
this.emitFile({
type: 'asset',
fileName: name.replace('.', `-${iconfontJson.updateTime}.`), // 缓存处理
source: code,
})
})
}
else { // 如果用的远程连接,则虚拟文件就是使用远程连接的css文件
iconSrc = iconfontJson.font.css_font_face_src
}
const iconCss = await generateCss(options, iconfontJson, iconSrc, viteConfig.base ?? '/')
return iconCss
}
return null
},
}
}
按需模式
ts
// 将下载的svg.json 提供给 unplugin-icons/vite 注册为svg组件
export function FileSystemIconLoader(transform?: (svg: string) => string): CustomIconLoader {
return (name) => {
return transform ? transform(iconMap.get(name)) : iconMap.get(name)
}
}
该插件最终的使用如下
在vite.config.ts中配置
ts
// vite.config.ts
import iconfontLoader, { FileSystemIconLoader } from 'vite-plugin-unocss-iconfont'
//...
plugins: [
// 以下参数可从iconfont官网接口的请求头中获取,用来请求iconfont配置
iconfontLoader({
cookie: '',
pid: '',
ctoken: '',
fontFamily: 'iconfont' // iconfont 的 font-family, 可修改成其它的,防止和项目中其它iconfont冲突
})
]
//...
在项目入口ts文件中配置
ts
// main.ts
import 'virtual:iconfont'
进阶使用,以svg的形式使用iconfont(完全按需,推荐)
ts
// vite.config.ts
import Icons from 'unplugin-icons/vite'
import IconsResolver from 'unplugin-icons/resolver'
import Components from 'unplugin-vue-components/vite'
import AutoImport from 'unplugin-auto-import/vite'
import { FileSystemIconLoader } from 'vite-plugin-unocss-iconfont'
// ...
plugins: [
Icons({ // 注册为svg组件
customCollections: {
font: FileSystemIconLoader(svg => svg), // 如果不需要样式,可以自己通过正则将 svg里面的style替换
},
}),
AutoImport({ // 使用时自动导入
resolvers: [
{
IconsResolver({
prefix: 'Icon',
customCollections: ['font'],
}),
}
]
}),
Components({ // 提示
resolvers: [
{
IconsResolver({
prefix: 'Icon',
customCollections: ['font'],
}),
}
]
})
]
在业务中使用
vue
<template>
<icon-font-{你的iconfont图标名称}>
</template>
最后
实际上该插件还有很多的优化空间,比如
- iconfont官网接口挂掉的容错处理
- 插件启动时不需要阻塞vite,等字体下载完后再热更新通知页面就行
- 微前端生产模式下路径的处理
- 单仓库拥有多个iconfont情况的处理
如果大家感兴趣,欢迎提pr一起共建 也很高兴有人来使用该插件,遇到问题提出来~
笔者是以vite4版本开发的此插件,所以vite2和vite3场景下的兼容性还没有测试,请见谅
好了,废话不多说,贴上源码链接 plugin/packages/plugins/vite-plugin-unocss-iconfont at master · wangziweng7890/plugin (github.com)