基于iconify开发vite插件,打包本地文件夹中的svg图标离线使用,iconify动态渲染图标

文章介绍了如何基于iconify创建一个Vite插件,用于打包本地SVG图标和iconify的离线图标库。通过这个插件,可以实现动态渲染SVG图标,并在main.js中导入使用。配置主要在vite.config.ts中进行,包括设置自定义SVG图标目录、图标前缀以及iconify的图标集合。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

背景

在项目中使用svg图标是一个非常常见的需求,目前常用的方案有iconfont矢量图标库提供的方案,基于svg-sprite-loader插件处理的方案,但是这些方案会生成大量的dom节点,性能底。因此iconify会是更好的选择,想要基于iconify使用v-for的方式动态渲染图标必须要本地打包图标。

iconify 官网

入口

使用方式

iconify有多种使用方式,结合iconify提供的 Icon 组件可完成动态渲染功能。

  <!-- 使用方式 {前缀}-{图标集合}-{图标名} -->
  <i-ep-edit />
  <i-mdi-account-box style="font-size: 2em; color: red"/>
  <!-- 结合element plus -->
  <el-icon><i-ep-menu /></el-icon>
  <Icon icon="ep:menu" />

渲染动态图标

 <!-- custom:${item.icon} => custom 是本地图标的自定义前缀,在vite.config中配置 -->
   // import {Icon} from 'iconify/vue'
    <el-icon>
    	 <Icon :icon="`custom:${item.icon}`" width="28" height="28" />
    </el-icon>       

使用iconify打包离线图标库和本地的svg图标

效果在这里插入图片描述

在main.js中导入

import "virtual:customIcon";

在这里插入图片描述

vite.config.ts配置

import ViteIconifyBundle from './plugins/vite-iconify-bundle'
import path from 'node:path'

export default defineConfig({
	plugins: [
		ViteIconifyBundle({
      // 打包自定义svg图标
      svg: [
        {
          dir: path.resolve(__dirname, './src/assets/svg'),
          monotone: false,
          prefix: 'custom', // 自定义本地图标前缀
        },
      ],

      // 从 iconify 打包 icon 集合
       icons: [
         "mdi:home",
         "mdi:account",
         "mdi:login",
         "mdi:logout",
         "octicon:book-24",
         "octicon:code-square-24"
       ]

      // 自定义 JSON 文件
       json: [
         // Custom JSON file
         // 'json/gg.json',
         // Iconify JSON file (@iconify/json is a package name, /json/ is directory where files are, then filename)
         require.resolve('@iconify/json/json/tabler.json'),
         // Custom file with only few icons
         {
           filename: require.resolve('@iconify/json/json/line-md.json'),
           icons: ['home-twotone-alt', 'github', 'document-list', 'document-code', 'image-twotone'],
         },
       ],
    }),
	]
})

完整代码

import type {PluginOption} from "vite";
import {promises as fs} from "node:fs";
import {importDirectory, cleanupSVG, parseColors, isEmptyColor, runSVGO} from "@iconify/tools";
import {getIcons, stringToIcon, minifyIconSet} from "@iconify/utils";
import type {IconifyJSON, IconifyMetaData} from "@iconify/types";

interface BundleScriptCustomSVGConfig {
  // 存放SVG文件的目录
  dir: string;

  // 是否为多色图标
  monotone: boolean;

  // Icon 集合的前缀
  prefix: string;
}

interface BundleScriptCustomJSONConfig {
  // JSON文件的路径
  filename: string;

  // 要导入的图标名称数组,如果不写则全部导入
  icons?: string[];
}

interface BundleScriptConfig {
  // 自定义导入的svg图标集合
  svg?: BundleScriptCustomSVGConfig[];

  // 要从@iconify/json打包的图标集
  icons?: string[];

  // 要打包的JSON文件列表
  json?: (string | BundleScriptCustomJSONConfig)[];
}

const component = "@iconify/vue";

// Set to true to use require() instead of import
const commonJS = false;

let iconCount: number = 0;

export default function (sources: BundleScriptConfig): PluginOption {
  const virtualModuleId = "virtual:customIcon";
  const resolvedVirtualModuleId = "\0" + virtualModuleId;

  return {
    name: "vite-iconify-bundle", // 必须的,将会在 warning 和 error 中显示
    resolveId(id) {
      if (id === virtualModuleId) {
        return resolvedVirtualModuleId;
      }
    },
    async load(id) {
      if (id === resolvedVirtualModuleId) {
        const iconSet = await createBundleTask(sources);

        return iconSet;
      }
    }
  };
}

