【Web】阿里icon圖標webpack插件(webpack-qc-iconfont-plugin)詳解

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;
              }
            };
          }
        }
      })
    }
    
    1. 代碼邏輯基本理順了,下面就是webpack插件一些簡單知識了
    • 涉及知識點:
      • ES6 class 構造函數,什麼是構造函數這裡不多講,類似於後端類
      • webpack 事件鉤子 tapable ,這個看下官方文檔,初步認識即可,類似於後端的委託代{過}{濾}理
      • webpack 的 compilercompilation, 這個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;
    
  1. 這樣一個簡單的webpack插件基架就完成了,剩下的就是根據我剛才分析寫出 IconfontDownloadCssIconfontDownloadFontFile 的邏輯
  2. 最後為了便於其他開發者可以個性化的使用,我們應該為我們的插件提供鉤子事件,這裡就需要用到 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 插件的實現原理,初步掌握了 compilercompilation
    • 半懵半懂的了解了 tapable
    • 看懂了未來前端的前景會越來越好,因為能實現的東西越來越多,查閱資料的同時也重新認識了一次前端,發現很多新東西和自己不曾了解的代碼寫法
    • 最後祝願每一位努力奮鬥的前端小夥伴們越來越好,在前端這條路上不斷奮進和學習,不要輕易放棄喲!

註:原文發佈與52pojie,由於很多小夥伴反應沒有賬號看不到文章,故轉到我的博客再發一次,鑒於此以後我發文都盡量一式兩份~~~~~哈哈哈