面试官:你写过webpack插件吗?

2,795 阅读5分钟

前言

本文介绍了webpack插件的组成与如何编写,然后会实现一个将打包好的静态资源上传至七牛的插件

关于插件

webpack中,pluginloader的作用更加广泛,plugin可以管理资源,注入全局变量,压缩代码等。

插件的组成:

  1. 一个js函数或者js
  2. 在插件函数的prototype上定义一个apply方法
  3. 绑定webpack的事件钩子
  4. 在钩子函数中处理webpack中的数据
  5. 处理数据完成后,需要调用webpack提供的回调
// 1.一个js函数或者js类
class MyPlugin {
    // 2.在插件函数的prototype上定义一个apply方法
    apply(compiler) {
        // 3.绑定webpack的事件钩子
        compiler.hooks.emit.tapAsync(
            'MyPlugin', 
            (compilation, callback) => {
                // 4.在钩子函数中处理webpack中的数据
                console.log('处理webpack中的数据');
                
                // 5.处理数据完成后,需要调用webpack提供的回调
                callback();
            }
        );
    }
}

在插件开发中最重要的两个资源就是compiler 和 compilation 对象。

Compiler

Compiler模块是webpack的主要引擎,它扩展自Tapable类,用来注册和调用插件。大多数面向用户的插件都会先在Compiler上注册。

compiler会存在于webpack的整个生命周期,直到node进程关闭。

Compilation

Compilation模块会被Compiler使用配置数据创建一个compilation实例。compilation实例可以访问所有的模块和他们的依赖。

Compilation也扩展自Tapable类,提供了编译过程中的生命周期钩子。

compilation实例在每次代码更新或者是打包时都会重新创建。

Webpack中的钩子函数

我们编写插件前,需要知道我们插件是在什么时候去被调用。这个就需要了解webpack中提供的钩子函数有哪些:

  1. compiler.hooks.compilation:启动编译创建出 compilation 对象后触发
  2. compiler.hooks.make:正式开始编译时触发
  3. compiler.hooks.emit:输出资源到output目录前执行
  4. compiler.hooks.afterEmit:输出资源到output目录后执行
  5. compiler.hooks.done:编译完成后触发

这里列举了一小部分,想要看完整的请查看官网

自定义插件

清楚了如何编写插件,现在我们就来自定义一个将打包好的静态资源上传至七牛的插件。首先我们创建一个webpack项目,不知道如何创建的同学请看这篇文章:《Webpack5,了解从0到1搭建一个项目的细节》

创建插件

然后在根目录下创建qiniu-s-webpack-plugin.js,然后添加代码:

const PLUGIN_NAME = 'qiniu-s-webpack-plugin';

class QiuniuPlugin {
    apply(compiler) {
        console.log('执行到我啦!!!');
    }
}

module.exports = QiuniuPlugin;

到这里我们可以将插件先引入,试一下效果:

const QiniuWebpackPlugin = require('../qiniu-s-webpack-plugin');

module.exports = {
    ...
    plugins: [
        new QiniuWebpackPlugin(),
    ]
}

然后运行一下,可以发现命令行中打输出了plugin中打印的log。

绑定钩子函数

这里我们需要考虑需要绑定什么钩子函数。我们的插件的作用是在打包完成后,将静态资源上传至七牛,在上面我们列举了几个钩子函数,其中有两个符合我们的需求:

  1. compiler.hooks.afterEmit:输出资源到output目录后执行
  2. compiler.hooks.done:编译完成后触发

这两个钩子函数都可以实现我们的需求,但是我们需要拿到输出的文件信息,在done中无法拿到输出的文件信息,所以在本插件中使用了afterEmit钩子函数。

const PLUGIN_NAME = 'qiniu-s-webpack-plugin';

class QiuniuPlugin {
    apply(compiler) {
        compiler.hooks.afterEmit.tapAsync(
            PLUGIN_NAME, 
            async (compilation, callback) => {
              const fileNameAry = Object.keys(compilation.assets);
              const buildPath = compiler.options.output.path;

              callback();
            }
        );
    }
}

module.exports = QiuniuPlugin;

这里我们通过compilation.assets拿到输出后的文件信息:

