原文連接javascript
做者:梯田前端
圖標是前端在業務開發中不得不寫的一個東西,以我司的幾個部門爲例,每一個組在寫圖標上都有不同的方式:vue
這幾種使用方式各有千秋,下面談一談他們的優缺點:)java
組1的使用方式【簡單】,不須要手動引入每一個 svg 文件,缺點是字體圖標不如 svg 文件【可擴展性好】,同時爲了引入一個圖標引入一個完整的字體圖標也會帶來必定冗餘。node
而其餘兩個組的問題在於【圖標的引入】以及【管理】方面,須要手動引入 svg 文件,固然優勢也很是可觀。react
這裏明確一個事實:svg 圖標的綜合表現是遠大於字體圖標的,從 antd 從 3.9.0 的更新就能夠看出來。webpack
摘自官方文檔 git
antd 的圖標使用體驗一直很好,好比下面的代碼就能夠定義一個 home
圖標程序員
<Icon type="home" />
複製代碼
不須要事先引入任何資源 ,只須要指定 type = "home"
就可使用。 可是 antd 沒有解決一個問題,那就是如何作到圖標的按需引用?github
摘自官方文檔
即使是這裏提到的 webpack 插件 也不過是圖標改爲了後置引入,並無解決圖標的按需引用問題。
固然 antd 很差優雅的這個問題是由它的使用方式決定的(合理猜想),做爲一個流行的組件庫,antd 在引入新的技術的同時又要照顧以前使用者的使用體驗,不可避免的會出現一些瑕疵。這是能夠理解的,不過換成咱們普通業務開發而言,咱們沒有必要去追求太過完美的開發體驗,作出略微的犧牲便可實現【既保持 antd Icon 同樣的使用方式,又按需引用了 svg 文件】,怎麼實現呢?
svg-sprite-loader 是一個在 webpack 中應用比較普遍的 svg 處理庫,它能夠將代碼裏引入的 svg 文件合併到一塊兒,而後以 svg symbol 的方式使用,關於它的使用方式網上有大量的文章,因此本文不會再描述它如何使用,請讀者自行查閱,
值得一提的是,介紹此 loader 的的文章中,通常都會附帶如何一次性引入項目中須要的全部 svg 的方法,那就是利用 webpack 的 require.context api,這個 api 能夠獲取一個特定的上下文,主要用來實現自動化導入模塊,因此爲了避免再每一個模塊中一一寫 import 'xxx.svg
這樣的語句,使用這個 api 是有必要的。
藉助 require.context
和 svg-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
語法,因此這個表達式的 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.svgSet.add(value.value);
});
}
}
};
}
複製代碼
最後,將 Set
裏存放的 svg ,遍歷以後,用 babel 工具庫生成以下的語句:
import 'xxx.svg'
複製代碼
而後插入到此文件的最頂端,剩下的事情就交給 webpack 以及其餘 loader 處理了。
我已經將上述代碼封裝了一個 npm 包,歡迎你們下載和體驗,固然目前還比較簡陋,源碼和詳細文檔也將在不久後發佈。
還有 vue 版本的工具也在開發中。
這篇文章實現的 babel 插件原理並不複雜,記錄下來但願可以幫助到你們:遇到項目中的問題的時候能夠參考社區的實現來解決。最後歡迎你們關注酷家樂前端團隊,能夠找我私聊或者內推,個人郵箱:titian@qunhemail.com
代碼參考:
工具使用