从 Web 图标的历史探索图标库的开发方案与实现

1,251 阅读16分钟

自浏览器诞生之日起,人们可以通过 Web 网页展示文字、图片、视频等信息。此外,为了能给用户提供一个更好的观感效果,页面上的图标则起到了一个关键的作用。而作为前端开发人员,页面上的图标开发则是一个重点,本文将从 Web 图标的发展史来探究前端图标开发的方式。

一、Web 图标的发展史

独立图片

1990 年 HTML 被正式发明,但此时 CSS 还没出现,在这段时间里,Web 图标只能使用 <img> 标签来引入图标的图片文件,然后通过 widthheight 来设置图标的大小。随着 1996 年 12 月 W3C 将 CSS 纳入标准,使得页面上的图片可以使用 CSS 的 background-image 来设置,开发者也更多的倾向于使用 CSS 来做页面的图标。

<img src="search.png" alt="搜索" width="24" height="24" />

.icon {
  width: 24px;
  height: 24px;
  background-image: url(search.png);
}

缺点:

  1. 增加 HTTP 请求,占用并行加载数量,导致页面加载缓慢。
  2. 图标的颜色都在图片文件中,无法由开发者动态设置颜色。
  3. 难以维护,多个状态的图标都是单独文件,会增加图片文件数量。
  4. 在 hover 鼠标悬停切换图标时,会出现首次加载图片的等待状态,体验极差。

CSS Sprite

为了减轻 HTTP 请求压力,图标开始使用一种图片合成技术,也就是常见的雪碧图,雪碧图的原理就是将多个图片通过一些工具合并成一张大图,使用 CSS 的 background-image 引入大图,background-position 进行背景定位来显示对应位置的图标,以下是雪碧图示例和 CSS 代码示例。

9838B665-1F26-4943-9C7A-B50215ED2285.png

相比与独立图片而言,雪碧图将多个图标文件合并成一个图片文件,虽然将 HTTP 请求压缩成一次,但这种方式但缺点依旧不少:

  1. 雪碧图的大小会随着图标的数量递增,同时无法进行按需加载。
  2. 页面打开后,需要加载完整个雪碧图才能显示图标,会出现较长的加载时间。
  3. 颜色都在图片中,依旧无法由开发者动态设置。
  4. 图标的位置一旦在雪碧图中发生改变,background-position 需要重新设置。
  5. 维护成本高,设计师将图标提供给开发者,开发者需要将所有图标重新生成雪碧图。

Font Icon

随着 CSS 开始支持 @font-face 引入字体文件,Web Icon 开始进入新的时代,开发者可以将图标都制作成字体文件(.woff、.ttf、.eot 等)来引入到页面中,通过图标的 Unicode 字符在页面上显示图标,这些图标也被成为字体图标。

通过这种方式显示的图标从视觉上看的是图片,但本质上其实上字体。

Font Icon 原理:占用 Unicode 字符编码区,一般占用私人使用区,私人使用区共分为三个区域:U+E000 ~ U+F8FFU+F0000 ~ U+FFFFDU+100000 ~ U+10FFFD

Font Icon 的 CSS 代码示例:

@font-face {
  font-family: "plv-icon-family";
  src: url("my-font-icon.woff") format("woff");
}
.icon {
  font-family: "plv-icon-family" !important;
  font-size: 16px;
}

.icon-search:before {
  content: "\e675";
}

在 HTML 中使用 Font Icon:

<!-- class 方式 -->
<i class="icon icon-search"></i>

<!-- unicode 方式 -->
<i class="icon">&#xe675;</i>

Unicode 字符结构:

  • &#xe675: &# 表示字符实体,x 表示十六进制,e675 表示该字符实体的 Unicode 编码。
  • \e675: CSS 的 content 属性本身就支持 Unicode 字符,用反斜杠 \ 作为开头来表示,因此 \ 等价于 &#x

