图标使用新姿式- react 按需引用 svg 的实现

原文连接javascript

做者:梯田前端

前言

图标是前端在业务开发中不得不写的一个东西,以我司的几个部门为例,每一个组在写图标上都有不同的方式:vue

  • 组1:单色图标用 iconfont 上提供的字体文件,彩色图标用 img 引入代替或者使用iconfont 上提供的 symbol.js 。
  • 组2:引入 svg 文件,经过 react-svg-loader 将其包裹成一个 react 组件使用。
  • 组3:引入 svg 文件,经过 svg-sprite-loader 将全部 svg 图标处理成 svg 雪碧图的方式使用。

这几种使用方式各有千秋,下面谈一谈他们的优缺点:)java

组1的使用方式【简单】,不须要手动引入每一个 svg 文件,缺点是字体图标不如 svg 文件【可扩展性好】,同时为了引入一个图标引入一个完整的字体图标也会带来必定冗余。node

而其余两个组的问题在于【图标的引入】以及【管理】方面,须要手动引入 svg 文件,固然优势也很是可观。react

这里明确一个事实:svg 图标的综合表现是远大于字体图标的,从 antd 从 3.9.0 的更新就能够看出来。webpack

摘自官方文档 git

antd 3.9.0

antd 的图标使用体验一直很好,好比下面的代码就能够定义一个 home 图标程序员

<Icon type="home" />
复制代码

不须要事先引入任何资源 ,只须要指定 type = "home" 就可使用。 可是 antd 没有解决一个问题,那就是如何作到图标的按需引用?github

摘自官方文档

即使是这里提到的 webpack 插件 也不过是图标改为了后置引入,并无解决图标的按需引用问题。

固然 antd 很差优雅的这个问题是由它的使用方式决定的(合理猜想),做为一个流行的组件库,antd 在引入新的技术的同时又要照顾以前使用者的使用体验,不可避免的会出现一些瑕疵。这是能够理解的,不过换成咱们普通业务开发而言,咱们没有必要去追求太过完美的开发体验,作出略微的牺牲便可实现【既保持 antd Icon 同样的使用方式,又按需引用了 svg 文件】,怎么实现呢?

如何处理 svg

svg-sprite-loader 是一个在 webpack 中应用比较普遍的 svg 处理库,它能够将代码里引入的 svg 文件合并到一块儿,而后以 svg symbol 的方式使用,关于它的使用方式网上有大量的文章,因此本文不会再描述它如何使用,请读者自行查阅,

值得一提的是,介绍此 loader 的的文章中,通常都会附带如何一次性引入项目中须要的全部 svg 的方法,那就是利用 webpack 的 require.context api,这个 api 能够获取一个特定的上下文,主要用来实现自动化导入模块,因此为了避免再每一个模块中一一写 import 'xxx.svg 这样的语句,使用这个 api 是有必要的。

借助 require.contextsvg-sprite-loader 可以使图标开发体验上升一个档次,也能配合 React 组件实现相似 antd Icon 的使用方式。

可是这种使用方式存在一个缺点,那就是【如何避免引入没必要要的 svg】,要知道 require.context 可不会区分哪些 svg 是真正须要的,固然对于我的项目而言,咱们能够给一个页面固定一个文件夹存在真正须要的 svg 文件,可是对于多页面的 repo 而言,咱们没法也不必给每个页面都设置一个专门存放该页面须要的 svg 的文件夹。

做为一个挑剔的程序员,我须要一种更智能更自动化的方式去引入我真正须要的 svg 图标。

思路分析

如今要解决的问题是我须要在写下相似如下代码的时候:

<Icon type="close" />
复制代码

有种工具能同时在文件中帮我 import 一个 close.svg

好比下面的代码:

import Icon from './Icon.jsx';

ReactDOM.render(<Icon type="close"/>);

复制代码

通过处理后变成这样:

import Icon from './Icon.jsx';
import './assets/close.svg'

ReactDOM.render(<Icon type="close"/>);
复制代码

