【造轮子篇】开发压缩资源为zip包的webpack插件|项目复盘

1,350 阅读5分钟

一、项目简介:

开发一款可以压缩构建资源为zip包的webpack插件。

实现功能点

  • 自动压缩构建资源,生成后缀为.zip的压缩包
  • 压缩包命名通过插件传参设定

二、项目背景:

为什么开发打包压缩zip的webpack插件?

开发webpack插件,可以让我们从webpack配置工程师,深入理解webpack工作机制及原理,然后走向webpack开发工程师。那选择开发压缩打包zip的webpack,是因为项目部署经常需要将编译构建的项目打包成zip包。因此选择最常用简单的功能,开发webpack插件。

三、实践过程:

3.1 首先恶补下webpack插件的基础知识

webpack插件是伴随 webpack 构建的初始化到最后文件生成的整个工作过程,插件的目的是在于解决loader无法实现的其他事情。
在 Webpack 整个工作过程会有很多环节,为了便于插件的扩展,Webpack 几乎在每一个环节都埋下了一个钩子。这样我们在开发插件的时候,通过往这些不同节点上挂载不同的任务,就可以轻松扩展 Webpack 的能力。 webpack钩子

3.1.1 插件的运行环境

插件只能运行在webpack环境中。简单来讲,要开发调试webpack插件前就需要创造webpack运行环境,创建配置文件webpack.config.js,并安装webpack、webpack-cli依赖。将开发好的插件放到配置文件的plugins数组中,跟随webpack进行编译执行。

3.1.2 插件的基本结构

// 1. 插件名称
class Plugin{
  // 接受插件传递的参数
  constructor(options){
      
  }
  // 2. 插件上提供apply方法,webpack提供compiler对象
  apply(compiler){
      // 3. 指定一个挂载到 webpack 自身的事件钩子
      // compiler提供hooks钩子方法,作用于构建流程中
      compiler.hooks.done.tap('Plugin',// 4. 处理 webpack 内部实例的特定数据
      // 5. 功能完成后调用 webpack 提供的回调
      ()=>{
        
      })
  }
}
module.exports = Plugin

3.1.3 插件的事件处理

1)插件中如何获取传递的参数?

通过插件的构造函数进行获取。

class Plugin{
    // 接受插件传递的参数
    constructor(options){
        this.options = options
    }
}

2)插件中的错误处理

  • 参数校验阶段可以直接throw的方式将错误抛出
    throw new Error('error options')
    
  • 如果已经进入到hooks钩子阶段,可以通过compilation对象的warnings和errors接收
    compilation.warnings.push('warnings')
    compilation.errors.push('error')
    

3)对资源文件处理:通过Compilation进行文件写入

对于一些插件,本身需要产生一些静态资源。比如生成zip包,之前源码浏览时候有了解到,文件生成阶段 是在emit阶段,则我们可以监听compileremit钩子阶段,获取到compilation对象,并将最终的内容 设置到compilation.assets对象上输出到磁盘目录。 另外,文件的写入,一般需要借助 webpack-sources

3.2 开始敲代码,开发压缩资源为zip包插件

3.2.1 准备知识:Node.js里面如何将文件压缩成zip包?

我们可以使用 jszip 进行压缩生成zip包。

3.2.2 初始化插件文件

新建 zipPlugin.js 文件,并参考官方文档中插件的基本结构,初始化插件代码:

class ZipPugin{
  // 接受插件传递的参数
  constructor(options){
      this.options = options
  }
  // 插件上提供apply方法,webpack提供compiler对象
  apply(compiler){
    
  }
}
module.exports = ZipPugin

3.2.3 选择插件触发时机

选择插件触发时机,其实是选择插件触发的 compiler 钩子(即何时触发插件)。 Webpack 提供钩子有很多,这里简单介绍几个,完整具体可参考官方文档“Compiler Hooks”:

  • entryOption : 在 webpack 选项中的 entry 配置项 处理过之后,执行插件。
  • afterPlugins : 设置完初始插件之后,执行插件。
  • compilation : 编译创建之后,生成文件之前,执行插件。
  • emit : 生成资源到 output 目录之前。
  • done : 编译完成。 我们压缩资源为zip包的插件需要在资源生成之前处理,所以选择了 emit 钩子。

3.2.4 选择触发钩子的方法

compiler.hooks 下指定事件钩子函数,便会触发钩子时,执行回调函数。
Webpack 提供三种触发钩子的方法:

  • tap :以同步方式触发钩子;
  • tapAsync :以异步方式触发钩子;
  • tapPromise :以异步方式触发钩子,返回 Promise; 因为上面选择了 emit 钩子是异步的,因此选择tapAsync 方式触发钩子。

3.2.5 编写插件逻辑

实现插件功能的逻辑代码:

// zipPlugin.js
const JSZip = require('jszip');
const path = require('path');
const RawSource = require('webpack-sources').RawSource;
const zip = new JSZip();

module.exports = class ZipPlugin {
    constructor(options) {
        this.options = options;
    }

    apply(compiler) {
        compiler.hooks.emit.tapAsync('ZipPlugin', (compilation, callback) => {
            // 创建目录名称
            const folder = zip.folder(this.options.filename);
            // 遍历compilation.assets对象
            for (let filename in compilation.assets) {
                // 获取source
                const source = compilation.assets[filename].source();
                // 将source添加到folder中
                folder.file(filename, source);
            }

            // 将内容生成zip
            zip.generateAsync({
                type: 'nodebuffer'
            }).then((content) => {
                // 获取output(绝对路径)
                const outputPath = path.join(
                    compilation.options.output.path, 
                    this.options.filename + '.zip'
                );
                const outputRelativePath = path.relative(
                    compilation.options.output.path,
                    outputPath
                );
                // 将内容挂载到compilation.assets上,并将buffer转换为source
                compilation.assets[outputRelativePath] = new RawSource(content);

                callback();
            });
        });
    }
}

使用插件的方式:

// webpack.config.js
const path = require('path');
const ZipPlugin = require('./plugins/zipPlugin');

module.exports = {
  entry: './src/main.js',
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'main.js'
  },
  mode: 'production',
  plugins: [
    new ZipPlugin({
      filename: 'offline'
    })
  ]
}

四、总结思考:

我们通过一个简单插件的开发过程,了解了webpack插件机制的工作原理,总结下来就是一句话:Webpack 为每一个工作环节都预留了合适的钩子,我们在扩展时只需要找到合适的时机去做合适的事情就可以了。

本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情