webpack打包产物解析及原理(含cjs/esm/代码分离/懒加载)

webpack打包产物解析及原理(含cjs/esm/代码分离/懒加载)


webpack 打包工具 代码分离 懒加载

webpack 的打包原理

(测试环境 webpack4+)

了解打包原理之前,需要先了解背景,webpack 诞生的背景

  • 先回顾下历史,在打包工具出现之前,我们是如何在 web 中使用 JavaScript 的。
  • 在浏览器中运行 JavaScript 有两种方法。
    • 第一种方式,引用一些脚本(script 标签)来存放每个功能;此解决方案很难扩展,因为加载太多脚本会导致网络瓶颈。
    • 第二种方式,使用一个包含所有项目代码的大型 .js 文件,但是这会导致作用域、文件大小、可读性和可维护性方面的问题。
  • 历史的解决方案(https://webpack.docschina.org/concepts/why-webpack)
    • iife
    • commonjs -(最大的问题:浏览器不支持 commonjs,因为 commonjs 是运行时 动态加载的,是同步的,浏览器同步的话,太慢了)
    • ESM - ECMAScript 模块
      • 未来的官方标准和主流。但是浏览器的版本需要比较高,比如 chorme 都需要 63 版本以上,(esm 是静态的,可以在编译的时候就分析出对应的依赖关系,不用像 commonjs 一样,运行时加载)

背景可以总结为:

  1. commonjs 很好,推出 npm 管理 JavaScript 模块包,但浏览器不支持
  2. esm 更好,浏览器也支持,但只有很新的浏览器才支持。 你可以源代码内写 esm 模块,webpack 可以帮忙打包,让不兼容 esm 的浏览器,也能兼容。

知道了 webpack 的诞生背景之后,理解 webpack 的打包原理就很简单了。webpack 的打包原理解析分为 2 种情况

  1. 处理方式一:所有内容打包到一个 chunk 包内

    无额外配置,webpack 一般会把所有 js 打成一个包。实现步骤:

    1. 读文件,扫描代码,按模块加载顺序,排列模块,分为模块 1,模块 2,…,模块 n 。放到一个作用域内,用 modules 保存,modules 是一个数组,所有模块按加载顺序,索引排序
    2. webpack 自己实现对应的 api(比如自己实现 require),让浏览器支持源代码内的模块化的写法(比如:module.export, require, esm 稍微有些不同 见下方)

    打包外部依赖也是一样的

  2. 处理方式二:多个 chunk 包?(比如:动态打包 ()=>import(‘xx’),代码分离)

以一个 demo 来更好的理解 处理方式一(合并到一个 chunk)(单 chunk)

单 chunk 原理解析

例子:先以 commonjs 模块作为例子

// 入口文件 main.js
const { b } = require("./test/b");
const a = "我是a";
console.log("打印" + a + b);

// ./test/b
module.exports = {
  b: "我是b",
};

打包得到:(简化后,方便理解原理)(代码可以直接在浏览器终端正确执行)

(function (modules) {
  var installedModules = {}; // 缓存模块

  // webpack自己实现的require方法,源代码内的require都会换成这个
  function __webpack_require__(moduleId) {
    // 加载过的模块,直接返回缓存
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // 注意!! 这个module会是webpack自己写的,然后会return出去,模仿commonjs的 module
    var module = (installedModules[moduleId] = {
      i: moduleId,
      exports: {}, // 模仿commonjs的 module.exports
    });

    // 注意!! 此行是执行模块函数,就是下面的 /* 0 */ /* 1 */  (并且传入了webpack模仿的 module.exports)
    modules[moduleId].call(
      module.exports,
      module,
      module.exports,
      __webpack_require__
    );

    // Return webpack模仿的 module.exports
    return module.exports;
  }
  // 从 第/* 0 */个模块开始执行
  return __webpack_require__(0); //
})(
  /************************************************************************/
  [
    /* 0 */ // 入口文件 main.js
    /***/ function (module, __webpack_exports__, __webpack_require__) {
      const { b } = __webpack_require__(1); // 源代码内的require换成了webpack模仿的__webpack_require__
      const a = "我是a";
      console.log("打印" + a + b);
    },
    /* 1 */ // 入口文件 main.js的依赖./test/b
    /***/ function (module, exports, __webpack_require__) {
      module.exports = {
        b: "我是b",
      };
    },
  ]
);

分析打包产物,注意看每一行的注释,和代码结构

代码结构是个 iife,传参是一个数组,数组的每一项,是源代码的模块代码 /* 0 */是入口main.js 代码 , /* 1 */是./test/b.js代码

结论:

  • webpack 解决模块问题的思路一是:所有的 js 依赖,打包到一个文件内,然后自己实现一套 require 和 module.exports,让浏览器可以执行源代码
    • 源代码的 require 会被换成 webpack_require
    • 源代码的 module.exports 不变,会由 webpack 作为函数的参数传给源代码

扩展:

这里只考虑了纯 commonjs,那 webpack 如何处理 esm 呢?

  • 其他情况 1:模块方式是纯的 esm

    • webpack 会做 tree shaking,最终的产物,会和 rollup 的产物比较接近,不会有过多的 webpack 注入的兼容代码
    • 实现思路类似 rollup,通过 esm 的静态特性,可以在编译的时候,就分析出对应的依赖关系
    • 例如上面的例子,改成纯的 esm 后,只会得到一个模块/* 0 */
    /* 0 */
    const b = "我是b";
    const a = "我是a";
    console.log("打印" + a + b);
  • 其他情况 2:模块方式是 esm + commonjs 混用的情况

    • webpack 很强大,他是支持混用的!!
    • 你可以 module.exports 导出, import xx from xx 导入
    • 也可以 exports { } 导出,require 引入
    • 实现的思路和上面的模拟 module.exports 和提供webpack_require替代 require 的思路类似,webpack 会去模拟 esm 的 exports 对象 让浏览器支持
  • 另外 对于打包第三方依赖,只要不是动态打包(比如 ()=>import(‘xx’)),不是代码分离的话,处理方式同上。

以三个 demo 来更好的理解 处理方式二(多个 chunk)

正常情况下,webpack 打包 js 文件都是只生成一个 chunk,除非做了一些额外的配置,或引入了一些共享的依赖,或者动态加载。

以下 3 种情况,打成多个 chunk:

1. import() 动态加载 (懒加载) import('./test/a').then(e => { console.log(e) })
console.log(111) 2. 公共依赖 (比如a,b 两文件 都依赖vue,
防止vue重复被打包进a和b) SplitChunksPlugin 开箱即用的 从 webpack v4
开始,移除了 `CommonsChunkPlugin`,取而代之的是 `optimization.splitChunks`。
webpack 将根据以下条件自动拆分 chunks: - 新的 chunk
可以被共享,或者模块来自于 `node_modules` 文件夹 - 新的 chunk 体积大于
20kb(在进行 min+gz 之前的体积) - 当按需加载 chunks
时,并行请求的最大数量小于或等于 30 -
当加载初始化页面时,并发请求的最大数量小于或等于 30
当尝试满足最后两个条件时,最好使用较大的 chunks。 3. 多个打包入口 entry: {
index: './src/index.js', another: './src/another-module.js', },

多 chunk 加载的原理解析

三种方式的实现原理都略有不同,以下会按 从简单到复杂的顺序来解析:(正好是上面的逆序)

1. 多个打包入口

  • 这个其实很容易理解,打包入口不一样,肯定会分离出多个包
// webpack.config.js
entry: {
  main: './src/main.js',
  a_js: './src/test/a.js',
},

// './src/main.js' (main.js 的内容)
console.log(1)

// './src/test/a.js' (a.js 的内容)
console.log(2222)

打包效果:

打包: Built at: 01/16/2022 11:31:52 AM Asset Size Chunks Chunk Names
favicon.ico 16.6 KiB [emitted] index.html 691 bytes [emitted] js/a_js.f67190e.js
3.91 KiB 0 [emitted] [immutable] a_js js/main.f09f871.js 4.0 KiB 1 [emitted]
[immutable] main 文件结构: dist js a_js.f67190e.js main.f09f871.js index.html
index.html 内容
<!DOCTYPE html>
<html>
  <head>
    ...
  </head>
  <body>
    <div id="app"></div>
    <script type="text/javascript" src="./js/main.f09f871.js"></script>
    <script type="text/javascript" src="./js/a_js.f67190e.js"></script>
  </body>
</html>
  • 结论
    • 多个入口分离多个包,然后生成多个 script 标签(按入口的顺序)
    • 分离出来的多个包,都包含同样多的模拟代码(webpack 注入的代码)

2. 分离公共依赖 (比如 a,b 两文件 都依赖 axios, 防止 axios 重复被打包进 a 和 b)(此处示例是无懒加载模块)

  • 用 index.html 来控制,先加载 venders(公共依赖 axios),后加载 main.js
// webpack.config.js    webpack的版本 v4.x
entry: './src/main.js',
optimization: {
  splitChunks: {
    chunks: 'all'
  }
}

// './src/main.js' (main.js 的内容)
import Axios from 'axios' // 共同引入了axios
Axios.get()
import {b} from './test/a'
console.log(b)

// './src/test/a.js' (a.js 的内容)
import Axios from 'axios' // 共同引入了axios
Axios.get()
export const b = 'xx'
export const bbbbbbb = 'xx'

打包结果,公共依赖 axios 会被放到 venders 内:

Built at: 01/16/2022 11:43:59 AM
                     Asset       Size  Chunks                         Chunk Names
               favicon.ico   16.6 KiB          [emitted]
                index.html  699 bytes          [emitted]
        js/main.48bf1d1.js    7.5 KiB       0  [emitted] [immutable]  main
js/vendors~main.4f0895a.js   41.9 KiB       1  [emitted] [immutable]  vendors~main

index.html 内容  ( 用index.html来控制,先加载venders(公共依赖) )
    <!DOCTYPE html>
    <html>
    <body>
        <div id="app"></div>
        <script type="text/javascript" src="./js/vendors~main.4f0895a.js"></script>
        <script type="text/javascript" src="./js/main.48bf1d1.js"></script></body>
    </body>
    </html>

分析 vendors~main.4f0895a.js (最先加载)(后面会把这个包简称为 venders 包, 意为 第三方依赖包)

贴上 vendors~main.4f0895a.js 的代码,此处简化了内容,重点看结构,有 26 个小模块,分别按索引排列(axios 源码内就有这么多模块,此处也是按顺序打包到了一起)

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[1],[
    /* 0 */
    (function(module, exports, __webpack_require__) { ...简化,主要看结构  }),
    /* 1 */
    (function(module, exports, __webpack_require__) { ...简化,主要看结构  }),
    ...
    ...
    /* 25 */  axios这个库,总共有0到25 共26个“小块”
])

重点看第一行 window["webpackJsonp"] = window["webpackJsonp"] || []

  • 第一行在全局,埋入了一个 webpackJsonp 属性,后续模块,就通过 window[“webpackJsonp”]来访问 axios 的 26 个“小块”的模块(读者如果读到了这里,可以试试,随意打开一个 webpack 项目,只要有多个 chunk 包的,查看控制台的 window 属性,都会找到 webpackJsonp 的属性的!!)

接下来分析 main.48bf1d1.js (后于 venders 加载)

  • 代码经过部分删减,已加上注释,会更好理解一点。
  • 结构和单 chunk 包是一样的,一个自执行函数 (function(modules))({26: main.js的内容}) (这个索引 26 是因为前面 0-25 都是 axios 的包,放在 venders 内,先加载了,此处的主要目的是把 venders 内的模块放进来,然后在正常解析)
  • 注意看注释,主要看中文注释,按代码执行顺序来看
(function (modules) {
  // webpackBootstrap
  // 把 刚才 vendors~main.4f0895a.js 内的, axios的26个模块, 加入到 modules内 ( 放到这个作用域内, 目前这个作用域内 modules只有1个模块, 就是下面的那个传参 {26: xx} )
  function webpackJsonpCallback(data) {
    var chunkIds = data[0];
    var moreModules = data[1];
    var executeModules = data[2];

    // add "moreModules" to the modules object,
    // then flag all "chunkIds" as loaded and fire callback
    var moduleId,
      chunkId,
      i = 0;
    for (; i < chunkIds.length; i++) {
      chunkId = chunkIds[i];
      installedChunks[chunkId] = 0;
    }
    for (moduleId in moreModules) {
      if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
        modules[moduleId] = moreModules[moduleId];
      }
    }

    // add entry modules from loaded chunk to deferred list
    deferredModules.push.apply(deferredModules, executeModules || []);

    // run deferred modules when all chunks ready
    return checkDeferredModules();
  }

  // 检测延迟加载的模块 (延迟模块, 可以理解为, 后于 venders 执行的模块, 目的: 先让venders内的模块 加入到本作用域内, 放到modules里面)
  function checkDeferredModules() {
    var result;
    for (var i = 0; i < deferredModules.length; i++) {
      var deferredModule = deferredModules[i];
      var fulfilled = true;
      for (var j = 1; j < deferredModule.length; j++) {
        var depId = deferredModule[j];
        if (installedChunks[depId] !== 0) fulfilled = false;
      }
      if (fulfilled) {
        deferredModules.splice(i--, 1);
        result = __webpack_require__(
          (__webpack_require__.s = deferredModule[0])
        );
      }
    }

    return result;
  }

  // The module cache
  var installedModules = {};

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

  var deferredModules = [];

  // 这里是webpack模拟require方法, 这里完全和单个chunk里面的 __webpack_require__ 一样的, 可以参考本文的上方解析
  function __webpack_require__(moduleId) {}

  // vendenrs包内的模块,都放在 全局对象window["webpackJsonp"]内了,此处,通过window["webpackJsonp"]来拿
  var jsonpArray = (window["webpackJsonp"] = window["webpackJsonp"] || []);
  for (var i = 0; i < jsonpArray.length; i++)
    webpackJsonpCallback(jsonpArray[i]);

  // add entry module to deferred list
  deferredModules.push([26, 1]); // 让下面的 {26: xx} 的模块 放到 __webpack_require__内去执行
  // run deferred modules when ready
  return checkDeferredModules(); // 触发defer模块(延迟模块, 可以理解为, 后于 venders 执行的模块, 目的: 先让venders内的模块 加入到本作用域内, 放到modules里面)
})({
  // 这里是传参 {26: xx}, 其实就是 main.js 内的代码

  // 为什么从索引26开始, 因为索引从 0 - 25 都是 axios的依赖, 会通过 webpackJsonpCallback 方法, 放到 modules 内去
  26: function (module, __webpack_exports__, __webpack_require__) {
    "use strict";
    // ESM COMPAT FLAG
    __webpack_require__.r(__webpack_exports__);

    // EXTERNAL MODULE: ./node_modules/_axios@0.18.1@axios/index.js
    var _axios_0_18_1_axios = __webpack_require__(1);
    var _axios_0_18_1_axios_default =
      /*#__PURE__*/ __webpack_require__.n(_axios_0_18_1_axios);

    // CONCATENATED MODULE: ./src/test/a.js
    _axios_0_18_1_axios_default.a.get();
    const b = "xx";
    const bbbbbbb = "xx";

    // CONCATENATED MODULE: ./src/main.js
    _axios_0_18_1_axios_default.a.get();
    console.log(b);
  },
});

main.48bf1d1.js 的解析总结

  • vendenrs 包内的模块,都放在 全局对象 window[“webpackJsonp”]内了,所以会先通过 window[“webpackJsonp”]来拿。比如此处,是拿 axios 的 26 个模块,然后放到 modules 内去(第一行可见这个参数)。
  • 后面的执行 和 此文上面的 执行方式一 执行 单 chunk 是一样的了,单 chunk 是只有一个 js 文件,所以 所有的模块都已经在 modules 里面了。
  • 此处多 chunk 因为没有 懒加载 chunk,所以,只需要把先加载的 venders 内的模块,放到 modules 里面,后面就和单 chunk 解析是一样的了

分离公共依赖的情况:整体流程总结

  1. 先加载 venders 包(第三方公共依赖),此加载不是解析代码,只是把第三方依赖的模块,以 webpack 能解析的格式,存到全局对象 window[“webpackJsonp”]内,方便后续的代码能访问到
  2. (此 demo 还剩 main 部分代码)加载 main.48bf1d1.js 代码,此处因为没有 懒加载 chunk(下面会解析懒加载模块 import(xx)),所以,只需要把 window[“webpackJsonp”]内的 venders 内的模块,放到 main 代码作用域内的 modules 里面,后面就和单 chunk 解析是一样的了