想想,以前使用过什么工具?会自动帮咱们引入咱们所须要的代码呢?

答案是:babel-plugin-transform-runtime,一个自动帮前端工程师导入 polyfill 的 babel 插件,

如下是官网介绍

Externalise references to helpers and builtins, automatically polyfilling your code without polluting globals

因此,参考 babel-plugin-transform-runtime 的原理和做用 ,咱们想要自动导入一个 svg,也能够借用 babel-plugin 实现。

实现原理

熟悉 babel 的同窗,应该知道 babel 插件做用原理,是经过对转化成 ast 的 js 代码作一些更改、替换之类的操做,不熟悉的同窗能够点 这里 了解一下 babel 插件是如何开发的。

之前文咱们提到的这一句代码 <Icon type="close"/> 为例,它通过 babel 转化后的 ast 长这个样子

转化成 json 会更清晰一些:

{
 "expression": {
    "type": "JSXElement",
    "start": 0,
    "end": 20,
    "openingElement": {
      "type": "JSXOpeningElement",
      "start": 0,
      "end": 20,
      "attributes": [
        {
          "type": "JSXAttribute",
          "start": 6,
          "end": 18,
          "name": {
            "type": "JSXIdentifier",
            "start": 6,
            "end": 10,
            "name": "type"
          },
          "value": {
            "type": "Literal",
            "start": 11,
            "end": 18,
            "value": "close",
            "raw": "\"close\""
          }
        }
      ],
      "name": {
        "type": "JSXIdentifier",
        "start": 1,
        "end": 5,
        "name": "Icon"
      },
      "selfClosing": true
    },
    "closingElement": null,
    "children": []
    }
  }
复制代码

由于用的是 Jsx 语法,因此这个表达式的 typeJSXElement , 同时设置了了 props.type 的值为 close , 因此他会有个 nametypevaluecloseJSXAttribute .

咱们在 babel plugin 中能够拿到上述的分析结果,天然也知道了这条语句产生的做用是:

  1. 我写下了一个 typecloseIcon Component
  2. 我但愿它可以放一个 close.svg 在这里

因此咱们能够 new 一个 Set() 对象,将当前 close 这个关键词存放进去, 为何用 Set ,由于 Set 中的对象是不想等的,免去重复添加关键词而后再去重的必要。

代码演示:

function plugin({ types: t }) {
  return {
    visitor: {
      Program: {
        enter(path, state) {
          state.svgSet = new Set();
        }
      }
    }
  };
}
复制代码

在初次访问整个语法树的时候,建立一个 Set 对象,注意 svgSet 必定要挂在 state 上。

而后借用 babel plugin 分析此文件内的全部 JSXElement ,直到整个文件的代码被处理完毕,这样咱们就能拿到一个装满了全部关键词的 Set 对象。

代码片断:

function plugin({ types: t }) {
  return {
    visitor: {
      Program: {
       ...
      },
      JSXElement(path, state) {
        const {
          openingElement: {
            attributes
          }
        } = path.node;
        attributes
          .forEach(({ name, value }) => {
            // 判断 name.name 是否等于 "type" 或者是其余设置好的关键词
            state.svgSet.add(value.value);
          });
      }
    }
  };
}
复制代码

最后,将 Set 里存放的 svg ,遍历以后,用 babel 工具库生成以下的语句:

import 'xxx.svg'
复制代码

而后插入到此文件的最顶端,剩下的事情就交给 webpack 以及其余 loader 处理了。

我已经将上述代码封装了一个 npm 包,欢迎你们下载和体验,固然目前还比较简陋,源码和详细文档也将在不久后发布。

还有 vue 版本的工具也在开发中。

后记

这篇文章实现的 babel 插件原理并不复杂,记录下来但愿可以帮助到你们:遇到项目中的问题的时候能够参考社区的实现来解决。最后欢迎你们关注酷家乐前端团队,能够找我私聊或者内推,个人邮箱:titian@qunhemail.com

代码参考:

工具使用

相关文章
相关标签/搜索