webpack5 打包原理

webpack 版本: ^5.24.2

单文件打包

  • 首先,我们新建项目,安装 webpack 依赖
npm init -y
npm i webpack webpack-cli -D
  • 简单编写一个 js 文件,并打包 dev 环境
  • 紧接着,我们看打包后的文件

    Development:

(() => { // webpackBootstrap
  var __webpack_modules__ = ({
    "./src/index.js":
      (() => {
        eval("console.log('duanxl.com')\n\n//# sourceURL=webpack://webpack5-demo/./src/index.js?");
      })
  });
  var __webpack_exports__ = {};
  __webpack_modules__["./src/index.js"]();
})();
  • 因为 scope hositing 的原因,所以 Production 并没有闭包,有声明变量,才会打成闭包
    Production:

console.log('duanxl.com')
  • 编译流程
    • 打包成了自执行函数,闭包,保证变量不被外部修改
    • 函数内部声明一个webpack_modules对象,并已文件名作为 key,函数体作为 value,函数内部,直接使用 eval 来执行了内容
    • 通过 key 取到对象的 value,并执行
  • 区别对比
    • 1.函数换为箭头函数
    • 2.在没有 module 及其他文件引用时,忽略内部 commonjs 规范的实现以及模块的缓存
    • 3.相比于 webpack4 的 dev 设计,体积减少的要多
    • 4.相比于 webpack4,webpack_modules写到了内部,不再以闭包传参

module 打包

简单写入代码

// sync.js
const num = 99;
export default num;
// index.js
import num from './sync'
console.log(num + 1)

Dev 打包后

(() => { // webpackBootstrap
    "use strict";
    var __webpack_modules__ = ({

        "./src/index.js":
            ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
                eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _sync__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./sync */ \"./src/sync.js\");\n\n\nconsole.log(_sync__WEBPACK_IMPORTED_MODULE_0__.default + 1)\n\n//# sourceURL=webpack://webpack5-demo/./src/index.js?");
            }),

        "./src/sync.js":
            ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
                eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\nconst num = 99;\n\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (num);\n\n//# sourceURL=webpack://webpack5-demo/./src/sync.js?");
            })

    });
    // The module cache
    var __webpack_module_cache__ = {};
    // The require function
    function __webpack_require__(moduleId) {
        // Check if module is in cache
        if (__webpack_module_cache__[moduleId]) {
            return __webpack_module_cache__[moduleId].exports;
        }
        var module = __webpack_module_cache__[moduleId] = {
            // no module.id needed
            // no module.loaded needed
            exports: {}
        };
        // Execute the module function
        __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
        // Return the exports of the module
        return module.exports;
    }

    (() => {
        // define getter functions for harmony exports
        __webpack_require__.d = (exports, definition) => {
            for (var key in definition) {
                if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
                    Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
                }
            }
        };
    })();

    (() => {
        __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
    })();

    (() => {
        // define __esModule on exports
        __webpack_require__.r = (exports) => {
            if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
                Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
            }
            Object.defineProperty(exports, '__esModule', { value: true });
        };
    })();

    var __webpack_exports__ = __webpack_require__("./src/index.js");

})();
  • 流程: - 首先,一进来,定义了一些变量,开始执行函数,函数内部有几个自执行函数,分别定义了webpack_require.d、webpack_require.o、webpack_require.r 函数,下面再说定义的这些函数做了什么 - 判断webpack_module_cache是否存在 moduleId,存在即返回,不存在新建对象 - 接着开始执行webpack_require,并以入口文名作为参数执行 - webpack_require函数内部,可以看到,判断webpack_module_cache是否存在 moduleId(即文件名),不存在,直接赋值,并添加了 exports 空对象,接着,开始执行文件对象(即 module 对象,内部存储了文件名做未 key,函数作为 value 的对象),最后,return module.exports;module.exports(即webpack_module_cache中对应的) - 接着,我们看一下接下来执行了什么

    ```
    // __unused_webpack_module : { exports: {}}
    // __webpack_exports__ : {}
    // __webpack_require__ : 函数自身
    ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
                eval(`__webpack_require__.r(__webpack_exports__);
       var _sync__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/sync.js");
       console.log(_sync__WEBPACK_IMPORTED_MODULE_0__.default + 1)`);
    })
    ```
    
    > 传入空对象还有对象属性干了什么,接着看
    
    ```
    // exports : {}
    __webpack_require__.r = (exports) => {
            if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
                Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
            }
            Object.defineProperty(exports, '__esModule', { value: true });
        };
    // 可以看到,最终创建了一个新的类型标签对象
    // Symbol.toStringTag  用于标记该对象的自定义类型标签
    // 可以看到,目前的module "./src/index.js":{exports:Module {Symbol(Symbol.toStringTag): "Module"}}
    ```
    
    > 可以看到,把 module 入口对象中的 exports 改成了空的类型标签对象,先不管,紧接着看是如何调用了另一个 module 对象 __webpack_require__("./src/sync.js")
    
    ```
    // "./src/sync.js":
    ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
            eval(`__webpack_require__.r(__webpack_exports__);
            __webpack_require__.d(__webpack_exports__,
            "default": () => (__WEBPACK_DEFAULT_EXPORT__)});
            const num = 99;
            const __WEBPACK_DEFAULT_EXPORT__ = (num);`);
    })
    ```
    
    - 和上面一样,把 module 入口对象中的 exports 改成了空的类型标签对象
    - 执行了__webpack_require__.d
    ```
        // 可以看到,挂载属性至module对象上,并添加了get方法
        __webpack_require__.d = (exports, definition) => {
            for (var key in definition) {
                if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
                    Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
                }
            }
        };
    ```
    ```
         // 这里不解,为啥不直接用obj.hasOwnProperty(prop),大佬的世界我还没理解
        (() => {
        __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
        })();
    ```
        - 再看接下来的声明变量,以及打印,就可以理解是如何得到的值了
    - > 可以看到,对于module,相比于webpack4,减少了commonjs
    的实现(需手动配置target),即传参以commonjs参数传入闭包,然后赋值到__webpack_require__等变量上
    

    Pro 打包后