字体图标之所以能成为当今最为流行的图标方案,正是因为它比前两种方案有着不少的优点:

  1. 灵活性:图标本质上是文字,可以通过 CSS 随意更改颜色、添加字体阴影等效果。
  2. 兼容性:@font-face 是 CSS 的标准,几乎兼容所有的浏览器。
  3. 渲染强:由于页面上的图标本质是字体,可以通过 font-size 设置大小,在页面放大后不会失真。

Font Icon 解决了独立图片与雪碧图的较多缺点,但由于不同浏览器厂商对字体文件对支持程度不同,开发者需要根据不同对浏览器生成多种字体文件进行兼容,而且图标内容都在同一个字体文件中,也无法做到图标的按需加载。

虽然现今有较多的免费图标库提供给开发者使用(如 FontAwesome、IcoMoon),但随着项目的图标定制需求,开发者需要工程化生成字体文件,但编排 Unicode 编码生成字体文件可不是个简单的事情,此时图标管理平台开始兴起,如阿里妈妈 MUX 推出的 iconfont.cn,该平台也是如今国内最为流行的平台之一。

列举 3 个典型的图标管理平台:

  • Iconfont: 阿里妈妈 MUX 推出,提供图标、矢量插画、3D 插画的管理功能,可在任意框架中使用,也是如今最流行的平台。
  • IconPark: 由字节跳动推出的平台,提供字节跳所有产品的图标,同时提供给 Vue2、Vue3、React 的动态集成功能。
  • YIcon: 由去哪儿网移动架构团队打造的新型 iconfont 平台,提供图标上传、展示、可内网部署的字体图标管理平台。

SVG Icon

虽然字体图标能让开发者通过设置字体颜色来自定义图标的颜色,但设计师可能要求页面上显示双色甚至具有多种颜色的图标,而字体图标在这方面上的表现就比较差了,此时开发者逐渐偏向于使用 SVG 来开发 Web Icon。

SVG 是一种图形文件格式,它的英文全称为 Scalable Vector Graphics,意思为可缩放的矢量图形。它是一种用 XML 来定义的语言,由 W3C 联盟进行开发的,可直接插入到 HTML 中通过浏览器进行观看。SVG 提供了目前PNG 和 JPEG 格式无法具备的优势:

  1. 可以任意放大图形显示,但绝不会以牺牲图像质量为代价。
  2. 可在 SVG 图像中保留可编辑和可搜寻的状态。
  3. SVG 文件比 JPEG 和 PNG 格式的文件要小很多,因而下载也很快。
  4. 开发者可以在 SVG 文件中嵌入动画元素或通过 JavaScript 脚本来定义动画

直到如今,SVG 图标已逐渐撼动了字体图标的地位,相比于字体图标而言,SVG 图标带来了如下的优势:

  1. 真 · 多色图标,开发者可以通过设置 SVG 的 strokefill 颜色属性做到真正意义上的多色图标。同时由于 JavaScript 可以直接选取 SVG 的子节点,因此可以通过 JavaScript 向 SVG 图标添加各种动画。

  2. 真 · 按需加载,随着 Vue、React 框架的使用,我们可以将 SVG 进行组件化,在框架打包时可达到按需加载的效果。

  3. 真 · 渲染效果,由于字体图标本质上属于文本,在浏览器的文本抗锯齿算法影响下,字体图标在某些特定情况下可能会出现失真的情况,而 SVG 是以图片的方式进行渲染,不受该算法的影响,因此渲染效果要比字体图标要更好。

二、图标库开发

由于开发字体图标需要编排 Unicode 编码,SVG 图标也逐渐走向趋势,该节将讲述 SVG 图标库的开发过程。而本次图标库的开发将通过 Nodejs 脚本,把 SVG 文件转换成 Vue 组件,而图标组件则是采用 Vue3.x + TSX 进行开发,整体源码可见文章底部的 Github 链接。

虽然本次教程所生成的组件是 Vue3.x + TSX,但只要了解图标库生成的原理即可生成 React、原生 JavaScript 的图标组件。

图标组件设计

