likes
comments
collection
share

揭秘buildAdmin开源项目引入的四种图标方式!

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

在项目开发中,我们经常使用可能都是UI组件库里的图标,当然由于业务需要,可能当前图标库没有我们需要的图标这时候就需要引入其它图标库的图标,比如iconfontFontAweSome本地图标库。在了解引入这些图标库之前,我们先学习一下各种图标库的引入使用:

  • Element-Plus:由于elemen官方已经把图标封装成了组件,所以当我们引入图标的时候,需要全局声明组件。

    import * as Icons from '@element-plus/icons';
    const app = createApp(App);
    // 全局注册图标,牺牲一点性能
    for (let i in Icons) {
    // 官方图标名称首字母都是大写,所以转为小写,并命名组件未el-icon-图标名
    app.component(`el-icon-${toLine(i)}`, (Icons as any)[i]);
    }
    
    // 组件中使用图标
    <el-icon-user />
    
  • Iconfont:阿里巴巴图标库,通过创建一个项目目录,然后把我们需要的图标添加进去,然后在项目中引入图标目录的cdn(三种方式之一:css代码、css链接、js链接)就可以使用了,如果没有特殊处理一般是在项目的index.html中引入相关链接,css代码可以在根目录的样式文件中引入。然后就可以在项目中使用:

    <i class="iconfont icon-user"></i>
    
  • FontAwesome:一个比较好用的字体图标库,可以直接通过cdn引入,也可以通过安装package包引入,然后就可以使用了:

    英文官网:fontawesome.com/search

    中文网:fontawesome.com.cn/

    <i class="fa fa-user"></i>
    
  • 引用本地图标:一般使用svg格式图标,因为svg性能好,相对于其它格式,它体积更小,可以任意放大图形显示,不以牺牲图标质量为代价,项目中是不能直接加载svg格式,需要额外插件实现(后面会详细介绍)。

    <svg class="svg-icon icon" style="width: 1em;height: 1em;color: black;">
    <use href="#local-vue" />
    </svg>
    

为了方便维护以及扩展,我们可以把四种图标封装为统一组件使用,在封装前我们需要明确三点:

  • 获取所有图标。
  • 实现图标可复制,复制就可用的原则。
  • 图标使用统一组件。

在学习各类的图标库之前我们先了解一下如何封装一下图标共用组件,它向外暴露的名称是Icon:

实现组件健壮性、易维护:支持图标名称(name)、图标颜色(color)、图标大小(size)三要素的自定义。四种图标格式引入为element-plus(el-icon-iconName)、iconfont(iconfont iconName)、fontawesome(fa fa-iconName)、本地图标(local-iconName),iconName是图标名称。

props: {
name: {
  type: String,
  required: true,
},
size: {
  type: String,
  default: '30px',
},
color: {
  type: String,
  default: '#00000',
},
},

// 处理样式,去掉多余px命名
const iconStyle = computed((): CSSProperties => {
  const { size, color } = props;
  let s = `${size.replace('px', '')}px`;
  return {
    fontSize: s,
    color: color,
  };
});

兼容上面四种图标实现:通过Vue3的setup的返回值中使用渲染函数实现,不需要在template中定义标签使用,分三种情况。

createVNode函数:创建虚拟节点,从左到右有三个参数:html标签名称或组件(String)、标签属性(Object)、嵌套标签定义(Array)。

  • 对于element-plus图标的渲染:官方是通过el-icon标签内直接使用图标组件,所以创建虚拟节点标签就是el-icon,由于图标组件是嵌套的,所以需要用到第三参数。

    setup(props) {
        // 当前引入的是element-plus图标
        if (props.name.indexOf('el-icon-') === 0) {
          return () =>
            createVNode(
              'el-icon',
              { class: 'icon el-icon', style: iconStyle.value },
              [createVNode(resolveComponent(props.name))]
            );
        }
     }
    
  • 对于iconfont、fontawesome图标的渲染:由于使用这两种的图标的标签都是i,它们唯一不同就是图标名称的命名所以可以共用同一个渲染函数。

    setup(props){
     // 当前引入的是iconfont或fontawesome图标
     else{
     return () =>
        createVNode('i', {
          class: [props.name, 'icon'],
          style: iconStyle.value,
        });
      }
    }
    
  • 对于本地svg图标的渲染:直接引入本地封装的svg组件,把这个组件当作渲染标签,这里图标命名以local-iconName格式引入的。

    setup(props){
     // 当前引入的是本地svg图标
     if (props.name.indexOf('local-') === 0 || isExternal(props.name)) {
      return () =>
        createVNode(svg, {
          name: props.name,
          size: props.size,
          color: props.color,
        });
      }
    }
    

