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

作用于整个构建过程

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

plugin 几个条件:

编写一个 prefetch-webpack-plugin

Webpack 的魔法注释 Prefetch

import(/* webpackPrefetch: true */ './lazy')

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 模块,这里有两种模块。
  4. 最后结合 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)
  }
}