上文提到字节跳动推出的 IconPark 图标管理平台不仅提供 Vue2、Vue3、React 的动态集成,其内部提供的图标组件甚至支持四种颜色的设定,因此本次图标组件的设计参考 IconPark 的图标组件。

首先从图标组件的使用方式上看,图标组件参数提供图标的线宽、端点、拐角、图标类型与颜色的设置。

图标类型分为:线性 (outline)、填充 (fill)、双色 (two-tone)、四色 (multi-color)。

颜色参数的类型则是: string | [string, string] | [string, string, string, string],四个颜色的元组对应的四个颜色含义为:外部描边色 (outStroke)、外部填充色 (outFill)、内部描边色 (innerStroke)、内部填充色 (innerFill)。

四种图标类型搭配颜色参数的关系图与效果图可见下表:

WX20220429-014521@2x.png

通过脚本生成的图标组件代码如下:

import { IconBuilder } from '../../icon-builder';

export default IconBuilder('camera', (data) => (
  <svg width={data.size} height={data.size} class={data.className} viewBox="0 0 48 48">
    <path
      d="m15 12 3-6h12l3 6H15Z"
      fill={data.colorDesc[1]}
      stroke={data.colorDesc[0]}
      stroke-linejoin={data.strokeLineJoin}
      stroke-width={data.strokeWidth}
    />
    <rect
      width="40" height="30" x="4" y="12" rx="3"
      fill={data.colorDesc[1]}
      stroke={data.colorDesc[0]}
      stroke-linejoin={data.strokeLineJoin}
      stroke-width={data.strokeWidth}
    />
    <path
      d="M24 35a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z"
      fill={data.colorDesc[3]}
      stroke={data.colorDesc[2]}
      stroke-linejoin={data.strokeLineJoin}
      stroke-width={data.strokeWidth}
    />
  </svg>
));

从图标组件的代码上看,脚本的处理则是将 SVG 文件上可配置的属性,从固定值转成 TSX 的语法,然后在外面包裹一层 JavaScript 的方法体。

相关的 SVG 属性:轮廓色 (stroke)、填充色 (fill)、线宽 (stroke-width)、拐角类型 (stroke-linejoin)、端点类型 (stroke-linecap)

接下来将正式进入图标库的开发!

涉及的 NPM 库

依赖包说明及用处npm 地址
fast-glob使用 shell 方式匹配文件,用于匹配 svg 文件。点击打开
fs-extraNode.js 中 fs 模块的扩展,用于读写文件。点击打开
camelcase字符驼峰转换插件,如 ab-cd -> AbCd。点击打开
prettier代码格式插件,用于格式化代码字符串。点击打开
tinycolor2颜色工具插件,用于匹配两个颜色是否一致。点击打开
svgo基于 Node.js 的 SVG 压缩工具,转换脚本的核心工具。点击打开

第一步:获取转换的 SVG 文件地址

利用 fast-glob 匹配 *.svg 文件后缀名,获取所有的 SVG 文件的地址,返回 string[]

import glob from 'fast-glob';

const SvgFilePath = '存放 SVG 文件的目录地址';

/**
 * 获取所有 svg 文件地址
 */
const getSvgFiles = (): string[] => {
  return glob.sync('*.svg', { cwd: SvgFilePath, absolute: true });
};

第二步:编译 SVG 生成 TSX 组件

通过 fast-glob 得到的 SVG 文件地址后,遍历并读取 SVG 文件内容,将文件内容加工成 TSX 代码字符串,整体流程可见下图:

图片2.png

import { readFileSync } from 'fs-extra';

const compileSvgs = (files: string[]) => {
  files.forEach((file) => svgToTsx(file));
};

/**
 * SVG 转 TSX
 * @param file svg 文件地址
 */
const svgToTsx = (file: string) => {
  // 读取文件内容
  const content = readFileSync(file, 'utf-8');
  // 通过文件地址获取所需的名称格式
  ......
  // svgo 处理 SVG
  ......
  // 生成 TSX 代码字符
  ......
  // 格式化 TSX 代码字符
  ......
  // 输出 .tsx 代码文件
  ......
};