(()=>{"use strict";console.log(100)})();

emmmmm….,就这一句话,对,你没有看错
这是因为 Webpack5 集成了 prepack,prepack 编译的代码比较激进,对于一些简单的值,会直接找到并进行运算,现在我们可以知道,为什么 Webpack5 的体积小了很多,当然,还可以更小,可以去看 prepack 官网,编写符合 prepack 编译优化的代码,这样既可以装 X,又可以优化项目~~~

prepack 地址 prepack.io

异步模块打包

编写简单异步

// async.js
const num = 99;
export default num;
// index.js
import('./async')
    .then(() => {
        console.log(num + 1)
    }).catch((e) => { console.log(e) })

Development:

  • 先看简单的,async.js 被打成了什么
// src_async_js.js
(self["webpackChunkwebpack5_demo"] = self["webpackChunkwebpack5_demo"] || []).push([["src_async_js"], {

/***/ "./src/async.js":
/*!**********************!*\
  !*** ./src/async.js ***!
  \**********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

      "use strict";
      eval(`
__webpack_require__.r(__webpack_exports__);
 __webpack_require__.d(__webpack_exports__, {  "default": () => (__WEBPACK_DEFAULT_EXPORT__)});
const num = 99;
const __WEBPACK_DEFAULT_EXPORT__ = (num);`);

      /***/
})

}]);

可以看到,异步模块打包后,是挂载在 window 属性上的二维数组,其中 eval 中的代码和 module 打包后的差不多,有一点区别,就是声明变量及赋值跑到了”./src/async.js”属性上。还是用了新的 api—globalThis

  • 接下来,看 index.js 是如何执行异步模块的
