如何编写一个webpack Plugin

798 阅读5分钟

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

背景

学习前端三个月了,准备刷刷面试题,总结总结,一天几道面试题,向大厂进军。

这次我们来学习如何编写一个webpack plugin。

Plugin 的作用

通过插件我们可以扩展webpack,加入自定义的构建行为,使webpack 可以执行更广泛的任务,拥有更强的构建能力。

plugin 的工作原理:

webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。webpack 通过 Tapable 来组织这条复杂的生产线。 webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。——「深入浅出 Webpack」

通俗的话就是: webpack 在编译代码过程中,会触发一系列 Tapable 钩子事件,插件所做的就是找到相应的钩子,往上面挂上自己的任务,也就是注册事件,这样,当 webpack 构建的时候,插件注册的事件就会随着钩子的触发而执行了。

Plugin 的基本结构

  1. 一个命名的javascript方法或者javascript类
  2. 它的原型上需要定义一个叫做apply的方法
  3. 注册一个事件钩子
  4. 操作webpack内部实例特性的数据
  5. 功能完成后,调用webpack提供回调
 /**
 * 
 * 
 * webpack 插件基本结构 类
 * @class CustomPlugin
 */
class CustomPlugin {

    constructor (options) {
    	console.log("CustomPlugin==constructor==",options);
    }
   
    apply(compiler) {
        const pluginName=this.constructor.name;
        compiler.hooks.done.tap(pluginName, (stats) => {
            console.log('CustomPlugin=生效了');
          });
    }
}

module.exports = CustomPlugin;

或者:


function CustomPlugin (options) {
    this.options = {
      ...options,
    };
  }
  
  CustomPlugin.prototype.apply = function (compiler) {
        const pluginName=this.constructor.name;
        compiler.hooks.done.tap(pluginName, (stats) => {
            console.log('CustomPlugin=生效了');
        });

 }
module.exports = CustomPlugin;

webpack 内部执行流程

image.png

一次完整的的webpack 打包大致流程:

  1. 将命令行参数与 webpack 配置文件 合并、解析得到参数对象。
  2. 参数对象传给 webpack 执行得到 Compiler 对象
  3. 执行 Compiler 的 run方法开始编译。每次执行 run 编译都会生成一个 Compilation 对象。
  4. 触发 Compiler 的 make方法分析入口文件,调用 compilation 的 buildModule 方法创建主模块对象。
  5. 生成入口文件 AST(抽象语法树),通过 AST 分析和递归加载依赖模块。
  6. 所有模块分析完成后,执行 compilation 的 seal 方法对每个 chunk 进行整理、优化、封装。
  7. 最后执行 Compiler 的 emitAssets 方法把生成的文件输出到 output 的目录中。

我们从上面可以看出webpack打包主要依赖两个核心对象:

image.png

我们可以参考官方文档来了解这两大核心对象给我们提供的钩子函数: webpack Plugin

image.png

image.png

image.png

Compilation 是 Compiler 用来创建一次新的编译过程的模块。一个 Compilation 实例可以访问所有模块和它们的依赖。在一次编译阶段,模块被加载、封装、优化、分块、散列和还原。 Compilation 也继承了 Tapabl 并提供了很多生命周期钩子。

我们可以写代码来测试一下:

    
const path = require('path');

/**
 * 
 * 插件每次编译compilation 常用 hooks方法
 * ps:注意注册时机 
 * @class CustomPlugin2
 */
class CustomPlugin2 {

    constructor (options) {
    	console.log("CustomPlugin2==constructor==",options);
    }
   

    compilationHook(compilation){
        const pluginName=this.constructor.name; 
        compilation.hooks.buildModule.tap(
            pluginName,
            (module) => {
              module.useSourceMap = true;
              console.log("CustomPlugin2==apply==compilation.hooks.buildModule")
            }
          );
          compilation.hooks.succeedModule.tap(
            pluginName,
            (module) => {
              console.log("CustomPlugin2==apply==compilation.hooks.succeedModule")
            }
          );
          compilation.hooks.finishModules.tapAsync(
            pluginName,
            (module,cb) => {
              console.log("CustomPlugin2==apply==compilation.hooks.finishModules");
              cb();
            }
          );
          compilation.hooks.seal.tap(
            pluginName,
            (module) => {
              console.log("CustomPlugin2==apply==compilation.hooks.seal",module)
            }
          );
    }
    