最终就可以通过这样使用图标:

<Icon name="" color="" size=""/>

其实上面就已经实现了四种图标的类型统一封装,一致使用。接下来为了更方便获取图标,我们把所有图标封装起来就可以直接cv使用了。

点击实现cv方式:通过点击图标传入我们想要复制的内容,一般都是整个组件的字符串。

export const useCopy = (text: string) => {
  let input = document.createElement('input'); // 创建输入框
  input.value = text; // 给输入框value赋值
  document.body.appendChild(input); // 追加到body里面去
  input.select(); // 选择输入框的操作
  document.execCommand('Copy'); // 执行复制操作
  document.body.removeChild(input); // 删除加入的输入框
  ElMessage.success('复制成功!');
};

引入Element-Plus图标库

import * as elIcons from '@element-plus/icons-vue';
// 获取所有Element-Plus图标组件名称,如搜索图标Search
export function getElementPlusIconfontNames() {
  return new Promise<string[]>((resolve, reject) => {
    nextTick(() => {
      const iconfonts = [];
      const icons = elIcons as any;
      // 遍历添加icons组件名称
      for (const i in icons) {
        iconfonts.push(icons[i].name);
      }
      if (iconfonts.length > 0) {
        resolve(iconfonts);
      } else {
        reject('No ElementPlus Icons');
      }
    });
  });
}

引入Iconfont图标库

  • 先加载图标样式表

    const cssUrls: Array<string> = [
      '//at.alicdn.com/t/c/font_3846007_vf3shrhbpya.css', // 阿里图标库cs,每添加一次图标都需要更换
      '//cdn.bootcdn.net/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css', // font-awesome的css
    ];
    export default function init() {
      // 遍历加载图标链接样式
      if (cssUrls.length > 0) {
        cssUrls.map((v) => {
          loadCss(v);
        });
      }
    }
    
    // 通过创建link标签引入样式链接
    export function loadCss(url: string): void {
      const link = document.createElement('link'); // 创建link标签
      link.rel = 'stylesheet';
      link.href = url;
      // 是否采用跨域的方式加载。它可以取两个值
      // anonymous(跨域请求时,不发送用户凭证,主要是 Cookie)
      // use-credentials(跨域时发送用户凭证)。
      link.crossOrigin = 'anonmous';
      document.getElementsByTagName('head')[0].appendChild(link);
    }
    
  • 获取当前页面中从指定域名加载到的样式表内容:在调用这个函数之前必须要先引入样式。

    // 获取样式表内容
    function getStylesFromDomain(domain: string) {
      const sheets = [];
      const styles: StyleSheetList = document.styleSheets;
      for (const key in styles) {
        if (styles[key].href && (styles[key].href as string).indexOf(domain) > -1) {
          sheets.push(styles[key]);
        }
      }
      return sheets;
    }
    

    调用这个函数之后就可以获取到当前图标库相关样式表内容,我们的目的就是拿到图标名称,可以提取rules数组内的样式名称就可以拿到所有图标名称了。 揭秘buildAdmin开源项目引入的四种图标方式!

    // 获取所有iconfont图标库图标名称
    export function getIconfontNames() {
      init();
      return new Promise<string[]>((resolve, reject) => {
        nextTick(() => {
          const iconfonts = [];
          const sheets = getStylesFromDomain('at.alicdn.com');
          for (const key in sheets) {
            const rules: any = sheets[key].cssRules;
            for (const k in rules) {
              // .表示匹配除换行符 \n 之外的任何单字符
              // *表示单个字符匹配任意次
              if (
                rules[k].selectorText &&
                /^\.icon-(.*)::before$/g.test(rules[k].selectorText)
              ) {
                // 去掉样式的.符号以及::before
                iconfonts.push(
                  `${rules[k].selectorText
                    .substring(1, rules[k].selectorText.length)
                    .replace(/\:\:before/gi, '')}`
                );
              }
            }
          }
          if (iconfonts.length > 0) {
            resolve(iconfonts);
          } else {
            reject('No Iconfont style sheet');
          }
        });
      });
    }
    