(() => { // webpackBootstrap
  var __webpack_modules__ = ({

    "./src/index.js":

      ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
        eval(`
        __webpack_require__.e("src_async_js")
        .then(__webpack_require__.bind(__webpack_require__,"./src/async.js"))
        .then(() => {console.log(num + 1)})
        .catch((e) => { console.log(e) })`);
      })

  });
  // The module cache
  var __webpack_module_cache__ = {};
  // The require function
  function __webpack_require__(moduleId) {
    // Check if module is in cache
    if (__webpack_module_cache__[moduleId]) {
      return __webpack_module_cache__[moduleId].exports;
    }
    // Create a new module (and put it into the cache)
    var module = __webpack_module_cache__[moduleId] = {
      // no module.id needed
      // no module.loaded needed
      exports: {}
    };
    // Execute the module function
    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
    // Return the exports of the module
    return module.exports;
  }

  // expose the modules object (__webpack_modules__)
  __webpack_require__.m = __webpack_modules__;

  /* webpack/runtime/define property getters */
  (() => {
    // define getter functions for harmony exports
    __webpack_require__.d = (exports, definition) => {
      for (var key in definition) {
        if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
          Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
        }
      }
    };
  })();

  /* webpack/runtime/ensure chunk */
  (() => {
    __webpack_require__.f = {};
    // This file contains only the entry chunk.
    // The chunk loading function for additional chunks
    __webpack_require__.e = (chunkId) => {
      return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
        __webpack_require__.f[key](chunkId, promises);
        return promises;
      }, []));
    };
  })();

  /* webpack/runtime/get javascript chunk filename */
  (() => {
    // This function allow to reference async chunks
    __webpack_require__.u = (chunkId) => {
      // return url for filenames based on template
      return "" + chunkId + ".js";
    };
  })();

  /* webpack/runtime/global */
  (() => {
    __webpack_require__.g = (function () {
      if (typeof globalThis === 'object') return globalThis;
      try {
        return this || new Function('return this')();
      } catch (e) {
        if (typeof window === 'object') return window;
      }
    })();
  })();

  /* webpack/runtime/hasOwnProperty shorthand */
  (() => {
    __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
  })();

  /* webpack/runtime/load script */
  (() => {
    var inProgress = {};
    var dataWebpackPrefix = "webpack5-demo:";
    // loadScript function to load a script via script tag
    __webpack_require__.l = (url, done, key, chunkId) => {
      if (inProgress[url]) { inProgress[url].push(done); return; }
      var script, needAttach;
      if (key !== undefined) {
        var scripts = document.getElementsByTagName("script");
        for (var i = 0; i < scripts.length; i++) {
          var s = scripts[i];
          if (s.getAttribute("src") == url || s.getAttribute("data-webpack") == dataWebpackPrefix + key) { script = s; break; }
        }
      }
      if (!script) {
        needAttach = true;
        script = document.createElement('script');

        script.charset = 'utf-8';
        script.timeout = 120;
        if (__webpack_require__.nc) {
          script.setAttribute("nonce", __webpack_require__.nc);
        }
        script.setAttribute("data-webpack", dataWebpackPrefix + key);
        script.src = url;
      }
      inProgress[url] = [done];
      var onScriptComplete = (prev, event) => {
        // avoid mem leaks in IE.
        script.onerror = script.onload = null;
        clearTimeout(timeout);
        var doneFns = inProgress[url];
        delete inProgress[url];
        script.parentNode && script.parentNode.removeChild(script);
        doneFns && doneFns.forEach((fn) => (fn(event)));
        if (prev) return prev(event);
      }
        ;
      var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);
      script.onerror = onScriptComplete.bind(null, script.onerror);
      script.onload = onScriptComplete.bind(null, script.onload);
      needAttach && document.head.appendChild(script);
    };
  })();

  /* webpack/runtime/make namespace object */
  (() => {
    // define __esModule on exports
    __webpack_require__.r = (exports) => {
      if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
        Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
      }
      Object.defineProperty(exports, '__esModule', { value: true });
    };
  })();

  /* webpack/runtime/publicPath */
  (() => {
    var scriptUrl;
    if (__webpack_require__.g.importScripts) scriptUrl = __webpack_require__.g.location + "";
    var document = __webpack_require__.g.document;
    if (!scriptUrl && document) {
      if (document.currentScript)
        scriptUrl = document.currentScript.src
      if (!scriptUrl) {
        var scripts = document.getElementsByTagName("script");
        if (scripts.length) scriptUrl = scripts[scripts.length - 1].src
      }
    }
    // When supporting browsers where an automatic publicPath is not supported you must specify an output.publicPath manually via configuration
    // or pass an empty string ("") and set the __webpack_public_path__ variable from your code to use your own logic.
    if (!scriptUrl) throw new Error("Automatic publicPath is not supported in this browser");
    scriptUrl = scriptUrl.replace(/#.*$/, "").replace(/\?.*$/, "").replace(/\/[^\/]+$/, "/");
    __webpack_require__.p = scriptUrl;
  })();

  /* webpack/runtime/jsonp chunk loading */
  (() => {
    // no baseURI

    // object to store loaded and loading chunks
    // undefined = chunk not loaded, null = chunk preloaded/prefetched
    // Promise = chunk loading, 0 = chunk loaded
    var installedChunks = {
      "main": 0
    };


    __webpack_require__.f.j = (chunkId, promises) => {
      // JSONP chunk loading for javascript
      var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;
      if (installedChunkData !== 0) { // 0 means "already installed".

        // a Promise means "currently loading".
        if (installedChunkData) {
          promises.push(installedChunkData[2]);
        } else {
          if (true) { // all chunks have JS
            // setup Promise in chunk cache
            var promise = new Promise((resolve, reject) => {
              installedChunkData = installedChunks[chunkId] = [resolve, reject];
            });
            promises.push(installedChunkData[2] = promise);

            // start chunk loading
            var url = __webpack_require__.p + __webpack_require__.u(chunkId);
            // create error before stack unwound to get useful stacktrace later
            var error = new Error();
            var loadingEnded = (event) => {
              if (__webpack_require__.o(installedChunks, chunkId)) {
                installedChunkData = installedChunks[chunkId];
                if (installedChunkData !== 0) installedChunks[chunkId] = undefined;
                if (installedChunkData) {
                  var errorType = event && (event.type === 'load' ? 'missing' : event.type);
                  var realSrc = event && event.target && event.target.src;
                  error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
                  error.name = 'ChunkLoadError';
                  error.type = errorType;
                  error.request = realSrc;
                  installedChunkData[1](error);
                }
              }
            };
            __webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId);
          } else installedChunks[chunkId] = 0;
        }
      }
    };

    // no prefetching

    // no preloaded

    // no HMR

    // no HMR manifest

    // no deferred startup

    // install a JSONP callback for chunk loading
    var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
      var [chunkIds, moreModules, runtime] = data;
      // add "moreModules" to the modules object,
      // then flag all "chunkIds" as loaded and fire callback
      var moduleId, chunkId, i = 0, resolves = [];
      for (; i < chunkIds.length; i++) {
        chunkId = chunkIds[i];
        if (__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
          resolves.push(installedChunks[chunkId][0]);
        }
        installedChunks[chunkId] = 0;
      }
      for (moduleId in moreModules) {
        if (__webpack_require__.o(moreModules, moduleId)) {
          __webpack_require__.m[moduleId] = moreModules[moduleId];
        }
      }
      if (runtime) runtime(__webpack_require__);
      if (parentChunkLoadingFunction) parentChunkLoadingFunction(data);
      while (resolves.length) {
        resolves.shift()();
      }

    }

    var chunkLoadingGlobal = self["webpackChunkwebpack5_demo"] = self["webpackChunkwebpack5_demo"] || [];
    chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
    chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));

    // no deferred startup
  })();

  /************************************************************************/

  // startup
  // Load entry module and return exports
  // This entry module can't be inlined because the eval devtool is used.
  var __webpack_exports__ = __webpack_require__("./src/index.js");

})();
    • 先不管闭包中的自执行函数(即变量赋值),可以看到,和之前一样,先执行__webpack_require__,和module流程也一样,不一样的,就是webpack_modules__这个模块对象,里面执行了__webpack_require.e

      var __webpack_modules__ = ({
      
      "./src/index.js":
      
        ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
          eval(`
          __webpack_require__.e("src_async_js")
          .then(__webpack_require__.bind(__webpack_require__,"./src/async.js"))
          .then(() => {console.log(num + 1)})
          .catch((e) => { console.log(e) })`);
        })
      

    });

    
    > Production:
    > 生产环境就不看了,和 dev 出入不大,只是代码进行了压缩,变量名变得精简,为了更小的内存空间
    

