【Web】阿里icon圖標webpack插件(webpack-qc-iconfont-plugin)詳解
- 2020 年 4 月 14 日
- 筆記
- JavaScript筆記, 工具-插件
webpack-qc-iconfont-plugin
webpack-qc-iconfont-plugin是一個webpack插件,可以輕鬆地幫你將阿里icon的圖標項目下載至本地
開發初衷
- 之前已經發佈過gulp的版本了,但是在webpack流行的時代,我還是覺得webpack插件版還是很有必有的,於是在我加班加點的研究下,我終於實現了webpack插件版
- 為啥子要加班加點呢,因為我很懶,到目前為止都沒有完整的看過官方的文檔,所以基礎很差,因此連更半夜的才把這個插件完成了,請原諒我的懶,真的,我一看文檔就感覺要睡著了,不知道有沒有同感的同學
實現原理
-
鑒於之前gulp版本的,有同學說看不懂,於是乎我決定在該版進行一個原理講解,大佬勿噴,小女子只是分享下學習心得。
-
實現這個插件首先你得研究阿里提供的css代碼,這裡我提供一個供大傢伙學習使用的 //at.alicdn.com/t/font_1425510_3v068prmkkw.css
-
研究這個你會發現,它其實就是一個css文件,保存下來就可以了。因此我們可以發起請求將css文件下載到本地,npm官方提供了相當多請求封裝包,什麼request/http/download…,你可以隨便挑一個你喜歡的。
-
文件請求成功後,因為我們想要可以自定義前綴,刪除部分不需要的代碼,這個時候需要用到正則,這裡以插件源碼來說明:
let rawData = body; if (!isDev) rawData = rawData.replace(new RegExp(urlPrefix, 'g'), fontPath) // 當為生成環境時,將css中的在線url css地址替換為本地字體文件路徑 fontPath var result = '/* 字體圖標,來源路徑:"' + url + '"*/ \r\n'; // 添加字體圖標來源url注釋,以便於快速定位樣式來源 var delUnnecessary = rawData.replace(/\.iconfont[\s\S]*?\}/, ''); // 利用正則刪除 .iconfont { ... } 圖標初始化代碼 var iconCss = delUnnecessary.match(/\.icon\-[\s\S]*?\}/g); // 利用正則匹配出所有 .icon-XXX { ... } 的圖標樣式代碼 var handlerData = keepIconFontStyle ? rawData : delUnnecessary; // 根據配置的 keepIconFontStyle 識別是否需要刪除 .iconfont { ... } 圖標初始化代碼 result += handlerData.replace(/\.icon\-[\s\S]*?\}/g, ''); // 利用正則刪除原有的 .icon-XXX { ... } 的圖標樣式代碼 // 在循環匹配出的 iconCss 重新生成正確前綴的 .icon-XXX { ... } 的圖標樣式代碼 for (var i in iconCss) { var item = iconCss[i]; if (iconPrefix) item = item.replace(/\.icon\-/, iconPrefix); result += item + '\r\n'; } // 最後刪除多餘的空行 result = result.replace(/\r{2,}/g, '\r'); result = result.replace(/\n{2,}/g, '\n');
-
這樣css文件就生成結束了,接下來就根據開發或生產環境決定是否需要下載css中引用的字體圖標文件,這裡通過對阿里提供的css文件分析,找到字體圖標文件路徑,如下:
//at.alicdn.com/t/font_1425510_3v068prmkkw.eot
//at.alicdn.com/t/font_1425510_3v068prmkkw.woff
//at.alicdn.com/t/font_1425510_3v068prmkkw.woff2
//at.alicdn.com/t/font_1425510_3v068prmkkw.ttf
//at.alicdn.com/t/font_1425510_3v068prmkkw.svg
與提供的css文件比較:
//at.alicdn.com/t/font_1425510_3v068prmkkw.css
-
經過對比,發現字體圖標文件路徑,就是提供的css文件換個擴展名而已,因此,我們可以把需要下載的字體圖標擴展名寫成一個數組,利用遞歸請求文件路徑下載字體圖標文件到本地
-
最後,webpack版有模板文件的說法,為此需要將我們生成的css文件注入到模板文件中,這裡以插件源碼講解:
if (template) { // 判斷 template 配置存在的時候進行注入 compiler.hooks.emit.tap(pluginName, compilation => { // 註冊webpack插件compiler emit hook for (var filename in compilation.assets) { // 循環 compilation.assets 準備輸出的資源列表 if (filename === template) { // 找到模板文件 const htmlData = compilation.assets[filename].source() // 獲取模板文件字符串數據 if (!htmlData) return const headLinkCss = '<link rel="stylesheet" href="./' + cssName + '">' // 生成本地路徑的 head link css標籤 // 詢問該數據中已是否存在head link css標籤,這裡的判斷是由於該插件代碼會被先後執行兩次,這需要你對webpack的compiler 與 compilation有初步的認識,compiler會在整個webpack生命周期中存在,而compilation是在每次編譯時執行,因此前後會執行一次compiler 和 compilation,就是兩次,因此未避免向生產環境注入兩次head link css,我們需要進行過濾 const findHeadLinkCss = htmlData.includes(headLinkCss) const htmlArr = htmlData.split('</head>') // 根據 '</head>' 分割數據為數組 let htmlHeadBefore = htmlArr[0] // 獲取第一個數據 if (isDev) { // 如果是開發環境,則將生成的css以 '<style> ... </style>' 形式注入到模板文件中,以便開發調試 const iconCss = compilation.assets[cssName].source() if (!iconCss) return htmlHeadBefore += '<style>' + iconCss + '</style>' } else if (!findHeadLinkCss) { // 如果是生成環境,且模板文件中不存在head link css標籤,便將前面生成的head link css標籤注入到數據中 htmlHeadBefore += headLinkCss } // 最後鏈接前後代碼 const handledHtml = htmlHeadBefore + '</head>' + htmlArr[1] // 替換掉準備輸出的模板資源文件 compilation.assets[template] = { source: function () { return handledHtml; }, size: function () { return handledHtml.length; } }; } } }) }
- 代碼邏輯基本理順了,下面就是webpack插件一些簡單知識了
- 涉及知識點:
- ES6 class 構造函數,什麼是構造函數這裡不多講,類似於後端類
- webpack 事件鉤子
tapable
,這個看下官方文檔,初步認識即可,類似於後端的委託代{過}{濾}理 - webpack 的
compiler
與compilation
, 這個webpack官方提供的事件,主要基於tapable
編寫
- 源碼解析:
// 聲明插件構造函數 class WebpackQcIconfontPlugin { // 構造函數本身 constructor(options) { // 用來對傳入的options進行處理,統一的處理便於日後的維護,也是你自己後面編寫文檔是查看options 屬性一個非常好的窗口 this.options = options || {}; if (!this.options.url) throw new Error('[' + pluginName + '] Missing options url!'); ... ... } // 構造函數的原型函數apply,webpack插件需要 apply(compiler) { // 這裡我們獲取到 webpack 插件為我們提供的事件對象 compiler // 對即將使用到的options進行獲取,我通常習慣將他們重新獲取賦值,在這裡,而不是直接在代碼中使用大量的 options.XXX,原因是當我需要去除或修改一個options屬性時我找的難受,其次是我可以清晰知道這個函數使用了那些options屬性,我的options屬性將影響到哪些函數 const options = this.options const isDev = options.isDev const fontExtList = options.fontExtList const cssName = options.cssName const template = options.template // 註冊一個 compiler.hooks.compilation 鉤子 compiler.hooks.compilation.tap(pluginName, compilation => { // 執行聲明的 IconfontDownloadCss 與 IconfontDownloadFontFile 構造函數,之所以將他們分別構造是為了邏輯清晰,增強代碼可讀性 new IconfontDownloadCss().apply(compilation, options); if (!isDev && fontExtList && fontExtList.length > 0) new IconfontDownloadFontFile().apply(compilation, options); }) } } module.exports = WebpackQcIconfontPlugin;
- 這樣一個簡單的webpack插件基架就完成了,剩下的就是根據我剛才分析寫出
IconfontDownloadCss
與IconfontDownloadFontFile
的邏輯 - 最後為了便於其他開發者可以個性化的使用,我們應該為我們的插件提供鉤子事件,這裡就需要用到
tapable
了,詳細的還請小白移步官網查閱,我就不詳細解說了,這裡簡單概述下該插件是如何編寫的事件
// 聲明一個異步事件,這裡因為返回參數一致,所以用了統一的聲明方式,簡單快速便捷
const asyncHooks = new HookMap(key => new AsyncParallelHook(['result', 'callback']))
IconfontDownloadCss
構造函數中:
// 判斷是否存在 iconfontCssCreateEnd 的註冊事件
const iconCssCreateEndHooks = asyncHooks.get('iconfontCssCreateEnd')
if (iconCssCreateEndHooks) {
// 存在使用 iconCssCreateEndHooks.callAsync 執行註冊事件
iconCssCreateEndHooks.callAsync(result, handledData => {
if (!handledData) return callback()
resultHandle(handledData)
})
} else {
// 不存在則執行默認方法
resultHandle(result)
}
// 註冊事件時使用如下方法,和官網是一致的:
WebpackQcIconfontPlugin.getHooks.for('iconfontCssCreateEnd').tapAsync(pluginName, (result, cb) => {
result += '1111111'
cb(result)
})
IconfontDownloadFontFile
構造函數中:
// 判斷是否存在 iconfontFileDownloadEnd 的註冊事件
const iconfontFileDownloadEndHooks = asyncHooks.get('iconfontFileDownloadEnd')
if (iconfontFileDownloadEndHooks) {
存在使用 iconfontFileDownloadEndHooks.callAsync 執行註冊事件
iconfontFileDownloadEndHooks.callAsync(fontFileList, handledData => {
if (!handledData) return callback()
resultHandle(handledData)
})
} else {
// 不存在則執行默認方法
resultHandle(fontFileList)
}
// 註冊事件時使用如下方法:
WebpackQcIconfontPlugin.getHooks.for('iconfontFileDownloadEnd').tapAsync(pluginName, (fontFileList, cb) => {
const testFile = '測試使用的文件而已'
fontFileList.push({
filename: 'test.text',
data: {
source: function () {
return testFile;
},
size: function () {
return testFile.length;
}
}
})
cb(fontFileList)
})
使用方法
npm install webpack-qc-iconfont-plugin
webpack.config.js
文件中進行調用:
// 引入插件
const WebpackQcIconfontPlugin = require('iconfont-webpack-plugin')
module.exports = {
plugins: [
// 插件調用代碼
new WebpackQcIconfontPlugin({
url: '//at.alicdn.com/t/font_xxxxxxx_xxxxxx.css',
isDev: true,
fontPath: './iconfont/iconfont',
iconPrefix: '.cu-icon-',
keepIconFontStyle: false,
fontExt: ['.eot', '.ttf', '.svg', '.woff', '.woff2'],
template: 'index.html'
}),
]
};
Options
-
url
- 類型:String
- 默認:無,該參數是必須(沒有將會報錯)
- 描述:為阿里圖標中 – 我的圖標項目 – 中獲取的css代碼url
- 基礎用法:
new WebpackQcIconfontPlugin({url: '//at.alicdn.com/t/font_xxxxxxx_xxxxxx.css' })
-
isDev
- 類型:String,
- 默認:
true
- 描述:當前是否為開發模式
-
fontPath
- 類型:String
- 默認:
'./iconfont/iconfont'
- 描述:下載的字體圖標文件保存路徑,只有在
isDev
為false,也就是生產環境才有效
-
iconPrefix
- 類型:String
- 默認:與源文件保持一致
.icon-
- 描述:字體圖標統一前綴,如設置為
{ iconPrefix: '.cu-icon-' }
,則圖標調用為:<i class="iconfont cu-icon-XXX"></i>
-
keepIconFontStyle
- 類型:Boolean
- 默認:undefined,即未開啟,不保留
- 描述:是否保留css源文件中的
.iconfont{/*...*/}
中的樣式,該屬性多用於與vant等類似已有自己字體圖標相關初始設置的組件庫配合使用,如您沒有與類似組件使用,建議開啟或自定義一個,否則您的圖標將不會有初始樣式
-
fontExt
- 類型:Array
- 默認:[‘.eot’, ‘.ttf’, ‘.svg’, ‘.woff’, ‘.woff2’] ,即全部下載
- 描述:需要下載的字體圖標格式擴展名,只有在
isDev
為false時有效
-
template
- 類型:String
- 默認:
index.html
- 描述:生成的圖標css將自動注入模板文件,圖標生成後會根據該配置自動注入到模板文件中,無需手動調用,如不需要自動注入,可以將該值設置為 null
- 補充:開發模式下會css會以
<style> ... </style>
形式注入,生成模式下會以<link rel="stylesheet" href="./iconfont.css">
方式注入
總結
- 詳細的用法與源碼在 我的github webpack-qc-iconfont-plugin 中有,有需要小夥伴可以去下載
- 最後分析下插件的不足之處:
- 沒有實現自動根據webpack mode環境判斷生產和開發環境,學藝不精,找了很多文檔,仍不知道如何實現,若有大神願意指點迷津將非常感謝
- 插件模板文件那一塊總感覺有點什麼說不上來問題,本來想實現根據webpack的出口配置,自動實現,不需要配置的,但是發現好像理想豐滿,現實骨感
- 我學到的知識
- 深入了解了 webpack 插件的實現原理,初步掌握了
compiler
與compilation
- 半懵半懂的了解了
tapable
- 看懂了未來前端的前景會越來越好,因為能實現的東西越來越多,查閱資料的同時也重新認識了一次前端,發現很多新東西和自己不曾了解的代碼寫法
- 最後祝願每一位努力奮鬥的前端小夥伴們越來越好,在前端這條路上不斷奮進和學習,不要輕易放棄喲!
- 深入了解了 webpack 插件的實現原理,初步掌握了
註:原文發佈與52pojie,由於很多小夥伴反應沒有賬號看不到文章,故轉到我的博客再發一次,鑒於此以後我發文都盡量一式兩份~~~~~哈哈哈