插件用于 bundle 文件的优化,资源管理和环境变量注入,增强 webpack 的能力

作用于整个构建过程

plugin 就是通过监听 compiler 的某些 hook 特定时机,然后处理 stats。

plugin 几个条件:

  • plugin 是一个类
  • 该类必须包含一个 apply 函数,该函数接收 compiler 对象
  • 该类可以使用 webpack 的 compiler 和 compilation 对象的钩子
  • 也可以自定义自己的钩子

编写一个 prefetch-webpack-plugin

Webpack 的魔法注释 Prefetch

  1. import(/* webpackPrefetch: true */ './lazy')
  2. import(/* webpackPreload: true */ './sync')

有了这个注释,可以在获取 chunk 对象的时候,拿到这个标注,从而根据这个注释给页面添加 <link rel="prefetch"> 标签。

/* webpackPrefetch: true */:把主加载流程加载完毕,在空闲时在加载其它,等再点击其他时,只需要从缓存中读取即可,性能更好,推荐使用;能够提高代码利用率,把一些交互后才能用到的代码写到异步组件里,通过懒加载的形式,去把这块的代码逻辑加载进来,性能提升,页面访问速度更快。
/* webpackPreload: true */:和主加载流程一起并行加载。

思路

  1. 通过 compiler.compilation 这个钩子,得到 Compilation 对象;
  2. 然后在 Compilation 对象中监听 html-webpack-plugin 钩子,拿到 HTML 对象。html-webpack-plugin(4.0-版本) 自己使用 Tapable 实现了自定义钩子,需要使用 HtmlWebpackPlugin.getHooks(compilation) 的方式获取自定义的钩子。
  3. 读取当前 HTML 页面所有 chunks,筛选异步加载的 chunk 模块,这里有两种模块。
    • 生成多个 HTML 页面,那么 html-webpack-plugin 插件会设置chunks 选项,我们需要从 Compilation.chunks 来选取 HTML 页面真正用到的 chunks,然后在从 chunks 中过滤出 Prefetch chunk。
    • 如果是单页应用,那么不存在 chunks 选项,这时候默认 chunks='all',我们需要从全部 Compilation.chunks 中过滤出 Prefetch chunk。
  4. 最后结合 Webpack 配置的 publicPath 得到异步 chunk 的实际线上地址,然后修改 html-webpack-plugin 钩子得到的 HTML 对象,给 HTML 的 <head> 添加 <link rel="prefetch"> 内容。
  1. // prefetch-webpack-plugin.js
  2. class PrefetchPlugin {
  3. constructor() {
  4. this.name = 'prefetch-plugin'
  5. }
  6. apply(compiler) {
  7. compiler.hooks.compilation.tap(this.name, (compilation) => {
  8. const run = this.run.bind(this, compilation)
  9. if (compilation.hooks.htmlWebpackPluginAfterHtmlProcessing) {
  10. // html-webpack-plugin v3 插件
  11. compilation.hooks.htmlWebpackPluginAfterHtmlProcessing.tapAsync(this.name, run)
  12. } else {
  13. // html-webpack-plugin v4
  14. HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync(this.name, run)
  15. }
  16. })
  17. }
  18. run(compilation, data, callback) {
  19. // 获取 chunks,默认不指定就是 all
  20. const chunkNames = data.plugin.options.chunks || 'all'
  21. // 排除需要排除的 chunks
  22. const excludeChunkNames = data.plugin.options.excludeChunks || []
  23. // 所有 chunks 的 Map,用于根据 ID 查找 chunk
  24. const chunks = new Map()
  25. // 预取的 id
  26. const prefetchIds = new Set()
  27. compilation.chunks
  28. .filter((chunk) => {
  29. const { id, name } = chunk
  30. // 添加到 map
  31. chunks.set(id, chunk)
  32. if (chunkNames === 'all') {
  33. // 全部的 chunks 都要过滤
  34. // 按照 exclude 过滤
  35. return excludeChunkNames.indexOf(name) === -1
  36. }
  37. // 过滤想要的 chunks
  38. return chunkNames.indexOf(name) !== -1 && excludeChunkNames.indexOf(name) === -1
  39. })
  40. .map((chunk) => {
  41. const children = new Set()
  42. // 预取的内容只存在 children 内,不能 entry 就预取吧
  43. const childIdByOrder = chunk.getChildIdsByOrders()
  44. for (const chunkGroup of chunk.groupsIterable) {
  45. for (const childGroup of chunkGroup.childrenIterable) {
  46. for (const chunk of childGroup.chunks) {
  47. children.add(chunk.id)
  48. }
  49. }
  50. }
  51. if (Array.isArray(childIdByOrder.prefetch) && childIdByOrder.prefetch.length) {
  52. prefetchIds.add(...childIdByOrder.prefetch)
  53. }
  54. })
  55. // 获取 publicPath,保证路径正确
  56. const publicPath = compilation.outputOptions.publicPath || ''
  57. if (prefetchIds.size) {
  58. const prefetchTags = []
  59. for (let id of prefetchIds) {
  60. const chunk = chunks.get(id)
  61. const files = chunk.files
  62. files.forEach((filename) => {
  63. prefetchTags.push(`<link rel="prefetch" href="${publicPath}${filename}">`)
  64. })
  65. }
  66. // 开始生成 prefetch html 片段
  67. const prefetchTagHtml = prefetchTags.join('\\n')
  68. if (data.html.indexOf('</head>') !== -1) {
  69. // 有 head,就在 head 结束前添加 prefetch link
  70. data.html = data.html.replace('</head>', prefetchTagHtml + '</head>')
  71. } else {
  72. // 没有 head 就加上个 head
  73. data.html = data.html.replace('<body>', '<head>' + prefetchTagHtml + '</head><body>')
  74. }
  75. }
  76. callback(null, data)
  77. }
  78. }

将实际处理的 HTML 数据的逻辑,扔给了 PreloadPlugin 这个类的 run 方法。
html-webpack-pluginhtmlWebpackPluginAfterHtmlProcessingbeforeEmit 钩子实际是个 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 插件的参数
  • callbacktapAsync 的回调函数,应该将 data 处理后的结果通过 callback 传递给下一个处理回调

其中对于 chunks 的遍历,获取每个 chunk 的子模块(children),根据 chunk.getChildIdsByOrders 得到的 childIdByOrder 对象中的 prefetch 来判断有没有预取的模块,如果 chunk 中存在 /*webpackPrefetch: true*/ 的模块,则可以得到 childIdByOrder.prefetch 数组。

最后就处理 data.html,在 HTML 页面 <head> 标签添加 link 标签。