使用 SVG 圖標: (2) 編寫 Webpack plugin

上篇文章中,主要討論了 gulp-svg-sprites 的使用以及 Icon 組件的編寫。上文中咱們是手動複製 SVG symbol 文件的內容粘貼到 index.html。這樣操做起來十分不方便,因此在本篇文章中咱們經過編寫 webpack 插件去實現操做的自動化。html

1. webpack 插件基礎知識

編寫一個 webpack 插件也許沒想象中複雜!不信你看 react-dev-utils 中的 InterpolateHtmlPlugin。 這個插件代碼量不超過 50 行!react

如何編寫 webpack 插件呢?官方文檔給出了很是好的示例。簡單一點來講,插件是一個 class。這個 class 有一個名爲 apply 的方法。webpack 及其插件在其編譯的過程當中會觸發不少的事件。apply 方法中,咱們經過編寫回調函數來對某一階段的數據進行處理。在下文中,咱們將以實例來講明。webpack

2. 編寫第一個 webpack 插件

編寫一個插件,首先要考慮到兩點問題:git

  1. 插件實現的什麼功能
  2. webpack 及其插件給在編譯過程當中會觸發哪些事件,在事件的回調中能夠訪問到哪些信息

咱們的第一個插件要實現的功能是把 sprite.symbol.svg 文件的內容插入到 index.html 中。在示例項目中,使用了 html-webpack-plugin來處理 HTML 文件。該插件提供了幾個事件,其中有一個事件 html-webpack-plugin-before-html-processing。在 html-webpack-plugin-before-html-processing 事件回調函數中能夠獲取到 index.html 內容。經過 fs.readFile 讀取 SVG 文件的內容,再把 SVG 文件內容寫入到 index.html 中的內容中便可。github

const fs = require('fs')
function SvgSymbolInline (options = {}) {
  this.options = {
    path: 'svg/symbol/svg/sprite.symbol.svg'
  }
}
SvgSymbolInline.prototype.apply = function (compiler) {
  const self = this
  compiler.plugin('compilation', function (compilation) {
    compilation.plugin('html-webpack-plugin-before-html-processing', function (htmlPluginData, callback) {
      self.insertSvg(htmlPluginData.html).then(function (html) {
        htmlPluginData.html = html
        callback(null, htmlPluginData)
      })
    })
  })
}

SvgSymbolInline.prototype.insertSvg = function (html) {
  const self = this
  return new Promise(function (resolve, reject) {
    fs.readFile(self.options.path, 'utf8', function (err, data) {
      if (err) throw err
      // 去除 symbol 文件頭部的 xml 信息,設置元素隱藏
      data = data.replace(/<\?xml.*?>/, '').replace(/(<svg.*?)(?=>)/, '$1 style="display:none;" ')
      // 把 symbol 的內容插入 html 中
      html = html.replace(/(<body\s*>)/i, `$1${data}`)
      resolve(html)
    })
  })
}
複製代碼

在 webpack 的配置文件添加該插件,那麼如今生成的 index.html 中的 body 部分包含了 SVG 文件的內容。但這樣作存着一點點問題,SVG 圖片不能被瀏覽器緩存。因此接下來編寫第二個插件,嘗試解決這個問題。web

3. 編寫第二個 webpack 插件

第二個插件功能是把 SVG 文件添加爲 webpack 的資源,而且在 index.html 的 head 中經過 link 標籤引入該 SVG 圖片。chrome

如何在 webpack 插件中,添加一個文件呢?webpack 的 complier 對象有一個 emit 事件,能夠在該事件的回調中添加一個文件。npm

