【前端性能优化实践】手把手教你实现webpack图片压缩插件

86 阅读4分钟

前言

我想写一个系列:前端性能优化实践方案。网上虽然一搜一大把这样的文章,但大多缺乏体系化。也有很多讲性能优化的书籍,但其实想照着书上的知识进行实践,还是挺难的一件事。
这是该系列的第一篇文章
由于自己的知识水平有限,文章中难免有出错的地方,如果你看到,望指正。
由于刚开始写作,文章风格和写作方式还比较青涩,如果你有任何建议,可以直接向我反馈,感谢Thanks♪(・ω・)ノ

背景

最近在做页面性能优化相关工作,发现项目中很多图片尺寸都比较大(>1M),虽然使用了webp图片方案,但对于不支持webp图片格式的设备来说,图片资源加载慢的问题并没有得到有效解决。针对这个问题,有两种解决方案,一是让设计同学重新给小尺寸的图;二是自己压缩图片。问题一治标不治本,且对不同的项目无法实现复用,故这里采用第二种方案。

压缩图片采用的是 tinypng.com/ 方式进行压缩,它单次最大支持20张图片的压缩个数,如果项目图片比较少,可以直接进行手动压缩;由于我优化的项目中图片个数达到了上百张,因此,写了webpack插件来提升开发效率,同时解决了单次压缩个数的限制。

具体实现

在讲具体实现之前,我这里先说一下自己的实现思路,我会按照实现思路依次讲解实现步骤【需要了解webpack插件实现原理】:
1.获取项目构建中的图片资源
2.将获取到的图片资源依次上传到 tinypng.com/ 官网进行压缩
3.获取压缩后的图片资源,替换原有图片

1.获取项目构建的图片资源

// 兼容不同版本的webpack
apply(compiler) {
    // webpack verison >= 4
    if (compiler.hooks && this._options.compress) {
      const isWebpack4 = compiler.webpack ? false : typeof compiler.resolvers !== 'undefined';
      if (isWebpack4) {
        compiler.hooks.emit.tapPromise(pluginName, compilation => {
          // handleImgAssets 方法具体处理图片资源
          return Promise.resolve(this.handleImgAssets(compilation))
        })
      } else { // webpack 5
        compiler.hooks.compilation.tap(pluginName, compilation => {
          compilation.hooks.processAssets.tapPromise(pluginName, () => {
            return Promise.resolve(this.handleImgAssets(compilation))
          })
        })
      }
    } else if (compiler.plugin && this._options.compress) {
      // webpack3的写法
      compiler.plugin('emit', async (compilation, callback) => {
        await this.handleImgAssets(compilation)
        callback()
      })
    } else {
      console.log(`The webpack version number supported by ${pluginName} is 3-5!, install: https://webpack.js.org/`)
    }
  }

通过上面的代码,我们就可以拿到compilation对象,图片资源就在这个对象里面,我们把它作为参数传给handleImgAssets方法,具体处理细节在handleImgAssets中。接下来,让我们看看它具体干了些啥。

2.将获取到的图片资源依次上传到 tinypng.com/ 官网进行压缩

async handleImgAssets(compilation) {
    // 通过 assets 字段获取所有的静态资源
    const ImgAssets = compilation.assets
    // 过滤出图片
    let images = Object.keys(compilation.assets).filter(asset => IMG_TEST.test(asset))
    // 如果图片不存在,直接返回
    if (!images.length) {
      return Promise.resolve()
    }
    // 插件支持自定义压缩图片的尺寸区间,这里就是找出真正需要压缩的图片
    // 比如我设置了 大于 10kb的图片才进行压缩,这里会过滤出尺寸大于10kb的图
    images = this.filterImages(images, ImgAssets)
    // 我们将每个图片压缩过程转成promise
    // 具体实现在 compressImg 方法里
    const imgPromises = images.map(img => this.compressImg(ImgAssets, img))
    const spinner = Ora('Compressing Image......').start()
    // 并发控制
    await this.promiseLimit(imgPromises, this._options.concurrency).then(res => {
      spinner.stop()
      this._options.log && res && res.forEach(msg => console.log(msg))
    })
  }

3.获取压缩后的图片资源,替换原有图片

/** 处理图片压缩工作 */
compressImg(assets, key) {
    try {
      // 获取图片的原始资源
      const file = assets[key].source()
      // 上传图片资源,进行压缩。具体实现在uploadImg方法里
      const originData = await this.uploadImg(file)
      // 获取压缩后的图片资源
      const compressedData = await this.downloadImg(originData.output.url)
      // 替换原资源
      assets[key] = new RawSource(Buffer.alloc(compressedData.length, compressedData, 'binary'))
      return new Promise((resolve, reject) => resolve(msg))
    } catch (error) {
      // do something
    }
  }

/**上传图片方法,接口会自动进行压缩 */ 
  uploadImg(source) {
    const header = DefaultHeader()
    return new Promise((resolve, reject) => {
      const req = Https.request(header, res => res.on('data', data => {
        const resObj = JSON.parse(data.toString())
        // 如果报错,直接reject
        // 如果成功,resolve 接口返回的对象,里面包含压缩后的图片地址,需要再次请求去下载
        resObj.error ? reject(resObj.message) : resolve(resObj)
      }))
      req.write(source, 'binary')
      req.on('error', e => reject(e))
      req.end()
    })
  }

/** 下载压缩后的图片 */
  downloadImg(url) {
    // 这里就是拿到刚刚接口返回的地址,再次请求,下载图片资源就好
    const URL = new Url.URL(url)
    return new Promise((resolve, reject) => {
      const req = Https.request(URL, res => {
        let file = ''
        res.setEncoding('binary')
        res.on('data', data => file += data)
        res.on('end', () => resolve(file))
      })
      req.on('error', e => reject(e))
      req.end()
    })
  }

以上就完成了图片压缩工作,完整的代码可查看这里。该插件在项目打包的过程中,实现图片压缩功能,当然,如果压缩后的效果设计同学不满意,我们去掉压缩即可,不会对原有图片资源造成污染。

我将上述插件发布成了npm包,想使用的同学可以直接安装使用。目前公司内部很多项目都在使用,整体图片资源大小降低了40%以上,页面加载速度和用户体验也得到了明显的提升。

结尾

目前只支持线上压缩功能,如果你的工作环境没有网,那该插件不能正常工作。后期想通过canvas来实现离线压缩,感兴趣的同学可以一起搞一下~ヾ(◍°∇°◍)ノ゙【你将会成为共同作者】