{
  'main-e6f758da.js': SizeOnlySource { _size: 3873 },
  'image/icon.d144096d.jpeg': SizeOnlySource { _size: 77904 },
  'main-e6f758da.js.map': SizeOnlySource { _size: 2679 },
  'index.html': SizeOnlySource { _size: 456 }
}

可以看到输出一个对象,对象的key是我们的文件路径,然后我们通过Object.keys将key提取出来。

[
    'main-e6f758da.js',
    'image/icon.d144096d.jpeg',
    'main-e6f758da.js.map',
    'index.html'
]

我们要上传一个文件,那么必须获取到这个文件的绝对路径。在webpack的配置文件中我们配置了output.path属性,是我们打包输出目录的绝对路径。我们通过compilation.options.output.path获取到path属性。

path = 'user/xx/xx/project/dist'

然后我们将两者进行拼接,就可以获取到完整的文件路径,为后续上传做准备:

class QiuniuPlugin {
    apply(compiler) {
        compiler.hooks.afterEmit.tapAsync(
            PLUGIN_NAME, 
            async (compilation, callback) => {
              const fileNameAry = Object.keys(compilation.assets);
              const buildPath = compiler.options.output.path;
              const filePathAry = fileNameAry.map((filename) => `${buildPath}/${filename}`);
              
              callback();
            }
        );
    }
}

上传至七牛

七牛官方提供了nodejs端上传的npm包,直接安装:

npm install qiniu

然后我们创建一个新的文件qiniu.js,用于封装七牛的上传方法:

const qiniu = require('qiniu');

class Qiniu {
  options = {
    accessKey: '', 
    secretKey: '',
    bucket: '',
  };

  constructor(options) {
    const {accessKey, secretKey, bucket} = options;
    const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
    const _options = {
      scope: bucket,
    };
    const putPolicy = new qiniu.rs.PutPolicy(_options);
    const config = new qiniu.conf.Config();
    this.options = options;
    this.uploadToken=putPolicy.uploadToken(mac);
    this.formUploader = new qiniu.form_up.FormUploader(config);
  }

  putFile(filePath) {
    const putExtra = new qiniu.form_up.PutExtra();

    return new Promise((resolve, reject) => {
      this.formUploader.putFile(this.uploadToken, null, filePath, putExtra, function (respErr,
        respBody, respInfo) {
        if (respErr) {
          throw respErr;
        }
        if (respInfo.statusCode == 200) {
          resolve();
        } else {
          console.log(respInfo.statusCode);
          console.log(respBody);
          reject(respBody);
        }
      });
    });
  }
 
}

module.exports = Qiniu;

七牛操作文件必须上传三个参数:

  1. accessKey:在个人中心 -> 密钥管理中获取
  2. secretKey:在个人中心 -> 密钥管理中获取
  3. bucket:空间名称

然后我们在插件的构造函数中要将这三个参数传入,通过实例化Qiniu这个对象调用上传文件的方法:

const Qiniu = require('./qiniu');

class QiuniuPlugin {
    constructor(options) {
        this.qiniu = new Qiniu(options);
    }
  
    apply(compiler) {
        compiler.hooks.afterEmit.tapAsync(
            PLUGIN_NAME, 
            async (compilation, callback) => {
              const fileNameAry = Object.keys(compilation.assets);
              const buildPath = compiler.options.output.path;
              const filePathAry = fileNameAry.map((filename) => `${buildPath}/${filename}`);
              
              //上传文件
              filePathAry.forEach(async (filePath) => {
                  await this.qiniu.putFile(filePath);
              });
              
              callback();
            }
        );
    }
}

由于七牛没有提供批量上传的方法,所以我们这里通过遍历的方式一个个将文件上传。

到这里插件就完成了。

总结

webpack插件本身是一个构造函数,需要在函数内定义apply方法。在webpack初始化时,会调用插件的apply方法,传入compiler对象,插件需要通过compiler对象注册钩子函数。webpack在编译的不同阶段会调用相应的钩子函数,从而调用插件去处理webpack的内部数据,在处理过程中可以访问compilation对象,compilation对象中可以访问所有的模块和它们的依赖,以及配置的属性。在处理完数据后,需要调用webpack提供的回调函数,让剩下的钩子函数继续执行。

点击这里查看源码

关于webpack其它文章

不了解Tapable?那你咋写的Webpack插件

Webpack5,了解从0到1搭建一个项目的细节

来吧!一起肝一个CLI工具