圖示使用新姿勢- 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 中可以拿到上述的分析結果,自然也知道了這條語句產生的作用是:
- 我寫下了一個type為close的Icon Component,
- 我希望它能夠放一個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版本的工具也在開發中。