在上篇文章中,主要討論了 gulp-svg-sprites 的使用以及 Icon 組件的編寫。上文中咱們是手動複製 SVG symbol 文件的內容粘貼到 index.html。這樣操做起來十分不方便,因此在本篇文章中咱們經過編寫 webpack 插件去實現操做的自動化。html
編寫一個 webpack 插件也許沒想象中複雜!不信你看 react-dev-utils 中的 InterpolateHtmlPlugin。 這個插件代碼量不超過 50 行!react
如何編寫 webpack 插件呢?官方文檔給出了很是好的示例。簡單一點來講,插件是一個 class。這個 class 有一個名爲 apply 的方法。webpack 及其插件在其編譯的過程當中會觸發不少的事件。apply 方法中,咱們經過編寫回調函數來對某一階段的數據進行處理。在下文中,咱們將以實例來講明。webpack
編寫一個插件,首先要考慮到兩點問題:git
咱們的第一個插件要實現的功能是把 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
第二個插件功能是把 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>
複製代碼
完整的代碼在這裏。
本文介紹了開發 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。若是大家知道答案,能夠告訴我。🙃