Webpack Loader

loader 是 webpack 的核心之一,loader 可以理解为一个导出的函数,是对模块源代码进行转换
下面先看一个简单的 loader,实现 const 转换为 var

'use strict';
const loaderUtils = require('loader-utils');
const acorn = require('acorn');
const walk = require('acorn-walk');
const MagicString = require('magic-string');
module.exports = function (content) {
    const options = loaderUtils.getOptions(this);
    console.log('前置钩子', this.data.value);
    console.log('🍌配置文件', options)
    const ast = acorn.parse(content, { ecmaVersion: 2020 });
    const code = new MagicString(content);
    walk.simple(ast, {

        VariableDeclarator(node) {
            console.log('节点', node);
            const { start } = node;
            code.overwrite(start, start + 5, 'var')
        }
    })
    return code.toString();

};
module.exports.pitch = function (r, preRequest, data) {
    // r:loader链中排在后面的loader以及资源文件路径组成的字符串
    // preRequest:loader链中排在前面的loader以及资源文件路径组成的字符串
    // data:每个loader中存放在上下文中的数据,可用于pitch传递给loader数据
    data.value = "段鑫磊";
}
  • Rule.enforce,配置 loader 的类型,下面也是几种 loader 的优先级即类型
    • 前置 loader
    • 普通 loader
    • 行内 loader
    • 后置 loader
  • 异步 loader

    使用 this.async 来获取 callback 函数

  • 内联 loader

    通过配置 raw,loader 可以接收 Buffer

  • pitch loader

    会阻断 loader,即在 loader 运行前会触发,在执行 loader 之前,会按照顺序调用 loader 上的 pitch 方法,而 loader 的执行顺序是从右向左,也可以理解成从下至上

  • 手写 loader 必备:
    • loader-utils:获取 loader 配置项以及处理文件名等
    • acorn: 源文件的解析器
    • acorn-walk:格式化 ast
    • magic-string:魔术字符串
    • schema-utils:校验 options 中的 json