3. import() 动态加载(懒加载)

  • 在 webpack 内,通过 import()函数,可以实现某个模块的懒加载,并且是异步的(没执行到对应行,就不会加载模块)
// './src/main.js' (main.js 的内容)
console.log("做一大堆事");
console.log("做一大堆事");
console.log("做一大堆事");
import("./test/a").then((e) => {
  // 懒加载:执行到这一行  才会加载'./test/a'模块
  console.log(e);
});
console.log(111);
console.log("做一大堆事");

// './src/test/a.js' (a.js 的内容)
export const b = "xx";
export const bbbbbbb = "xx";

打包结果

Built at: 01/16/2022 5:21:55 PM Asset Size Chunks Chunk Names favicon.ico 16.6
KiB [emitted] index.html 624 bytes [emitted] js/1.bc77410.js 750 bytes 1
[emitted] [immutable] js/main.34d22b8.js 9.01 KiB 0 [emitted] [immutable] main
index.html 内容 ( 只会加载main.34d22b8.js,懒加载的依赖
通过main.34d22b8.js内的代码控制来加载,实际上会动态生成script标签 )
<!DOCTYPE html>
<html>
  <body>
    <div id="app"></div>
    <script type="text/javascript" src="./js/main.34d22b8.js"></script>
  </body>