    apply(compiler) {
        const pluginName=this.constructor.name;
        console.log("获取插件的名字=",pluginName);
        if(compiler.hooks){
             compiler.hooks.run.tapAsync(pluginName,(compilation,cb)=>{
                	console.log("CustomPlugin2==apply==compiler.hooks.run",cb);
               cb &&  cb();
            });
             compiler.hooks.compile.tap(pluginName,(compilationParams)=>{
                	console.log("CustomPlugin2==apply==compiler.hooks.compile");
            });
             compiler.hooks.compilation.tap(pluginName,(compilation,compilationParams)=>{
                	console.log("CustomPlugin2==apply==compiler.hooks.compilation");
                    //注意注册事件时机。
                    this.compilationHook(compilation);
            });
             compiler.hooks.make.tapAsync(pluginName,(compilation,cb)=>{
                	console.log("CustomPlugin2==apply==compiler.hooks.make");
                cb();
            });
             compiler.hooks.emit.tapAsync(pluginName,(compilation,cb)=>{
                	console.log("CustomPlugin2==apply==compiler.hooks.emit");
                    //this.compilationHook(compilation);
                    cb();
            });
            compiler.hooks.afterEmit.tapAsync(pluginName,(compilation,cb)=>{
                	console.log("CustomPlugin2==apply==compiler.hooks.afterEmit");
                    cb();
            });
            compiler.hooks.assetEmitted.tapAsync(pluginName,(file,info,cb)=>{
                	console.log("CustomPlugin2==apply==compiler.hooks.assetEmitted",cb);
                    cb();
            });
            compiler.hooks.done.tapAsync(pluginName,(stats,cb)=>{
                	console.log("CustomPlugin2==apply==compiler.hooks.done");
                cb();
            });
        }
    }
  }
  
  module.exports = CustomPlugin2;

image.png

写一个读取lottie.json 动画资源,提取出动画中的所有图片,并生成文件。

这里我们编写通用webpack插件的时候,一定要考虑webpack 的版本,要考虑好兼容。下面这个案例就考虑到了一定的兼容。

 
const fs = require('fs');
const request = require('request');
const path = require('path');
const webpack = require("webpack");

/**
 * 需求:自定义组件,lottie资源提取
 * 兼容
 */

/**
 * 
 * 
 * @class CustomPlugin6
 */
class CustomPlugin6 {


    constructor(options) {
        //1:获取 lottie配置文件路径
        this.configPath = options && options.configPath;
        //2:获取输出文件名称
        console.log("CustomPlugin6==constructor==", options);
        this.outFileName = options && options.outFileName ? options.outFileName : "lottie-assets.js";

        this.globalName = options && options.globalName ? options.globalName : "window._config";
    }


    compilationHook(compilation) {
        const pluginName = this.constructor.name;
        if(compilation.hooks.processAssets){
            //compilation.emitAsset(name, new webpack.sources.RawSource(html, false));
           // 添加资源
            compilation.hooks.processAssets.tapAsync({ name: pluginName, stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS }, async (assets, cb) => {
                if (this.configPath) {
                    await this.readJsonFile(this.configPath, assets);
                    cb();
                } else {
                    cb();
                }
            });
        }else if(compilation.hooks.additionalAssets){
            compilation.hooks.additionalAssets.tapAsync( pluginName,  async (cb) => {
                if (this.configPath) {
                    await this.readJsonFile(this.configPath, compilation.assets);
                    cb();
                } else {
                    cb();
                }
            });
        }else{
            //throw new Error("请升级webpack版本>=4");
            compilation.errors.push("请升级webpack版本>=4");
        } 
    }

    apply(compiler) {
        const pluginName = this.constructor.name;
        console.log("获取插件的名字=", pluginName);
        if (compiler.hooks) {
            // Webpack 4+ Plugin System
            compiler.hooks.compilation.tap(pluginName, (compilation, compilationParams) => {
                console.log("CustomPlugin6==apply==compiler.hooks.compilation");
                //注意注册事件时机。
                this.compilationHook(compilation);
            });
          
        }else{
            compilation.errors.push("请升级webpack版本>=4"); 
        }
    }



    /**
     * 
     * 
     * 读取配置文件,生成js文件。
     * @memberOf LottieExtractAssetsPlugin
     */
    readJsonFile = async (assetPath, assets, cb) => {
        //获取配置
        let lottieConfig = await new Promise((resolve, reject) => {
            try {
                //读取配置文件
                fs.readFile(assetPath, (err, data) => {
                    if (err) {
                        reject(err);
                    } else {
                        let curData = data.toString();
                        const config = JSON.parse(curData);
                        resolve(config);
                    }
                });
            } catch (e) {
                reject(e);
            }
        });
        //根据配置获取资源链接(包含当前的lottie和lottie中图片)
        const imgLink = await this.getLink(lottieConfig);
        // 采用js文件,方便我们前端代码集成使用。
        let content = this.globalName + " = " + JSON.stringify(imgLink, null, 4) + ";";
        const assetsInfo = {
            // 写入新文件的内容
            source: function () {
                return content;
            },
            // 新文件大小(给 webapck 输出展示用)
            size: function () {
                return content.length;
            }
        }
        assets[this.outFileName]= assetsInfo;
    }

    /**
    * 
    * 
    * 获取lottie 资源地址。
    * @memberOf LottieExtractAssetsPlugin
    */
    getLink = async (lottieConfig) => {
        let imgArray = [];
        if (lottieConfig) {
            for (let i = 0; i < lottieConfig.length; i++) {
                const url = lottieConfig[i];
                //添加lottie json
                this.addLottieInfo(url, imgArray);
                //请求lottie json文件,获取图片资源
                const result = await this.requestLottie(lottieConfig[i]);
                imgArray.push(...result);
            }
        }
        return imgArray;
    }


    /**
     * 
     * 
     * 添加lottie json 文件
     * @memberOf 
     */
    addLottieInfo = (url, imgArr) => {
        const info = this.getLottieInfo(url);
        imgArr.push({
            key: info.name,
            url: url,
        })
    }

    /**
    * 
    * 根据url获取lottie信息,方便生成配置文件。
    * @memberOf 
    */
    getLottieInfo = (url) => {
        const lastIndex = url.lastIndexOf("/");
        const curUrlPre = url.substring(0, lastIndex);
        const nameLastIndex = curUrlPre.lastIndexOf("/");
        return { url: curUrlPre, name: curUrlPre.substring(nameLastIndex + 1, nameLastIndex.length) }
    }


    /**
   * 
   * 
   * 请求lottie json文件
   * @memberOf LottieExtractAssetsPlugin
   */
    requestLottie = (url) => {
        return new Promise((resolve, reject) => {
            request(url, (error, response, body) => {
                if (!error && response.statusCode == 200) {
                    try {
                        const lottieData = JSON.parse(body);
                        const result = this.lottieParse(lottieData, url);
                        resolve(result);
                    } catch (e) {
                        console.log(e);
                    }
                } else {
                    reject(url + "==失败");
                }
            })
        })

    }


    /**
     * 
     * 解析lottie
     * @memberOf 
     */
    lottieParse = (data, url) => {
        let urlArray = [];
        try {
            const assets = data.assets;
            const lottieInfo = this.getLottieInfo(url);
            for (let i = 0; i < assets.length; i++) {
                const item = assets[i];
                if (item.p && item.u) {
                    const imgUrl = `${lottieInfo.url}/${item.u}${item.p}`;
                    urlArray.push({
                        key: `${lottieInfo.name}_${item.p}`,
                        url: imgUrl,
                        source: url,
                        lottieName: lottieInfo.name
                    });
                }
            }
        } catch (e) {
            console.log(e);
        }
        return urlArray;
    }
}

module.exports = CustomPlugin6;

webpack 使用该插件:

image.png

结语

一步一步慢慢来,踏踏实实把活干!