圖標使用新姿勢- 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.context api,這個 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 版本的工具也在開發中。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM