1. 程式人生 > 實用技巧 >圖示使用新姿勢- react 按需引用 svg 的實現

圖示使用新姿勢- react 按需引用 svg 的實現

前言

圖示是前端在業務開發中不得不寫的一個東西,以我司的幾個部門為例,每個組在寫圖示上都有不一樣的方式:

  • 使用者平臺:單色圖示用 iconfont 上提供的字型檔案,彩色圖示用 img 引入代替或者使用iconfont 上提供的 symbol.js。
  • saas:引入 svg 檔案,通過react-svg-loader將其包裹成一個react元件使用。
  • 到店購:引入 svg 檔案,通過svg-sprite-loader將所有 svg 圖示處理成 svg 雪碧圖的方式使用。

這幾種使用方式各有千秋,下面談一談他們的優缺點:)

使用者平臺的使用方式【簡單】,不需要手動引入每個 svg 檔案,缺點是字型圖示不如 svg 檔案【可擴充套件性好】,同時為了引入一個圖示引入一個完整的字型圖示也會帶來一定冗餘。

而其他兩個組的問題在於【圖示的引入】以及【管理】方面,需要手動引入 svg 檔案,當然優點也非常可觀。

這裡明確一個事實:svg 圖示的綜合表現是遠大於字型圖示的,從 antd 從 3.9.0 的更新就可以看出來。

摘自官方文件

antd 的圖示使用體驗一直很好,比如下面的程式碼就可以定義一個home圖示

<Icon type="home" />

不需要事先引入任何資源 ,只需要指定type = "home"就可以使用。但是 antd 沒有解決一個問題,那就是如何做到圖示的按需引用?

摘自官方文件

即便是這裡提到的webpack 外掛也不過是圖示改成了後置引入,並沒有解決圖示的按需引用問題。

當然 antd 不好優雅的這個問題是由它的使用方式決定的(合理猜測),作為一個流行的元件庫,antd 在引入新的技術的同時又要照顧之前使用者的使用體驗,不可避免的會出現一些瑕疵。這是可以理解的,不過換成我們普通業務開發而言,我們沒有必要去追求太過完美的開發體驗,做出略微的犧牲即可實現【既保持 antd Icon 一樣的使用方式,又按需引用了 svg 檔案】,怎麼實現呢?

如何處理 svg

svg-sprite-loader是一個在webpack中應用比較廣泛的 svg 處理庫,它可以將程式碼裡引入的 svg 檔案合併到一起,然後以 svg symbol 的方式使用,關於它的使用方式網上有大量的文章,所以本文不會再描述它如何使用,請讀者自行查閱,

值得一提的是,介紹此 loader 的的文章中,一般都會附帶如何一次性引入專案中需要的所有 svg 的方法,那就是利用webpack的require.contextapi,這個 api 可以獲取一個特定的上下文,主要用來實現自動化匯入模組,所以為了不再每個模組中一一寫import 'xxx.svg這樣的語句,使用這個 api 是有必要的。

藉助require.contet和svg-sprite-loader能夠使圖示開發體驗上升一個檔次,也能配合react元件實現類似 antd Icon 的使用方式。

但是這種使用方式存在一個缺點,那就是【如何避免引入不必要的 svg】,要知道require.contet可不會區分哪些 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 實現。

資源搜尋網站大全 https://www.renrenfan.com.cn

實現原理

熟悉 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語法,所以這個表示式的type是JSXElement, 同時設定了了props.type的值為close, 所以他會有個name為type而value為close的JSXAttribute.

我們在 babel plugin 中可以拿到上述的分析結果,自然也知道了這條語句產生的作用是:

  1. 我寫下了一個type為close的Icon 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.svgCache.add(value.value);
          });
      }
    }
  };
}

最後,將Set裡存放的 svg ,遍歷之後,用 babel 工具庫生成如下的語句:

import 'xxx.svg'

然後插入到此檔案的最頂端,剩下的事情就交給 webpack 以及其他 loader 處理了。

我已經將上述程式碼封裝了一個npm 包,歡迎大家下載和體驗,當然目前還比較簡陋,原始碼和詳細文件也將在不久後釋出。

還有vue版本的工具也在開發中。