通过文件地址获取所需的名称格式

通过 camelcasepath 依赖,从文件地址中获取后续所需的格式组件名。

import camelcase from 'camelcase';
import path from 'path';

/**
 * 获取 svg 名称
 * @param file svg 文件地址
 */
const getSvgName = (file: string) => {
  const fileName = path.basename(file).replace('.svg', '');

  return {
    /** 横线格式, foo-bar */
    lineName: fileName,
    /** 小驼峰格式, fooBar */
    lowerName: camelcase(fileName, { locale: 'en-US' }),
    /** 大驼峰格式, FooBar */
    upperName: camelcase(fileName, { pascalCase: true }),
  };
};

通过 svgo 处理 SVG 文本

svgo 是 Node.js 中常用的 SVG 压缩优化工具,其内部提供了较多的功能有助于开发者处理 SVG 的内容,其用法也较为简单:

import { optimize, OptimizeOptions } from 'svgo';

const SvgoOptions: OptimizeOptions = { [svgo 的配置] };

/**
 * svgo 处理 SVG 文本
 * @param content SVG 文本
 */
const formatSvg = (content: string): string => {
  const result = optimize(content, SvgoOptions);

  if (result.data) {
    return result.data;
  }

  return content;
};

而这次的转换脚本所用到的 svgo 插件也较少,除了内部提供的插件外,还可以通过自定义插件来按需处理 SVG,详细的插件可见 svgo 官方文档

import { OptimizeOptions } from 'svgo';

const SvgoOptions: OptimizeOptions = {
  // 浮点数精度取 2 位
  floatPrecision: 2,
  plugins: [
    // 删除 XML 处理指令
    'removeXMLProcInst',
    // 删除 svg 标签的 xmlns 属性
    'removeXMLNS',
    // 删除无用的 stroke 和 fill 属性
    'removeUselessStrokeAndFill',
    // 排序属性
    'sortAttrs',
    // 添加 svg 节点属性配置
    {
      name: 'addAttributesToSVGElement',
      params: {
        attribute: {
          // className 占位属性
          class: '_className',
          // width 尺寸占位属性
          width: '_svgSize',
          // height 尺寸占位属性
          height: '_svgSize',
        }
      }
    },

    // 自定义插件 - 处理颜色属性
    covertColorAttrsPlugin,
    // 自定义插件 - 处理其他属性配置
    covertOtherAttrsPlugin,
  ]
};

自定义插件原理:将 SVG 节点解析成对象结构,并通过对象的引用关系修改相应的属性值。

// SVG 节点
<path
  stroke-linejoin="round"
  stroke-width="4"
  stroke="#333"
  fill="#2F88FF"
  d="m15 12 3-6h12l3 6H15Z"
/>

// svgo 解析后的对象结构
{
  type: 'element',
  name: 'path',
  attributes: {
    'stroke-linejoin': 'round',
    'stroke-width': '4',
    'stroke': '#333',
    'fill': '#2F88FF',
    'd': 'm15 12 3-6h12l3 6H15Z'
  }
}

图标组件有四种颜色,为了能让脚本识别出哪个颜色值属于哪个颜色类型,这里需要设计师与开发者约定好 SVG 文件颜色规范,当图标需要具备四色图标的特点时,设计师需要在 SVG 上标记好相应的四种颜色:

未命名表单-第 2 页 (4).png

如果图标只是一个普通的单色图标,则可以提供只有外部描边色的 SVG 文件。

通过自定义插件替换 SVG 的颜色值,这里用到 tinycolor2 判断颜色值是否一致:

import { XastElement } from 'svgo';
import tinycolor from 'tinycolor2';

/** 颜色名称元组 */
const colorNames = ['_outStrokeColor', '_outFillColor', '_innerStrokeColor', '_innerStrokeColor'];