async function createBundleTask(sources: BundleScriptConfig) {
  let bundle = commonJS
    ? "const { addCollection } = require('" + component + "');\n\n"
    : "import { addCollection } from '" + component + "';\n\n";

  // 将sources.icons转换为sources.json
  if (sources.icons) {
    const sourcesJSON = sources.json ? sources.json : (sources.json = []);

    // Sort icons by prefix
    const orgainzedList = organizeIconsList(sources.icons);
    for (const prefix in orgainzedList) {
      const filename = require.resolve(`@iconify/json/json/${prefix}.json`);
      sourcesJSON.push({
        filename,
        icons: orgainzedList[prefix]
      });
    }
  }

  // 打包 JSON 文件
  if (sources.json) {
    for (let i = 0; i < sources.json.length; i++) {
      const item = sources.json[i];

      // 加载 icon集合
      const filename = typeof item === "string" ? item : item.filename;
      let content = JSON.parse(await fs.readFile(filename, "utf-8")) as IconifyJSON;

      // Filter icons
      if (typeof item !== "string" && item.icons?.length) {
        const filteredContent = getIcons(content, item.icons);
        if (!filteredContent) {
          throw new Error(`Cannot find required icons in ${filename}`);
        }
        content = filteredContent;
      }

      // 移除元数据并且添加到bundle
      removeMetaData(content);
      minifyIconSet(content);

      bundle += "addCollection(" + JSON.stringify(content) + ");\n";

      console.log(`Bundled icons from ${filename}`);
    }
  }

  // 自定义 SVG 文件
  if (sources.svg) {
    for (let i = 0; i < sources.svg.length; i++) {
      const source = sources.svg[i];
      // 导入图标
      const iconSet = await importDirectory(source.dir, {
        prefix: source.prefix
      });

      // 验证,清理,修复颜色并优化
      await iconSet.forEach(async (name, type) => {
        if (type !== "icon") {
          return;
        }
        // 获取SVG实例以进行分析
        const svg = iconSet.toSVG(name);
        if (!svg) {
          // 无效的图标
          iconSet.remove(name);
          return;
        }

        // 清除并且优化图标
        try {
          // Clean up icon code

          await cleanupSVG(svg);

          if (source.monotone) {
            // Replace color with currentColor, add if missing
            // If icon is not monotone, remove this code
            await parseColors(svg, {
              defaultColor: "currentColor",
              callback: (attr, colorStr, color) => {
                return !color || isEmptyColor(color) ? colorStr : "currentColor";
              }
            });
          }

          // Optimise
          await runSVGO(svg);
        } catch (err) {
          // Invalid icon
          console.error(`Error parsing ${name} from ${source.dir}:`, err);
          iconSet.remove(name);
          return;
        }
        iconCount = iconSet.count();
        // Update icon from SVG instance
        iconSet.fromSVG(name, svg);
      });
      // Export to JSON
      const content = iconSet.export();
      bundle += "addCollection(" + JSON.stringify(content) + ");\n";
    }
  }

  console.log(`\n图标打包完成! 总共打包了${iconCount}个图标,大小:(${bundle.length} bytes)\n`);
  // Save to file
  return bundle;
}

/**
 * Sort icon names by prefix
 * @param icons
 * @returns icons
 */
function organizeIconsList(icons: string[]) {
  const sorted: Record<string, string[]> = Object.create(null);
  icons.forEach(icon => {
    const item = stringToIcon(icon);
    if (!item) {
      return;
    }

    const prefix = item.prefix;
    const prefixList = sorted[prefix] ? sorted[prefix] : (sorted[prefix] = []);

    const name = item.name;
    if (prefixList.indexOf(name) === -1) {
      prefixList.push(name);
    }
  });

  return sorted;
}

// 从icon集合移除元数据
function removeMetaData(iconSet: IconifyJSON) {
  const props: (keyof IconifyMetaData)[] = ["info", "chars", "categories", "themes", "prefixes", "suffixes"];
  props.forEach(prop => {
    delete iconSet[prop];
  });
}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值