引入FontAwesome图标库

  • 先加载图标样式表:也就是直接调用init函数,加载相应的样式链接。

  • 获取当前页面中从指定域名加载到的样式表内容:如下图所示。

    揭秘buildAdmin开源项目引入的四种图标方式!

    export function getAwesomeIconfontName() {
      init();
      return new Promise<string[]>((resolve, reject) => {
        nextTick(() => {
          const iconfonts = [];
          // 获取所有图标名称
          const sheets = getStylesFromDomain(
            'cdn.bootcdn.net/ajax/libs/font-awesome/'
          );
          for (const key in sheets) {
            const rules: any = sheets[key].cssRules;
            // 处理方法与iconfont一致,只不过名称不一样
            for (const k in rules) {
              if (
                rules[k].selectorText &&
                /^\.fa-(.*)::before$/g.test(rules[k].selectorText)
              ) {
                if (rules[k].selectorText.indexOf(', ') > -1) {
                  // selectorText里有多个图标,只提取第一个
                  const iconNames = rules[k].selectorText.split(', ');
                  iconfonts.push(
                    `${iconNames[0]
                      .substring(1, iconNames[0].length)
                      .replace(/\:\:before/gi, '')}`
                  );
                } else {
                  iconfonts.push(
                    `${rules[k].selectorText
                      .substring(1, rules[k].selectorText.length)
                      .replace(/\:\:before/gi, '')}`
                  );
                }
              }
            }
          }
    
          if (iconfonts.length > 0) {
            resolve(iconfonts);
          } else {
            reject('No AwesomeIcon style sheet');
          }
        });
      });
    }
    

引入本地svg图标

在引入本地svg图标之前,我们先了解一下svg标签的相关知识:

🔍 什么是svg?

  • svg:即 Scalable Vector Graphics,是一种用来绘制矢量图的 HTML5 标签,与canvas有点类似,它可以像HTML画布一样用于制作图形和动画。

    svg标签常见属性有:

    • x: x轴协调图像的位置。
    • y: y轴协调图像的位置。
    • width: 图像的宽度。
    • height: 图片的高度。
    • viewBox: SVG元素的界限。
    • id、class属性
    • fill:svg元素的填充颜色。
    • stroke:svg 元素的描边颜色,例如线条、文本等描边颜色。
    • ....

    svg标签内部常见的绘制标签有:

    • <line> 标签:绘制一条直线。
    • <rect> 标签:绘制一个矩形。
    • <polygon> 标签:绘制一个多边形。
    • <circle> 标签:绘制一个圆形。
    • <ellipse> 标签:绘制一个椭圆。
    • <path> 标签:于绘制路径,其是 svg 基本形状中最强大的一个,你可以用它创建线条,曲线,弧形,圆等各种形状,其具有 d 属性,用于指定一系列绘制的命令,命令后面接坐标。

🔍 如何加载svg?

  • 在html中如何定义svg:在svg标签内定义绘制图形的标签。

    // 方式一:只能定义一个svg图标
    <svg class="hamburger" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="64" height="64">
    <path
      d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
    </svg>
    
    /* 方式二:可定义多个svg图标,需要symbol标签配合,把绘制标签定义在symbol标签内 */
    /* 一般是在index.html文件中body标签下定义 */
    <svg>
       // 一个symbol定义代表一个图标
       <symbol id="local-vue"></symbol>
       <symbol id="local-logo"></symbol>
       ...
    </svg>
    
    /* 然后就可以在组件中实例化使用 */
    <svg><use href="#local-vue"></use></svg>
    