/** 颜色转换关系 */
const colorRelations = {
  '#333': 0,
  '#2F88FF': 1,
  '#FFF': 2,
  '#43CCF8': 3,
};

/** 转换的 svg 属性名 */
const colorsProps = [
  'color',
  'fill',
  'stroke',
  'stop-color',
  'flood-color',
  'lighting-color',
];

/** 处理颜色属性自定义插件 */
const covertColorAttrsPlugin = {
  name: 'covertColorAttrs',
  type: 'perItem',
  fn: (node: XastElement) => {
    // 遍历节点的属性
    for (const [name, value] of Object.entries(node.attributes)) {
      // 处理颜色 key 值
      if (colorsProps.includes(name)) {
        for (const [color, num] of Object.entries(colorRelations)) {
          // 颜色一致
          if (tinycolor.equals(value, color)) {
            node.attributes[name] = colorNames[num];
          }
        }
      }
    }
  },
};

同样的原理处理 stroke-widthstroke-linecapstroke-linejoin 三个属性:

/** 其他属性的转换关系 */
const otherAttrRelation = {
  'stroke-width': '_strokeWidth',
  'stroke-linecap': '_strokeLinecap',
  'stroke-linejoin': '_strokeLinejoin',
};

/** 处理其他属性自定义插件 */
const covertOtherAttrsPlugin = {
  name: 'covertOtherAttrs',
  type: 'perItem',
  fn: (node: XastElement) => {
    const otherKeys = Object.keys(otherAttrRelation);
    // 遍历节点的属性
    for (const [name, value] of Object.entries(node.attributes)) {
      // 处理颜色 key 值
      if (otherKeys.includes(name)) {
        node.attributes[name] = otherAttrRelation[name];
      }
    }
  },
};

最终 SVG 文本通过 svgo 处理后,得到的是将固定值转成了开头带有下划线 _ 的可标识内容,后续可通过正则字符串替换成相应的代码语法,如下图:

5C45989C-4950-4A2B-A104-D00505488E07.png

生成 TSX 代码字符串

原理较为简单,通过字符串模板,在外面包裹一层方法体,然后通过正则表达式替换标识内容:

/**
 * 创建 vue next 代码模板
 * @param svgContent 处理后的 svg 内容
 * @param lineName svg图标横线格式名称
 */
const createVueNextCodeTemplate = (svgContent: string, lineName: string): string => {
  // 生成代码
  const temp = `
    import { IconBuilder } from '../../icon-builder';

    export default IconBuilder('${lineName}', (data) => (
      ${svgContent}
    ));  
  `;

  return replaceCodeAttributes(temp);
};

/**
 * 替换可配置属性内容
 * @param code 代码文本内容
 */
const replaceCodeAttributes = (code: string): string => {
  return code
    .replace(/"_svgSize"/g, '{data.size}')
    .replace(/"_className"/g, '{data.className}')
    .replace(/"_outStrokeColor"/g, '{data.colorDesc[0]}')
    .replace(/"_outFillColor"/g, '{data.colorDesc[1]}')
    .replace(/"_innerStrokeColor"/g, '{data.colorDesc[2]}')
    .replace(/"_innerFillColor"/g, '{data.colorDesc[3]}')
    .replace(/"_strokeWidth"/g, '{data.strokeWidth}')
    .replace(/"_strokeLinecap"/g, '{data.strokeLineCap}')
    .replace(/"_strokeLinejoin"/g, '{data.strokeLineJoin}');
};

通过 replaceCodeAttributes 替换后所得到的字符串内容已经是 TSX 语法下的代码文本,如下图:

7828CE11-68FE-4AEA-8FEF-993B4B494465.png

格式化代码

通过前面的处理得到的代码字符串文本的格式比较混乱,如果直接输出的话 eslint 会报错,这里使用 prettier 进行代码格式化:

import { BuiltInParserName, format } from 'prettier';

/**
 * 格式化代码
 * @param code 代码文本
 * @param parser 代码类型
 */
const formatCode = (code: string, parser: BuiltInParserName = 'typescript') => {
  return format(code, {
    parser,
    singleQuote: true,
  });
};