</html>

先分析 懒加载模块 js/1.bc77410.js 的代码 (本身足够简单)

  • 和 多 chunk 的模式一样,要用 全局对象window["webpackJsonp"]作为缓存的“中间人”
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
  [1],
  [
    ,
    /* 0 */ // 第0个模块是 先加载的 main.34d22b8.js
    /* 1 */
    /***/ function (module, __webpack_exports__, __webpack_require__) {
      "use strict";
      __webpack_require__.r(__webpack_exports__); // 这里和 webpack 解析 esm 模块一样, 就是模拟 exports 对象
      /* harmony export (binding) */ __webpack_require__.d(
        __webpack_exports__,
        "b",
        function () {
          return b;
        }
      );
      /* harmony export (binding) */ __webpack_require__.d(
        __webpack_exports__,
        "bbbbbbb",
        function () {
          return bbbbbbb;
        }
      );

      const b = "xx";
      const bbbbbbb = "xx";

      /***/
    },
  ],
]);

在分析 main.34d22b8.js(最先执行的是这个,懒加载模块现在还未执行),先看主结构,结构和单 chunk 包是一样的,一个自执行函数 (function(modules))( /* 0 */索引0 是 main.34d22b8.js的代码 )

(function (modules) {
    ... // 篇幅原因,简化一大段代码
    function webpackJsonpCallback(data) {
      var chunkIds = data[0];
      var moreModules = data[1];

      // 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 (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
          resolves.push(installedChunks[chunkId][0]);
        }
        installedChunks[chunkId] = 0;
      }
      for (moduleId in moreModules) {
        if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
          modules[moduleId] = moreModules[moduleId];
        }
      }

      while (resolves.length) {
        resolves.shift()();
      }
    };

    var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
    jsonpArray.push = webpackJsonpCallback;

    // Load entry module and return exports
    return __webpack_require__(__webpack_require__.s = 0);
})
([
  /* 0 */
  (function (module, exports, __webpack_require__) {
    console.log('做一大堆事');
    console.log('做一大堆事');
    console.log('做一大堆事');
    __webpack_require__.e(/* import() */ 1).then(__webpack_require__.bind(null, 1)).then(e => {
      // 懒加载:执行到这一行  才会加载'./test/a'模块
      console.log(e);
    });
    console.log(111);
    console.log('做一大堆事');
  })
]);

