渐进式图片加载后续: 优化开发体验

368 阅读3分钟

这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战

上篇文章 渐进式图片加载 讲解了如何一步步优化网页中的图片提升加载性能,其中最开始有一步得先预处理所有的图像,只有生成对应的尺寸的图片,才能为后续的步骤提供可操作性.

但是图片是通过一个单独的脚本生成,每次都需要执行额外的步骤来生成图片,实现起来总是不够优雅.

在使用 webpack 的项目中,一般文件都是交由 webpack 来负责打包生成的,如果能将图片生成的步骤集成到 webpack 中,就不用额外去关心图片的生成.

我们可以编写一个 webpack 的插件来实现这个目标.

所有源码都在 github.com/XYShaoKang/…

定义配置项

首先我们需要知道需要处理哪里图片,可以通过配置项传入.

首先传入一个 publicDir 的绝对路径作为图片文件的根目录,然后传入一个图片路径 imageSources 的数组,其中图片路径是相对 publicDir 的相对路径.

有了图片之后,我们还得知道生成哪些宽度的图片,可以通过 widths 选项进行定制,或者如果不传的话,会使用默认的宽度列表[40, 200, 400, 800, 1200, 1600].

最后默认会出输出原始图片,如果想阻止输出原始图片的话,可以通过original:false阻止输出原始图片.

// @filename src/generaterImagePlugin.js
class GeneraterImagePlugin {
  /**
   *
   * @param {Object} options
   * @param {string} options.publicDir public 的绝对路径,默认为项目顶层的 public 文件夹
   * @param {Array<string>} options.imageSources 需要生成的图片相对 public 的路径
   * @param {Array<number> =} options.widths 需要生成的图片的宽度列表,默认为 `[40, 200, 400, 800, 1200, 1600]`
   * @param {boolean =} options.original 是否输出原始图片,默认为 true
   */
  constructor(options = {}) {
    this.options = Object.assign(
      {
        imageSources: [],
        publicDir: path.join(__dirname, '../public/'),
        widths: [40, 200, 400, 800, 1200, 1600],
        original: true,
      },
      options,
    )
  }
}

module.exports = GeneraterImagePlugin

在配置文件中添加对应的配置

// @filename webpack.config.js
const path = require('path')
const GeneraterImagePlugin = require('../src/generaterImagePlugin')

module.exports = {
  //...
  plugins: [
    new GeneraterImagePlugin({
      imageSources: [
        'image/beauty-woman-portrait-face.jpg',
        'image/cat-young-animal-curious-wildcat.jpg',
      ],
      publicDir: path.join(__dirname, '../public/'),
    }),
  ],
}

编写 apply 方法

编写一个 plugin 其实就是找到一个合适的时机,然后将我们自己的处理函数添加到对应的 hook 中即可.

每个 webpack 都要有一个 apply 方法.插件实例化之后,通过配置传给 webpack,由 webpack 会调用插件的 applay 方法,并将 compiler 对象做为参数传入.

compiler 是一个编译器实例,负责 webpack 的整体调度,其中 compiler.hooks 对应着 webpack 运行的不同阶段,通过在对应的 hook 上添加函数,当 webpack 运行到对应的阶段时,就会触发我们编写好的函数.可以理解为网页中的事件,在某些特定的情况下触发.

通过在 thisCompilation 这个 hook 上添加一个函数,能在函数中接收到 compilation 对象,这个 compilation 对象则是一个编译的实例,负责一次编译的调度,跟 compiler 类似 compilation.hooks 上也有一堆 hook 可以往上面添加处理函数,而我们的图片生成的函数则可以添加到 processAssets 这个 hook 中.

处理图片相关的代码如下:

// @filename src/generaterImagePlugin.js
const sharp = require('sharp')
const path = require('path')

class GeneraterImagePlugin {
  // ...
  apply(compiler) {
    //...
    compiler.hooks.thisCompilation.tap(pluginName, compilation => {
      compilation.hooks.processAssets.tapPromise(
        {
          name: 'generater-image-plugin',
          stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
        },
        () => {
          let tasks = imageSources
            .map(
              img =>
                widths.map(async width => {
                  const input = path.join(publicDir, img)
                  const data = await sharp(input).resize(width).toBuffer()
                  const output = img.replace(/(.*)(\.\w+?$)/, `$1-${width}$2`)

                  compilation.emitAsset(output, new RawSource(data))
                }),
              // ...
            )
            .flat()

          return Promise.allSettled(tasks)
        },
      )
    })
  }
}

module.exports = GeneraterImagePlugin

详细源码请查看 generaterImagePlugin.js