在上篇文章中,主要讨论了 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。若是大家知道答案,能够告诉我。🙃