插件用于 bundle 文件的优化,资源管理和环境变量注入,增强 webpack 的能力
作用于整个构建过程
plugin 就是通过监听 compiler 的某些 hook 特定时机,然后处理 stats。
plugin 几个条件:
- plugin 是一个类
- 该类必须包含一个 apply 函数,该函数接收 compiler 对象
- 该类可以使用 webpack 的 compiler 和 compilation 对象的钩子
- 也可以自定义自己的钩子
编写一个 prefetch-webpack-plugin
Webpack 的魔法注释 Prefetch
import(/* webpackPrefetch: true */ './lazy')
import(/* webpackPreload: true */ './sync')
有了这个注释,可以在获取 chunk 对象的时候,拿到这个标注,从而根据这个注释给页面添加 <link rel="prefetch">
标签。
/* webpackPrefetch: true */
:把主加载流程加载完毕,在空闲时在加载其它,等再点击其他时,只需要从缓存中读取即可,性能更好,推荐使用;能够提高代码利用率,把一些交互后才能用到的代码写到异步组件里,通过懒加载的形式,去把这块的代码逻辑加载进来,性能提升,页面访问速度更快。/* webpackPreload: true */
:和主加载流程一起并行加载。
思路
- 通过
compiler.compilation
这个钩子,得到Compilation
对象; - 然后在
Compilation
对象中监听html-webpack-plugin
钩子,拿到 HTML 对象。html-webpack-plugin
(4.0-版本) 自己使用 Tapable 实现了自定义钩子,需要使用HtmlWebpackPlugin.getHooks(compilation)
的方式获取自定义的钩子。 - 读取当前 HTML 页面所有
chunks
,筛选异步加载的 chunk 模块,这里有两种模块。- 生成多个 HTML 页面,那么
html-webpack-plugin
插件会设置chunks
选项,我们需要从Compilation.chunks
来选取 HTML 页面真正用到的 chunks,然后在从 chunks 中过滤出 Prefetch chunk。 - 如果是单页应用,那么不存在 chunks 选项,这时候默认
chunks='all'
,我们需要从全部Compilation.chunks
中过滤出 Prefetch chunk。
- 生成多个 HTML 页面,那么
- 最后结合 Webpack 配置的
publicPath
得到异步 chunk 的实际线上地址,然后修改html-webpack-plugin
钩子得到的 HTML 对象,给 HTML 的<head>
添加<link rel="prefetch">
内容。
// prefetch-webpack-plugin.js
class PrefetchPlugin {
constructor() {
this.name = 'prefetch-plugin'
}
apply(compiler) {
compiler.hooks.compilation.tap(this.name, (compilation) => {
const run = this.run.bind(this, compilation)
if (compilation.hooks.htmlWebpackPluginAfterHtmlProcessing) {
// html-webpack-plugin v3 插件
compilation.hooks.htmlWebpackPluginAfterHtmlProcessing.tapAsync(this.name, run)
} else {
// html-webpack-plugin v4
HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync(this.name, run)
}
})
}
run(compilation, data, callback) {
// 获取 chunks,默认不指定就是 all
const chunkNames = data.plugin.options.chunks || 'all'
// 排除需要排除的 chunks
const excludeChunkNames = data.plugin.options.excludeChunks || []
// 所有 chunks 的 Map,用于根据 ID 查找 chunk
const chunks = new Map()
// 预取的 id
const prefetchIds = new Set()
compilation.chunks
.filter((chunk) => {
const { id, name } = chunk
// 添加到 map
chunks.set(id, chunk)
if (chunkNames === 'all') {
// 全部的 chunks 都要过滤
// 按照 exclude 过滤
return excludeChunkNames.indexOf(name) === -1
}
// 过滤想要的 chunks
return chunkNames.indexOf(name) !== -1 && excludeChunkNames.indexOf(name) === -1
})
.map((chunk) => {
const children = new Set()
// 预取的内容只存在 children 内,不能 entry 就预取吧
const childIdByOrder = chunk.getChildIdsByOrders()
for (const chunkGroup of chunk.groupsIterable) {
for (const childGroup of chunkGroup.childrenIterable) {
for (const chunk of childGroup.chunks) {
children.add(chunk.id)
}
}
}
if (Array.isArray(childIdByOrder.prefetch) && childIdByOrder.prefetch.length) {
prefetchIds.add(...childIdByOrder.prefetch)
}
})
// 获取 publicPath,保证路径正确
const publicPath = compilation.outputOptions.publicPath || ''
if (prefetchIds.size) {
const prefetchTags = []
for (let id of prefetchIds) {
const chunk = chunks.get(id)
const files = chunk.files
files.forEach((filename) => {
prefetchTags.push(`<link rel="prefetch" href="${publicPath}${filename}">`)
})
}
// 开始生成 prefetch html 片段
const prefetchTagHtml = prefetchTags.join('\\n')
if (data.html.indexOf('</head>') !== -1) {
// 有 head,就在 head 结束前添加 prefetch link
data.html = data.html.replace('</head>', prefetchTagHtml + '</head>')
} else {
// 没有 head 就加上个 head
data.html = data.html.replace('<body>', '<head>' + prefetchTagHtml + '</head><body>')
}
}
callback(null, data)
}
}
将实际处理的 HTML 数据的逻辑,扔给了 PreloadPlugin 这个类的 run
方法。html-webpack-plugin
的 htmlWebpackPluginAfterHtmlProcessing
和 beforeEmit
钩子实际是个 AsyncSeriesWaterfallHook
类型的钩子,所以需要使用 tapAsync
来绑定,然后需要执行异步回调的 callback
。
run 方法:
- 获取
html-webpack-plugin
的配置,然后根据chunks
的值从Compilation.chunks
筛选当前 HTML 页面真正用到的chunks
- 从当前页面获取
chunks
中需要预取的chunk
- 生成 prefetch link 标签,添加到 HTML 片段。
在 tapAsync Hook 的 run 中会得到三个参数:
compilation
:本次编译的 Compilation 对象data
:是html-webpack-plugin
创建的一个给其插件使用的对象,里面包含页面的 HTML 判断和html-webpack-plugin
插件实例化后的实例本身data.html
:这个是生成 HTML 页面的 HTML 片段字符串data.plugin
:这个是html-webpack-plugin
的实例,可以从data.plugin.options
读取html-webpack-plugin
插件的参数
callback
:tapAsync
的回调函数,应该将data
处理后的结果通过callback
传递给下一个处理回调
其中对于 chunks 的遍历,获取每个 chunk 的子模块(children),根据 chunk.getChildIdsByOrders
得到的 childIdByOrder
对象中的 prefetch
来判断有没有预取的模块,如果 chunk 中存在 /*webpackPrefetch: true*/
的模块,则可以得到 childIdByOrder.prefetch
数组。
最后就处理 data.html
,在 HTML 页面 <head>
标签添加 link
标签。