// 以下作为解析, 因为代码篇幅过长, 此处只讲一下核心点
首先关注 import() 哪去了
原先
    import('./test/a').then(e => { // 懒加载:执行到这一行  才会加载'./test/a'模块
      console.log(e)
    })
替换成了
    __webpack_require__.e(/* import() */ 1).then(__webpack_require__.bind(null, 1)).then(e => { // 懒加载:执行到这一行  才会加载'./test/a'模块
      console.log(e);
    });

所以我们重点关注
    __webpack_require__.e(/* import() */ 1).then(__webpack_require__.bind(null, 1)).then(e => { // 懒加载:执行到这一行  才会加载'./test/a'模块
      console.log(e);
    });
很明显, __webpack_require__.e(/* import() */ 1) 会得到一个Promise,
如果这个Promise 状态变成了 fulfilled,才能往后执行.then(__webpack_require__.bind(null, 1)).then(e => ...);
    这行代码__webpack_require__.bind(null, 1),我相信理解了单chunk部分的同学,会一看就明白,这里就是解析 1.bc77410.js 里面的模块, 然后 模拟module.exports,并return module.exports,然后后续的.then(e => ...); 就可以接收到参数了。

然后解析__webpack_require__.e(/* import() */ 1)

此处贴出这个函数 (看个大概就好,不用纠结细节)
__webpack_require__.e = function requireEnsure(chunkId) {
  var promises = [];

  // JSONP chunk loading for javascript
  var installedChunkData = installedChunks[chunkId];
  if (installedChunkData !== 0) { // 0 means "already installed".
    // a Promise means "currently loading".
    if (installedChunkData) {
      promises.push(installedChunkData[2]);
    } else {
      // setup Promise in chunk cache
      var promise = new Promise(function (resolve, reject) {
        installedChunkData = installedChunks[chunkId] = [resolve, reject];
      });
      promises.push(installedChunkData[2] = promise);

      // start chunk loading
      var script = document.createElement('script');
      var onScriptComplete;

      script.charset = 'utf-8';
      script.timeout = 120;
      if (__webpack_require__.nc) {
        script.setAttribute("nonce", __webpack_require__.nc);
      }
      script.src = jsonpScriptSrc(chunkId);

      // create error before stack unwound to get useful stacktrace later
      var error = new Error();
      onScriptComplete = function (event) {
        // avoid mem leaks in IE.
        script.onerror = script.onload = null;
        clearTimeout(timeout);
        var chunk = installedChunks[chunkId];
        if (chunk !== 0) {
          if (chunk) {
            var errorType = event && (event.type === 'load' ? 'missing' : event.type);
            var realSrc = event && event.target && event.target.src;
            error.message = 'Loading chunk ' + chunkId + ' failed.\
(' + errorType + ': ' + realSrc + ')';
            error.name = 'ChunkLoadError';
            error.type = errorType;
            error.request = realSrc;
            chunk[1](error);
          }
          installedChunks[chunkId] = undefined;
        }
      };
      var timeout = setTimeout(function () {
        onScriptComplete({type: 'timeout', target: script});
      }, 120000);
      script.onerror = script.onload = onScriptComplete;
      document.head.appendChild(script);
    }
  }
  return Promise.all(promises);
};

看到 var script = document.createElement('script');
    document.head.appendChild(script); 这2行,相信大家都懂了,用script标签 去加载 js/1.bc77410.js (懒加载包)
另外 在更加上面的代码内, 在 webpackJsonpCallback函数内,有2行
    var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
    jsonpArray.push = webpackJsonpCallback;

window["webpackJsonp"].push 被覆盖为了 webpackJsonpCallback 函数 (既保留了push的能力,也增加了下面的作用)
webpackJsonpCallback函数 可以把Promise的状态从pending变成了fulfilled

总结过程

  • ① 先执行 main 模块的内容,从上到下执行,关注 import(‘xx’).then()行。打包后,import()会被替换成 webpack 的 api(__webpack_require__.e(/* import() */ 1).then(__webpack_require__.bind(null, 1)).then()

  • ② 替换后的 api 做了几件事

    • ① 生成 script 标签,并 appendChild 到 ducument.head 内
    • ② return 一个 Promise 对象,状态是 pending(pending 状态不会往后执行.then(__webpack_require__.bind(null, 1)).then(),但不会阻塞主程序,因为是异步的,不懂的可以了解一下 promise)
    • ③(异步)等了一段时间后,需求懒加载的模块通过 script 标签,被下载到浏览器后会直接解析执行,触发 window["webpackJsonp"].push(此方法被改写了,和生成同步多 chunk 有点不一样,会触发 webpackJsonpCallback 函数
    • ④ webpackJsonpCallback 函数的作用
      • ① 懒加载的模块 内容 会被加入到 main 的 mudules 的模块列表内去(等效 push 的作用)
      • ② 会把 Promise 的状态从 pending 改成 fulfilled,因为要懒加载的模块,通过 script 标签,已经解析完成了,所以.then()可以往后了
    • ⑤ 后面就是正常解析包,和单 chunk 解析多模块是一样的了

最后,解释一下 webpack 打包出来的代码看起为什么很“丑”?

webpack 诞生在 esm 标准出来前,commonjs 出来后

  • 当时的浏览器只能通过 script 标签加载模块

    • script 标签加载代码是没有作用域的,只能在代码内 用 iife 的方式 实现作用域效果,
      • 这就是 webpack 打包出来的代码 大结构都是 iife 的原因
      • 并且每个模块都要装到 function 里面,才能保证互相之间作用域不干扰。
      • 这就是为什么 webpack 打包的代码为什么乍看会感觉乱,找不到自己写的代码的真正原因
  • 关于 webpack 的代码注入问题,是因为浏览器不支持 cjs,所以 webpack 要去自己实现 require 和 module.exports 方法(才有很多注入)(相当于 webpack 自己做了 polyfill)

    • 这么多年了,甚至到现在 2022 年,浏览器为什么不支持 cjs?

      • cjs 是同步的,运行时的,node 环境用 cjs,node 本身运行在服务器,无需等待网络握手,所以同步处理是很快的
      • 浏览器是 客户端,访问的是服务端资源,中间需要等待网络握手,可能会很慢,所以不能 同步的 卡在那里等服务器返回的,体验太差
  • 后续出来 esm 后,webpack 为了兼容以前发在 npm 上的老包(并且当时心还不够决绝,导致这种“丑结构的包”越来越多,以后就更不可能改这种“丑结构了”),所以保留这个 iife 的结构和代码注入,导致现在看 webpack 打包的产物,乍看结构比较乱且有很多的代码注入,自己写的代码都找不到

本文转载自
作者:bigtree
链接:https://juejin.cn/post/7053998924071174175
来源:稀土掘金
© 2025 Niko Xie