Webpack Plugin

plugin,针对 loader 结束后,在 webpack 打包的过程中,基于事件机制,不直接操作文件,执行的任务

// plugin.js
const pluginName = 'TestPlugin';

class TestPlugin {
    apply(compiler) {
        compiler.hooks.run.tap(pluginName, compilation => {
            console.log('lalalal', compilation);
        })
    }
}

module.exports = TestPlugin;
// webpack.config.js
const TestPlugin = require('./plugin/plugin')
module.exports = {
    plugins: [
        new TestPlugin()
    ]
}

Webpack 源码流程

简单看一下 webpack 的流程,基于上面我们自己编写的 plugin,下面代码展示部分代码,用于清晰流程

  • 1.首先,我们看 webpack 的 package.json 的 bin,指向了 bin/webpack.js,即入口
"main": "lib/index.js",
  "bin": {
    "webpack": "bin/webpack.js"
  },
  • 2.bin/webpack.js 中检查 cli 是否被安装,执行 runCLI
runCommand(packageManager, installOptions.concat(cli.package))
            .then(() => {
                runCli(cli);
            })
            .catch(error => {
                console.error(error);
                process.exitCode = 1;
            });
  • 3.webpack/cli/bin/cli.js 中,检查 webpack 是否被安装,执行 runCLI
const { promptInstallation, logger, colors } = utils;
const runCLI = require('../lib/bootstrap');

    promptInstallation('webpack', () => {
        utils.logger.error(`It looks like ${colors.bold('webpack')} is not installed.`);
    })
        .then(() => {
            logger.success(`${colors.bold('webpack')} was installed successfully.`);

            runCLI(process.argv, originalModuleCompile);
        })
        .catch(() => {
            logger.error(`Action Interrupted, Please try once again or install ${colors.bold('webpack')} manually.`);

            process.exit(2);
        });
  • 4.webpack/cli/lib/bootstrap.js,进行容错及检查配置,执行 cli.run()
const WebpackCLI = require('./webpack-cli');
const utils = require('./utils');

const runCLI = async (args, originalModuleCompile) => {
    try {
        // Create a new instance of the CLI object
        const cli = new WebpackCLI();

        cli._originalModuleCompile = originalModuleCompile;

        await cli.run(args);
    } catch (error) {
        utils.logger.error(error);
        process.exit(2);
    }
};

module.exports = runCLI;
  • 5.run 方法中,配置了一些提示信息,执行 createCompiler
// 省略99%代码。。。
compiler = await this.createCompiler(options, callback);
  • 6.createCompiler 将配置 options 拿过去,执行 this.webpack,在 this.webpack 中,真正执行了 webpack.js
// 获取配置,this.webpack,才真正调用了webpack中的createCompiler
async createCompiler(options, callback) {
        this.applyNodeEnv(options);

        let config = await this.resolveConfig(options);

        config = await this.applyOptions(config, options);
        config = await this.applyCLIPlugin(config, options);

        let compiler;

        try {
            compiler = this.webpack(
                config.options,
                callback
                    ? (error, stats) => {
                          if (error && this.isValidationError(error)) {
                              this.logger.error(error.message);
                              process.exit(2);
                          }

                          callback(error, stats);
                      }
                    : callback,
            );
        } catch (error) {
            if (this.isValidationError(error)) {
                this.logger.error(error.message);
            } else {
                this.logger.error(error);
            }

            process.exit(2);
        }

        // TODO webpack@4 return Watching and MultiWatching instead Compiler and MultiCompiler, remove this after drop webpack@4
        if (compiler && compiler.compiler) {
            compiler = compiler.compiler;
        }

        return compiler;
    }
  • 7.createCompiler 中,实例化了 Compiler,并遍历了 options.plugin,然后执行了 apply 方法,并传入了实例化的 compiler,目前,可以解释得通上面手写 plugin 中 apply 中的 compiler 是如何来的