let symbolFileName = ''
function SvgSymbolLink () {
  this.options = {
    path: 'svg/symbol/svg/sprite.symbol.svg'
  }
}
SvgSymbolLink.prototype.apply = function (compiler) {
  const self = this
  compiler.plugin('emit', function (compilation, callback) {
    self.getSvgContent().then(function (content) {
      symbolFileName = `static/icon-symbol.svg`
      compilation.assets[symbolFileName] = {
        source: function () {
          return content
        },
        size: function () {
          return content.length
        }
      }
      callback()
    })
  })
}
SvgSymbolLink.prototype.getSvgContent = function () {
  const self = this
  return new Promise(function (resolve, reject) {
    fs.readFile(self.options.path, 'utf8', function (err, data) {
      if (err) throw err
      // 去除 symbol 文件頭部的 xml 信息
      data = data.replace(/<\?xml.*?>/, '')
      resolve(data)
    })
  })
}
複製代碼

在 webpack 的配置中,引入插件。執行 npm run build,在項目的構建輸出文件夾下就出現了一個名爲 icon-symbol.svg 的文件。gulp

效果以下

當開發環境時,咱們還能夠根據文件的內容生成一串 hash。在文件名後面添加這串 hash,若是文件內容有變更時,瀏覽器就會請求新生成的文件了。數組

const crypto = require('crypto')
SvgSymbolLink.prototype.hash = function (content) {
  return crypto.createHash('md5').update(content).digest('hex').substr(0, 20)
}
SvgSymbolLink.prototype.apply = function (compiler) {
  // ...
  compiler.plugin('emit', function (compilation, callback) {
    self.getSvgContent().then(function (content) {
      const hash =  process.env.NODE_ENV === 'production' ? self.hash(content) : ''
      symbolFileName = `static/icon-symbol.svg${hash ? `?h=${hash}` : ''}`
      // ...
    })
  })
}
複製代碼

如今進行第二步。經過設置 link 標籤的 rel="preload" 可讓瀏覽器提早加載資源。按照編寫第一個插件的思路,也是在 html-webpack-plugin 的事件回調中添加 link 標籤。html-webpack-plugin 會向 index.html 中插入 link 與 script 標籤,須要在插入標籤以前,添加一個新的 link。html-webpack-plugin-alter-asset-tags 正是咱們須要的。在該事件的回調函數中,htmlPluginData 的 head 部分包含了 link 標籤數組。向 head 數組再添加一個新的標籤便可。

SvgSymbolLink.prototype.apply = function (compiler) {
  const self = this
  // ...
  compiler.plugin('compilation', function (compilation) {
    compilation.plugin('html-webpack-plugin-alter-asset-tags', function (htmlPluginData, callback) {
      const head = htmlPluginData.head
      head.push({
        tagName: 'link',
        selfClosingTag: true,
        attributes: {
          href: '/' + symbolFileName,
          rel: 'preload',
          as: 'image',
          type: 'image/svg+xml',
          crossorigin: 'anonymous'
        }
      })
      callback(null, htmlPluginData)
    })
  })
}
複製代碼

最後,index.html 內容以下。

<head>
    <link href="./static/icon-symbol.svg" rel="preload" as="image" type="image/svg+xml" crossorigin="anonymous">
</head>
複製代碼

以這種方式引用文件,須要調整 Icon 組件。

<template>
  <svg :class="iconClassName" :style="{color: this.color}">
    <use :xlink:href="/static/icon-symbol.svg#' + type"></use>
  </svg>
</template>
複製代碼

完整的代碼在這裏

4. 總結

本文介紹了開發 webpack 插件的一些知識,並根據項目需求,編寫了兩個 webpack 插件。若是對項目中的某一個流程以爲不太滿意,能夠嘗試經過編寫 loader 或者 插件來解決問題。編寫 webpack 插件沒有想象中那麼困難,只要你願意去動手作。Don't repeat yourself

一些問題

使用 preload 的話,在控制檯會出現相似於 The resource [xxxx] was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it wasn't preloaded for nothing 這樣的警告信息。這條警告信息出現的緣由是 preload 的資源未被使用,但是即使我是在頁面中使用了這個 SVG 文件仍是有這個警告,因此後來把 link 的 rel 屬性設置爲 prefetch。GitHub 與 Stack Overflow 上相關的討論連接1連接2。若是大家知道答案,能夠告訴我。🙃

參考資料

相關文章
相關標籤/搜索