圖標使用新姿式- 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

代碼參考:

工具使用

相關文章
相關標籤/搜索