// webpack/lib/webpack.js
const createCompiler = rawOptions => {
    const options = getNormalizedWebpackOptions(rawOptions);
    applyWebpackOptionsBaseDefaults(options);
    const compiler = new Compiler(options.context);
    compiler.options = options;
    new NodeEnvironmentPlugin({
        infrastructureLogging: options.infrastructureLogging
    }).apply(compiler);
    if (Array.isArray(options.plugins)) {
        for (const plugin of options.plugins) {
            if (typeof plugin === "function") {
                plugin.call(compiler, compiler);
            } else {
                plugin.apply(compiler);
            }
        }
    }
    applyWebpackOptionsDefaults(options);
    compiler.hooks.environment.call();
    compiler.hooks.afterEnvironment.call();
    new WebpackOptionsApply().process(options, compiler);
    compiler.hooks.initialize.call();
    return compiler;
};
  • 8.接着看,compiler 上的 hooks、run、tap 是干什么的,可以想到,既然实例化了 Compiler,那么直接去看 Compiler
class Compiler {
    /**
     * @param {string} context the compilation path
     */
    constructor(context) {
        this.hooks = Object.freeze({
            /** @type {SyncHook<[]>} */
            initialize: new SyncHook([]),

            /** @type {SyncBailHook<[Compilation], boolean>} */
            shouldEmit: new SyncBailHook(["compilation"]),
            /** @type {AsyncSeriesHook<[Stats]>} */
            done: new AsyncSeriesHook(["stats"]),
            /** @type {SyncHook<[Stats]>} */
            afterDone: new SyncHook(["stats"]),
            /** @type {AsyncSeriesHook<[]>} */
            additionalPass: new AsyncSeriesHook([]),
            /** @type {AsyncSeriesHook<[Compiler]>} */
            beforeRun: new AsyncSeriesHook(["compiler"]),
            /** @type {AsyncSeriesHook<[Compiler]>} */
            run: new AsyncSeriesHook(["compiler"]),
            /** @type {AsyncSeriesHook<[Compilation]>} */
            emit: new AsyncSeriesHook(["compilation"]),
            /** @type {AsyncSeriesHook<[string, AssetEmittedInfo]>} */
            assetEmitted: new AsyncSeriesHook(["file", "info"]),
            /** @type {AsyncSeriesHook<[Compilation]>} */
            afterEmit: new AsyncSeriesHook(["compilation"]),

            /** @type {SyncHook<[Compilation, CompilationParams]>} */
            thisCompilation: new SyncHook(["compilation", "params"]),
            /** @type {SyncHook<[Compilation, CompilationParams]>} */
            compilation: new SyncHook(["compilation", "params"]),
            /** @type {SyncHook<[NormalModuleFactory]>} */
            normalModuleFactory: new SyncHook(["normalModuleFactory"]),
            /** @type {SyncHook<[ContextModuleFactory]>}  */
            contextModuleFactory: new SyncHook(["contextModuleFactory"]),

            /** @type {AsyncSeriesHook<[CompilationParams]>} */
            beforeCompile: new AsyncSeriesHook(["params"]),
            /** @type {SyncHook<[CompilationParams]>} */
            compile: new SyncHook(["params"]),
            /** @type {AsyncParallelHook<[Compilation]>} */
            make: new AsyncParallelHook(["compilation"]),
            /** @type {AsyncParallelHook<[Compilation]>} */
            finishMake: new AsyncSeriesHook(["compilation"]),
            /** @type {AsyncSeriesHook<[Compilation]>} */
            afterCompile: new AsyncSeriesHook(["compilation"]),

            /** @type {AsyncSeriesHook<[Compiler]>} */
            watchRun: new AsyncSeriesHook(["compiler"]),
            /** @type {SyncHook<[Error]>} */
            failed: new SyncHook(["error"]),
            /** @type {SyncHook<[string | null, number]>} */
            invalid: new SyncHook(["filename", "changeTime"]),
            /** @type {SyncHook<[]>} */
            watchClose: new SyncHook([]),
            /** @type {AsyncSeriesHook<[]>} */
            shutdown: new AsyncSeriesHook([]),

            /** @type {SyncBailHook<[string, string, any[]], true>} */
            infrastructureLog: new SyncBailHook(["origin", "type", "args"]),

            // TODO the following hooks are weirdly located here
            // TODO move them for webpack 5
            /** @type {SyncHook<[]>} */
            environment: new SyncHook([]),
            /** @type {SyncHook<[]>} */
            afterEnvironment: new SyncHook([]),
            /** @type {SyncHook<[Compiler]>} */
            afterPlugins: new SyncHook(["compiler"]),
            /** @type {SyncHook<[Compiler]>} */
            afterResolvers: new SyncHook(["compiler"]),
            /** @type {SyncBailHook<[string, Entry], boolean>} */
            entryOption: new SyncBailHook(["context", "entry"])
        });
        // 省略无数code。。。
}
  • 9.我们找到了 run: new AsyncSeriesHook([“compiler”]),可以看到,AsyncSeriesHook 在 tapable 中,webpack 源码简单流程就结束了。。。有时间会再深入
// /tapable/index.js
"use strict";
exports.__esModule = true;
exports.SyncHook = require("./SyncHook");
exports.SyncBailHook = require("./SyncBailHook");
exports.SyncWaterfallHook = require("./SyncWaterfallHook");
exports.SyncLoopHook = require("./SyncLoopHook");
exports.AsyncParallelHook = require("./AsyncParallelHook");
exports.AsyncParallelBailHook = require("./AsyncParallelBailHook");
exports.AsyncSeriesHook = require("./AsyncSeriesHook");
exports.AsyncSeriesBailHook = require("./AsyncSeriesBailHook");
exports.AsyncSeriesLoopHook = require("./AsyncSeriesLoopHook");
exports.AsyncSeriesWaterfallHook = require("./AsyncSeriesWaterfallHook");
exports.HookMap = require("./HookMap");
exports.MultiHook = require("./MultiHook");
  • 根据 tapable 文档的介绍,我们也可以知道这些钩子的定义
    • SyncHook 同步串行钩子,不关心返回值
    • SyncBailHook 同步串行,只要监听函数有一个返回不为 undefined,跳过剩下的
    • SyncWaterfallHook 同步串行,上一个监听函数可以返回给下一个监听函数
    • SyncLoopHook 同步循环,如果有一个函数返回 true 即反复执行
    • AsyncParallelHook 异步并发 不关心监听函数的返回值
    • AsyncParallelBailHook 异步并发,监听函数返回不为 null
    • AsyncSeriesHook 异步串行,不关心 callback
    • AsyncSeriesBailHook 异步串行
    • AsyncSeriesLoopHook 异步串行,上一个监听函数可以返回给下一个监听函数
    • AsyncSeriesWaterfallHook 异步串行,上一个监听函数的中的 callback(err, data)的第二个参数,可以作为下一个监听函数的参数

看个简单的 SyncHook 如何实现

function SyncHook(args = [], name = undefined) {
    const hook = new Hook(args, name);
    hook.constructor = SyncHook;
    hook.tapAsync = TAP_ASYNC;
    hook.tapPromise = TAP_PROMISE;
    hook.compile = COMPILE;
    return hook;
}
// hook.js
class Hook {
    constructor(args = [], name = undefined) {
        this._args = args;
        this.name = name;
        this.taps = [];
        this.interceptors = [];
        this._call = CALL_DELEGATE;
        this.call = CALL_DELEGATE;
        this._callAsync = CALL_ASYNC_DELEGATE;
        this.callAsync = CALL_ASYNC_DELEGATE;
        this._promise = PROMISE_DELEGATE;
        this.promise = PROMISE_DELEGATE;
        this._x = undefined;

        this.compile = this.compile;
        this.tap = this.tap;
        this.tapAsync = this.tapAsync;
        this.tapPromise = this.tapPromise;
    }