🔍 封装加载svg插件?

  • 去掉svg标签,只取svg标签内部的绘制内容。

    let idPrefix = '';
    const iconNames: string[] = [];
    const svgTitle = /<svg([^>+].*?)>/;
    const clearHeightWidth = /(width|height)="([^>+].*?)"/g;// 清空宽高
    const hasViewBox = /(viewBox="[^>+].*?")/g;// 是否有ViewBox属性
    const clearReturn = /(\r)|(\n)/g; // 清空换行符
    const clearFill = /(fill="[^>+].*?")/g; // 清理 svg 的 fill
    // 查找svg所有文件
    function findSvgFile(dir: string = '../../../assets/icons/'): string[] {
      const svgRes = [];
      // readdirSync,返回一个包含“指定目录下所有文件名称”的数组对象
      // [ Dirent { name: 'vue.svg', [Symbol(type)]: 1 } ]
      const dirents = readdirSync(dir, {
        withFileTypes: true,
      });
      console.log(dirents);
      for (const dirent of dirents) {
        iconNames.push(`${idPrefix}-${dirent.name.replace('.svg', '')}`); // [ 'local-vue' ]
        // 如果path表示的是一个目录则返回true
        if (dirent.isDirectory()) {
          svgRes.push(...findSvgFile(dir + dirent.name + '/'));
        } else {
          const svg = readFileSync(dir + dirent.name)
            .toString().replace(clearReturn, '').replace(clearFill, 
            'fill=""'.replace(svgTitle, ($1, $2) => {
              let width = 0;
              let height = 0;
              let content = $2.replace(clearHeightWidth,
                (s1: string, s2: string, s3: number) => {
                  if (s2 === 'width') {
                    width = s3;
                  } else if (s2 === 'height') {
                    height = s3;
                  }
                  return '';
                }
              );
              if (!hasViewBox.test($2)) {
                content += `viewBox="0 0 ${width} ${height}"`;
              }
              // 去掉扩展名
              return `<symbol id="${idPrefix}-${dirent.name.replace(
                '.svg',
                ''
              )}" ${content}>`;
            })
            .replace('</svg>', '</symbol>');// 替换尾部标签
          svgRes.push(svg);
        }
      }
      return svgRes;
    }
    
  • 转化为真正渲染的html,在vite.config中的plugin插件属性中引入svgBuilder并传入存放svg图标的路径。

    /**
     *
     * @param path // 所有svg图标存放地址
     * @param perfix // 图标自定义前缀
     * @returns
     */
    export const svgBuilder = (path: string, perfix = 'local') => {
      if (path === '') return;
      idPrefix = perfix;
      // 每个图标都是symbol标签,去掉了svg标签,只包含的svg内部嵌套标签,res是一个数组,每个元素就是一个图标
      const res = findSvgFile(path);
      return {
        name: 'svg-transform',
        transformIndexHtml(html: string) {
          /* eslint-disable */
          return html.replace(
            '<body>',
            `
            <body>
            <svg id="local-icon" data-icon-name="${iconNames.join(',')}" 
            xmlns="http://www.w3.org/2000/svg" 
            xmlns:xlink="http://www.w3.org/1999/xlink" 
            style="position: absolute; width: 0; height: 0">
            ${res.join('')}
            </svg>
            `
          );
          /* eslint-enable */
        },
      };
    };
    
  • 获取所有本地图标名称通过定义id,由于封装插件的时候已经定义了元素id="local-icon",所以可以全局定义获取。下面获取到的svgEl值如图所示。

    揭秘buildAdmin开源项目引入的四种图标方式!

    export function getLocalIconfontNames() {
      return new Promise<string[]>((resolve, reject) => {
        nextTick(() => {
          let iconfonts: string[] = [];
          const svgEl = document.getElementById('local-icon');
          // 判断DOMStringMap对象内的iconName属性是否有值
          if (svgEl?.dataset.iconName) {
            iconfonts = (svgEl?.dataset.iconName as string).split(',');
          }
    
          if (iconfonts.length > 0) {
            resolve(iconfonts);
          } else {
            reject('No Local Icons!');
          }
        });
      });
    }
    

揭秘buildAdmin开源项目引入的四种图标方式!

参考资料

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