输出 TSX 文件

通过 prettier 处理后,得到了格式整齐的代码字符串,剩下的就是通过 fs-extra 输出 .tsx 文件。

import { writeFileSync } from 'fs-extra';
import path from 'path';

const OutputDirPath = '输出的文件夹目录地址';

/**
 * 输出代码
 * @param code 输出的代码
 * @param lineName 横线格式名
 */
const outputCode = (code: string, lineName: string) => {
  const file = path.join(OutputDirPath, lineName, 'index.tsx');
  writeFileSync(file, code, 'utf-8');
};

到此整个图标库的脚本就完成了开发,接下来将处理 IconBuilder 的开发。

第三步:IconBuilder 开发

IconBuilder 方法属于图标库的方法,该方法返回 Vue3 的 DefineComponent 组件类型,其 typescript 的类型定义如下:

import { DefineComponent } from 'vue';

/** icon builder 数据 */
type IconSvgBuilderData = {
  className: string;
  size: number;
  colorDesc: [string, string, string, string];
  strokeWidth: number;
  strokeLineCap: StrokeLineCap;
  strokeLineJoin: StrokeLineJoin;
};

/** icon render 类型 */
type IconRenderFn = (data: IconSvgBuilderData) => JSX.Element;

/**
 * icon builder 类型
 * @param name 图标名
 * @param render 获取图标 jsx 节点方法
 */
type IconBuilderFn = (name: string, render: IconRenderFn) => DefineComponent<IconCommonProps>;

图标组件对外提供的 typescript 参数类型:

/** 图标类型 */
export type IconType = 'outline' | 'filled' | 'two-tone' | 'multi-color';

/** 图标线帽类型 */
export type StrokeLineCap = 'round' | 'butt' | 'square';

/** 图标拐角类型 */
export type StrokeLineJoin = 'round' | 'bevel' | 'miter';

/** 图标颜色类型 */
export type IconColor = string | [string, string] | [string, string, string, string];

/** 图标公用类型 */
export type IconCommonProps = {
  size?: number,
  type?: IconType,
  color?: IconColor,
  strokeWidth?: number,
  strokeLineCap?: StrokeLineCap,
  strokeLineJoin?: StrokeLineJoin,
};

IconBuilder 方法实现:

import { ComponentOptions, computed, DefineComponent, unref } from 'vue';
import { getColorDescription, IconCommonProps, IconSvgRenderData, IconType, StrokeLineCap, StrokeLineJoin } from './types.ts';

/** 默认图标配置 */
export const defaultIconConfig = {
  size: 24,
  type: 'outline' as IconType,
  strokeWidth: 4,
  strokeLineCap: 'round' as StrokeLineCap,
  strokeLineJoin: 'round' as StrokeLineJoin,
};
 
export const IconBuilder = (
  name: string,
  render: (data: IconSvgRenderData) => JSX.Element
) => {
  const iconSvgOptions: ComponentOptions<IconCommonProps> = {
    name: `icon-${name}`,
    props: ['size', 'type', 'color', 'strokeWidth', 'strokeLineCap', 'strokeLineJoin'],

    setup(props) {
      const data = computed<IconSvgRenderData>(() => {
        const className = `icon-${name}`;
        const iconType = props.type || defaultIconConfig.type;
        const size = props.size || defaultIconConfig.size;
        const strokeWidth = props.strokeWidth || defaultIconConfig.strokeWidth;
        const strokeLineCap = props.strokeLineCap || defaultIconConfig.strokeLineCap;
        const strokeLineJoin = props.strokeLineJoin || defaultIconConfig.strokeLineJoin;
        // 处理图标类型与图标颜色参数
        const colorDesc = getColorDescription(props.color, iconType);

        return {
          className,
          size,
          colorDesc,
          strokeWidth,
          strokeLineCap,
          strokeLineJoin,
        };
      });

      return () => {
        return render(unref(data));
      };
    }
  };

  const IconSvg = iconSvgOptions as DefineComponent<IconCommonProps>;

  return IconSvg;
};

