likes
comments
collection
share

我把iconfont做成了vite插件(附有源码解析)

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

这里先说说我对iconfont使用过程中遇到的痛点

  1. 下载iconfont到本地文件夹步骤繁多
  2. 多人开发下很容易出现冲突
  3. 无法做到按需引入

然后在说说为啥对我来说是痛点

对于第一点: 下载iconfont所需要进行多步骤

  1. 需要打开网址找到对应项目
  2. 下载
  3. 解压到指定目录
  4. (可选)删除多余文件

对于第二点: 因为我所在公司的项目刚启动,且有10多个小伙伴在并行开发同一个前端仓库,所以造成合代码时经常遇到冲突,且大部分冲突基本都有iconfont的影子。 解决一次可能还好,但是解决一次又一次就会变得很痛苦。

对于第三点: iconfont作为使用最多的图标库,也广泛分布在我们的代码里面。它大大简化了我们对图标的使用,但是它也带来了一个问题,就是iconfont本身体积过大,甚至是比我们的代码还大。我们在首屏可能也就仅仅使用了几个图标,却要承担整个iconfont的大小。无法做到按需引入。

为了解决以上痛点,便有了做一个插件的想法

针对以上痛点,该插件需要满足以下功能:

  1. 自动下载iconfont
  2. iconfont不作为git提交的一部分,而是由我们自己来管理
  3. 能做到按需引入

对于自动下载来说,其实node脚本和vscode插件很容易做到,但是对于后面两个功能,就很难做到了。 思来想去,最终还是unocss给了我灵感。 那就是做成vite插件

  1. 自己写逻辑来处理iconfont下载,满足功能1
  2. 利用vite插件的虚拟文件功能来满足功能2,不导入iconfont实体文件,而是vite运行或编译时才动态的由逻辑生成。(具体可见官网插件 API | Vite (vitejs.net))
  3. 结合unplugin-icons/vite 将iconfont注册成不同svg组件,在通过unplugin-auto-import/vite来在使用的地方按需引入该组件。

大家对第三点可能有疑问,iconfont和svg有什么关系。当我去看了iconfont的接口后,发现有将全部字体图标返回成svg格式的接口。 当然如果大家不想使用svg的形式,而是正常的使用iconfont,那么也没问题,毕竟功能三也是有代价的

有了思路后,其实具体实现也只是时间问题了

下面贴一下主要代码,最后也会放上github链接

1.对于自动下载关键的逻辑如下

// 获取所有字体的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压缩包

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
    },

  }
}

按需模式

// 将下载的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)
  }
}

该插件最终的使用如下

npm i vite-plugin-unocss-iconfont

在vite.config.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文件中配置

// main.ts
import 'virtual:iconfont'

进阶使用,以svg的形式使用iconfont(完全按需,推荐)

// 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'],
        }),
      }
    ]
  })
]

在业务中使用

<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)

转载自:https://juejin.cn/post/7314851288084758528
评论
请登录