    compile(options) {
        throw new Error("Abstract: should be overridden");
    }

    _createCall(type) {
        return this.compile({
            taps: this.taps,
            interceptors: this.interceptors,
            args: this._args,
            type: type
        });
    }

    _tap(type, options, fn) {
        if (typeof options === "string") {
            options = {
                name: options.trim()
            };
        } else if (typeof options !== "object" || options === null) {
            throw new Error("Invalid tap options");
        }
        if (typeof options.name !== "string" || options.name === "") {
            throw new Error("Missing name for tap");
        }
        if (typeof options.context !== "undefined") {
            deprecateContext();
        }
        options = Object.assign({ type, fn }, options);
        options = this._runRegisterInterceptors(options);
        this._insert(options);
    }

    tap(options, fn) {
        this._tap("sync", options, fn);
    }

    tapAsync(options, fn) {
        this._tap("async", options, fn);
    }

    tapPromise(options, fn) {
        this._tap("promise", options, fn);
    }

    _runRegisterInterceptors(options) {
        for (const interceptor of this.interceptors) {
            if (interceptor.register) {
                const newOptions = interceptor.register(options);
                if (newOptions !== undefined) {
                    options = newOptions;
                }
            }
        }
        return options;
    }

    withOptions(options) {
        const mergeOptions = opt =>
            Object.assign({}, options, typeof opt === "string" ? { name: opt } : opt);

        return {
            name: this.name,
            tap: (opt, fn) => this.tap(mergeOptions(opt), fn),
            tapAsync: (opt, fn) => this.tapAsync(mergeOptions(opt), fn),
            tapPromise: (opt, fn) => this.tapPromise(mergeOptions(opt), fn),
            intercept: interceptor => this.intercept(interceptor),
            isUsed: () => this.isUsed(),
            withOptions: opt => this.withOptions(mergeOptions(opt))
        };
    }

    isUsed() {
        return this.taps.length > 0 || this.interceptors.length > 0;
    }

    intercept(interceptor) {
        this._resetCompilation();
        this.interceptors.push(Object.assign({}, interceptor));
        if (interceptor.register) {
            for (let i = 0; i < this.taps.length; i++) {
                this.taps[i] = interceptor.register(this.taps[i]);
            }
        }
    }

    _resetCompilation() {
        this.call = this._call;
        this.callAsync = this._callAsync;
        this.promise = this._promise;
    }

    _insert(item) {
        this._resetCompilation();
        let before;
        if (typeof item.before === "string") {
            before = new Set([item.before]);
        } else if (Array.isArray(item.before)) {
            before = new Set(item.before);
        }
        let stage = 0;
        if (typeof item.stage === "number") {
            stage = item.stage;
        }
        let i = this.taps.length;
        while (i > 0) {
            i--;
            const x = this.taps[i];
            this.taps[i + 1] = x;
            const xStage = x.stage || 0;
            if (before) {
                if (before.has(x.name)) {
                    before.delete(x.name);
                    continue;
                }
                if (before.size > 0) {
                    continue;
                }
            }
            if (xStage > stage) {
                continue;
            }
            i++;
            break;
        }
        this.taps[i] = item;
    }
}

简单实现上面的事件订阅

class SyncHook {
    constructor() {
        this.taps = [];
    }
    tap(name, fn) {
        this.taps.push(fn)
    }
    call() {
        this.taps.map(tap => tap(...arguments))
    }
}

我们可以看到,webpack 核心就是使用 tapable 来实现 plugins 的 building,tapable 是一个用于事件发布订阅的插件

与 webpack4 的对比

  • 1.webpack5 自集成了 friendly、analysisTable 等插件
  • 2.webpack5 打包文件体积缩小到很小
  • 3.webpack5 在 dev 环境下,直接使用 eval,大幅度提升开发环境- 下的速度,上线环境下不会编译 const 等,最大幅度缩小包体积
  • 4.webpack5 中集成了 prepack 插件,主要作用就是激进编译,例如一个方法计算几个值,打包后直接使用最终值
  • 5.webpack5 解决了 webpack4 异步 chunkName 的问题,可以通过指定 optimization.chunkIds 和 optimization.moduleIds 来指定,对比与 webpack4,区别不是主入口里映射区别,自动区分上线开发环境的文件名
  • 6.webpack5 新增 cache 模块,用于缓存资源
  • 7.对于 prod 环境改进 tree shanking,与 prepack 思想一致
  • 8.对于 splitChunks,更加细致指定 JavaScript 以及 style
  • 9.新增了实验特性,比如在 experiments 中开启 topLevelAwait,await 就不需要使用 async 函数声明等等
  • 10.webpack5 中可以不适用 file-loader 处理图片,直接配置 loader
module:{
    rules:[
        {
            test:/\.(png|jps|svg))/i,
            type:'asset'
        }
    ]
}
0