getColorDescription 用于处理图标类型与图标颜色参数,处理后得到一个具有四个值的数组:

/** 颜色描述元组 */
export type ColorDescription = [outStroke: string, outFill: string, innerStroke: string, innerFill: string];

/** currentColor 字符串 */
export const currentColor = 'currentColor';

/** 默认图标颜色配置 */
export const defaultIconColorConfig = {
  twoTone: {
    stroke: '#333',
    fill: '#2F88FF',
  },
  multiColor: {
    outStroke: '#333',
    outFill: '#2F88FF',
    innerStroke: '#FFF',
    innerFill: '#43CCF8',
  },
};

/**
 * 获取图标颜色描述
 * @param colorProp 颜色配置
 * @param iconType 图标类型
 */
export const getColorDescription = (colorProp: IconColor | undefined, iconType: IconType) => {
  const color: (string | undefined)[] = typeof colorProp === 'string' ? [colorProp] : colorProp || [];
  const colorDesc: ColorDescription = [currentColor, currentColor, currentColor, currentColor];

  switch (iconType) {
    case 'outline': {
      colorDesc[0] = typeof color[0] === 'string' ? color[0] : currentColor;
      colorDesc[1] = 'none';
      colorDesc[2] = typeof color[0] === 'string' ? color[0] : currentColor;
      colorDesc[3] = 'none';
      break;
    }
    case 'filled': {
      colorDesc[0] = typeof color[0] === 'string' ? color[0] : currentColor;
      colorDesc[1] = typeof color[0] === 'string' ? color[0] : currentColor;
      colorDesc[2] = '#FFF';
      colorDesc[3] = '#FFF';
      break;
    }
    case 'two-tone': {
      const { stroke, fill } = defaultIconColorConfig.twoTone;
      colorDesc[0] = typeof color[0] === 'string' ? color[0] : stroke;
      colorDesc[1] = typeof color[1] === 'string' ? color[1] : fill;
      colorDesc[2] = typeof color[0] === 'string' ? color[0] : stroke;
      colorDesc[3] = typeof color[1] === 'string' ? color[1] : fill;
      break;
    }
    case 'multi-color': {
      const { outStroke, outFill, innerStroke, innerFill } = defaultIconColorConfig.multiColor;
      colorDesc[0] = typeof color[0] === 'string' ? color[0] : outStroke;
      colorDesc[1] = typeof color[1] === 'string' ? color[1] : outFill;
      colorDesc[2] = typeof color[2] === 'string' ? color[2] : innerStroke;
      colorDesc[3] = typeof color[3] === 'string' ? color[3] : innerFill;
      break;
    }
  }
  return colorDesc;
};

最终通过 Webpack、Vite 等构建工具构建后,我们就可以通过 <icon-camera type="multi-color" :color="['#333', '#2F88FF', '#FFF', '#43CCF8']" /> 的方式在 Vue3 的项目中使用图标了。

总结

到此为止,我们从 Web 图标的发展历史上看,字体图标因为 iconfont.cn 带来的便捷目前仍然是主流的图标方案,但 SVG 图标组件凭着支持多色图标、可按需加载等优点正逐步取代字体图标,目前主流的组件库如 element-plus、ant-design-vue 都使用 SVG Component 来开发自带的图标库,可见 SVG Icon 已逐渐成为主流。

而本文章讲述的图标库开发分为脚本开发与组件开发两部分

  • 脚本开发的原理是对 SVG 的字符串处理,使用 svgo 加工将固定值转成可标识值,再通过正则字符串替换成对应语法的字符,通过 prettier 格式代码后输出到文件中,其中在生成 TSX 的代码过程中只要稍微改动下就可以生成 React 等框架的组件代码。
  • 图标开发则是通过 IconBuilder 方法返回对外提供的 Vue3 组件配置。

图标库源码 Github 链接:github.com/Junyu8893/z…

参考文章: