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

以下结论都是是通过一步一步实际测试,然后分析打包产物,来解析原理

  • 以小见大,先从简单的入手
  • 先纯cjs,在纯esm,在混用,在分析第三方模块,分析动态引入

测试用例范围:

  • (经测试,很多结果比较类似,篇幅原因,所以后面的文章介绍只挑了典型)
    内部代码
    1. 最简单的包,入口文件无导出和导入
    2. 入口文件有导出,无导入
    3. 入口文件无导出,有导入
    4. 入口文件有导出,有导入
    5. 串行依赖 (2级)
    6. 并行依赖
    7. 并行依赖 有公共依赖
      外部依赖
    8. 静态加载(cjs和esm)
    9. 动态加载 () => import(‘xx’) 和 代码分离

webpack的打包原理

(测试环境 webpack4+)

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

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

esm-compatible.png

背景可以总结为

  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.exports, require, esm稍微有些不同 见下方)

打包外部依赖也是一样的

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

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

单chunk原理解析

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

  1. // 入口文件 main.js
  2. const { b } = require('./test/b')
  3. const a = '我是a';
  4. console.log('打印' + a + b);
  5. // ./test/b
  6. module.exports = {
  7. b: '我是b'
  8. };

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

  1. (function (modules) {
  2. var installedModules = {}; // 缓存模块
  3. // webpack自己实现的require方法,源代码内的require都会换成这个
  4. function __webpack_require__(moduleId) {
  5. // 加载过的模块,直接返回缓存
  6. if (installedModules[moduleId]) {
  7. return installedModules[moduleId].exports;
  8. }
  9. // 注意!! 这个module会是webpack自己写的,然后会return出去,模仿commonjs的 module
  10. var module = installedModules[moduleId] = {
  11. i: moduleId,
  12. exports: {} // 模仿commonjs的 module.exports
  13. };
  14. // 注意!! 此行是执行模块函数,就是下面的 /* 0 */ /* 1 */ (并且传入了webpack模仿的 module.exports)
  15. modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
  16. // Return webpack模仿的 module.exports
  17. return module.exports;
  18. }
  19. // 从 第/* 0 */个模块开始执行
  20. return __webpack_require__(0); //
  21. })
  22. /************************************************************************/
  23. ([
  24. /* 0 */ // 入口文件 main.js
  25. /***/ (function (module, __webpack_exports__, __webpack_require__) {
  26. const { b } = __webpack_require__(1); // 源代码内的require换成了webpack模仿的__webpack_require__
  27. const a = '我是a';
  28. console.log('打印' + a + b);
  29. }),
  30. /* 1 */ // 入口文件 main.js的依赖./test/b
  31. /***/ (function (module, exports, __webpack_require__) {
  32. module.exports = {
  33. b: '我是b'
  34. };
  35. })
  36. ]);

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

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

结论

  • webpack解决模块问题的思路一是:所有的js依赖,打包到一个文件内,然后自己实现一套require和module.exports,让浏览器可以执行源代码
    1. 源代码的require会被换成 __webpack_require__
    2. 源代码的module.exports不变,会由webpack作为函数的参数传给源代码
      扩展
  • 细心的朋友可能发现了,这里只考虑了纯commonjs,那webpack如何处理esm呢?

    • 篇幅原因,本文先给结论,感兴趣的小伙伴可以自己去test一下
    • 其他情况1:模块方式是纯的esm

      • webpack会做tree shaking,最终的产物,会和rollup的产物比较接近,不会有过多的webpack注入的兼容代码
      • 实现思路类似rollup,通过esm的静态特性,可以在编译的时候,就分析出对应的依赖关系
      • 例如上面的例子,改成纯的esm后,只会得到一个模块/* 0 */
        1. /* 0 */
        2. const b = '我是b';
        3. const a = '我是a';
        4. 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’)),不是代码分离的话,处理方式同上。有兴趣的小伙伴可以自行test一下。有疑问可以评论留言

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

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

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

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

多chunk加载的原理解析

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

1. 多个打包入口。

  • 这个其实很容易理解,打包入口不一样,肯定会分离出多个包 打包效果: ```javascript // 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)

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

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

  • 用index.html来控制,先加载venders(公共依赖axios),后加载main.js
    打包结果,公共依赖axios会被放到venders内
    vender.png
    贴上vendors~main.4f0895a.js的代码,此处简化了内容,重点看结构,有26个小模块,分别按索引排列(axios源码内就有这么多模块,此处也是按顺序打包到了一起)
    我们重点看第一行 window["webpackJsonp"] = window["webpackJsonp"] || [] ```javascript // 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’

  1. ```html
  2. Built at: 01/16/2022 11:43:59 AM
  3. Asset Size Chunks Chunk Names
  4. favicon.ico 16.6 KiB [emitted]
  5. index.html 699 bytes [emitted]
  6. js/main.48bf1d1.js 7.5 KiB 0 [emitted] [immutable] main
  7. js/vendors~main.4f0895a.js 41.9 KiB 1 [emitted] [immutable] vendors~main
  8. index.html 内容 ( 用index.html来控制,先加载venders(公共依赖) )
  9. <!DOCTYPE html>
  10. <html>
  11. <body>
  12. <div id="app"></div>
  13. <script type="text/javascript" src="./js/vendors~main.4f0895a.js"></script>
  14. <script type="text/javascript" src="./js/main.48bf1d1.js"></script></body>
  15. </body>
  16. </html>

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

  1. (window["webpackJsonp"] = window["webpackJsonp"] || []).push([[1],[
  2. /* 0 */
  3. (function(module, exports, __webpack_require__) { ...简化,主要看结构 }),
  4. /* 1 */
  5. (function(module, exports, __webpack_require__) { ...简化,主要看结构 }),
  6. ...
  7. ...
  8. /* 25 */ axios这个库,总共有025 26个“小块”
  9. ])
  • 第一行在全局,埋入了一个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内的模块放进来,然后在正常解析
  • !!注意看注释,主要看中文注释,按代码执行顺序来看

    1. (function (modules) { // webpackBootstrap
    2. // 把 刚才 vendors~main.4f0895a.js 内的, axios的26个模块, 加入到 modules内 ( 放到这个作用域内, 目前这个作用域内 modules只有1个模块, 就是下面的那个传参 {26: xx} )
    3. function webpackJsonpCallback(data) {
    4. var chunkIds = data[0];
    5. var moreModules = data[1];
    6. var executeModules = data[2];
    7. // add "moreModules" to the modules object,
    8. // then flag all "chunkIds" as loaded and fire callback
    9. var moduleId, chunkId, i = 0;
    10. for (; i < chunkIds.length; i++) {
    11. chunkId = chunkIds[i];
    12. installedChunks[chunkId] = 0;
    13. }
    14. for (moduleId in moreModules) {
    15. if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
    16. modules[moduleId] = moreModules[moduleId];
    17. }
    18. }
    19. // add entry modules from loaded chunk to deferred list
    20. deferredModules.push.apply(deferredModules, executeModules || []);
    21. // run deferred modules when all chunks ready
    22. return checkDeferredModules();
    23. };
    24. // 检测延迟加载的模块 (延迟模块, 可以理解为, 后于 venders 执行的模块, 目的: 先让venders内的模块 加入到本作用域内, 放到modules里面)
    25. function checkDeferredModules() {
    26. var result;
    27. for (var i = 0; i < deferredModules.length; i++) {
    28. var deferredModule = deferredModules[i];
    29. var fulfilled = true;
    30. for (var j = 1; j < deferredModule.length; j++) {
    31. var depId = deferredModule[j];
    32. if (installedChunks[depId] !== 0) fulfilled = false;
    33. }
    34. if (fulfilled) {
    35. deferredModules.splice(i--, 1);
    36. result = __webpack_require__(__webpack_require__.s = deferredModule[0]);
    37. }
    38. }
    39. return result;
    40. }
    41. // The module cache
    42. var installedModules = {};
    43. // object to store loaded and loading chunks
    44. // undefined = chunk not loaded, null = chunk preloaded/prefetched
    45. // Promise = chunk loading, 0 = chunk loaded
    46. var installedChunks = {
    47. 0: 0
    48. };
    49. var deferredModules = [];
    50. // 这里是webpack模拟require方法, 这里完全和单个chunk里面的 __webpack_require__ 一样的, 可以参考本文的上方解析
    51. function __webpack_require__(moduleId) {}
    52. // vendenrs包内的模块,都放在 全局对象window["webpackJsonp"]内了,此处,通过window["webpackJsonp"]来拿
    53. var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
    54. for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
    55. // add entry module to deferred list
    56. deferredModules.push([26, 1]); // 让下面的 {26: xx} 的模块 放到 __webpack_require__内去执行
    57. // run deferred modules when ready
    58. return checkDeferredModules(); // 触发defer模块(延迟模块, 可以理解为, 后于 venders 执行的模块, 目的: 先让venders内的模块 加入到本作用域内, 放到modules里面)
    59. })({ // 这里是传参 {26: xx}, 其实就是 main.js 内的代码
    60. 26: // 为什么从索引26开始, 因为索引从 0 - 25 都是 axios的依赖, 会通过 webpackJsonpCallback 方法, 放到 modules 内去
    61. (function (module, __webpack_exports__, __webpack_require__) {
    62. "use strict";
    63. // ESM COMPAT FLAG
    64. __webpack_require__.r(__webpack_exports__);
    65. // EXTERNAL MODULE: ./node_modules/_axios@0.18.1@axios/index.js
    66. var _axios_0_18_1_axios = __webpack_require__(1);
    67. var _axios_0_18_1_axios_default = /*#__PURE__*/__webpack_require__.n(_axios_0_18_1_axios);
    68. // CONCATENATED MODULE: ./src/test/a.js
    69. _axios_0_18_1_axios_default.a.get();
    70. const b = 'xx';
    71. const bbbbbbb = 'xx';
    72. // CONCATENATED MODULE: ./src/main.js
    73. _axios_0_18_1_axios_default.a.get();
    74. console.log(b);
    75. })
    76. });

    main.48bf1d1.js的解析总结

  1. vendenrs包内的模块,都放在 全局对象window["webpackJsonp"]内了,所以会先通过window["webpackJsonp"]来拿。比如此处,是拿axios的26个模块,然后放到modules内去(第一行可见这个参数)。
  2. 后面的执行 和 此文上面的 执行方式一 执行 单chunk 是一样的了,单chunk是只有一个js文件,所以 所有的模块都已经在modules里面了。
  3. 此处多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()函数,可以实现某个模块的懒加载,并且是异步的(没执行到对应行,就不会加载模块)
    打包结果
    先分析 懒加载模块 js/1.bc77410.js 的代码 (本身足够简单) ```javascript // ‘./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’

  1. ```html
  2. Built at: 01/16/2022 5:21:55 PM
  3. Asset Size Chunks Chunk Names
  4. favicon.ico 16.6 KiB [emitted]
  5. index.html 624 bytes [emitted]
  6. js/1.bc77410.js 750 bytes 1 [emitted] [immutable]
  7. js/main.34d22b8.js 9.01 KiB 0 [emitted] [immutable] main
  8. index.html 内容 ( 只会加载main.34d22b8.js,懒加载的依赖 通过main.34d22b8.js内的代码控制来加载,实际上会动态生成script标签 )
  9. <!DOCTYPE html>
  10. <html>
  11. <body>
  12. <div id="app"></div>
  13. <script type="text/javascript" src="./js/main.34d22b8.js"></script>
  14. </body>
  15. </html>
  • 和 多chunk的模式一样,要用 全局对象**window["webpackJsonp"]**作为缓存的“中间人” ```javascript (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’;

/*/ }) ]]);

  1. 在分析 main.34d22b8.js(最先执行的是这个,懒加载模块现在还未执行),先看主结构,**结构和单chunk包是一样的**,一个自执行函数 `(function(modules))( /* 0 */索引0 是 main.34d22b8.js的代码 )`
  2. - 注意看注释!!!
  3. ```javascript
  4. (function (modules) {
  5. ... // 篇幅原因,简化一大段代码
  6. function webpackJsonpCallback(data) {
  7. var chunkIds = data[0];
  8. var moreModules = data[1];
  9. // add "moreModules" to the modules object,
  10. // then flag all "chunkIds" as loaded and fire callback
  11. var moduleId, chunkId, i = 0, resolves = [];
  12. for (; i < chunkIds.length; i++) {
  13. chunkId = chunkIds[i];
  14. if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
  15. resolves.push(installedChunks[chunkId][0]);
  16. }
  17. installedChunks[chunkId] = 0;
  18. }
  19. for (moduleId in moreModules) {
  20. if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
  21. modules[moduleId] = moreModules[moduleId];
  22. }
  23. }
  24. while (resolves.length) {
  25. resolves.shift()();
  26. }
  27. };
  28. var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
  29. jsonpArray.push = webpackJsonpCallback;
  30. // Load entry module and return exports
  31. return __webpack_require__(__webpack_require__.s = 0);
  32. })
  33. ([
  34. /* 0 */
  35. (function (module, exports, __webpack_require__) {
  36. console.log('做一大堆事');
  37. console.log('做一大堆事');
  38. console.log('做一大堆事');
  39. __webpack_require__.e(/* import() */ 1).then(__webpack_require__.bind(null, 1)).then(e => {
  40. // 懒加载:执行到这一行 才会加载'./test/a'模块
  41. console.log(e);
  42. });
  43. console.log(111);
  44. console.log('做一大堆事');
  45. })
  46. ]);
  47. // 以下作为解析, 因为代码篇幅过长, 此处只讲一下核心点
  48. 首先关注 import() 哪去了
  49. 原先
  50. import('./test/a').then(e => { // 懒加载:执行到这一行 才会加载'./test/a'模块
  51. console.log(e)
  52. })
  53. 替换成了
  54. __webpack_require__.e(/* import() */ 1).then(__webpack_require__.bind(null, 1)).then(e => { // 懒加载:执行到这一行 才会加载'./test/a'模块
  55. console.log(e);
  56. });
  57. 所以我们重点关注
  58. __webpack_require__.e(/* import() */ 1).then(__webpack_require__.bind(null, 1)).then(e => { // 懒加载:执行到这一行 才会加载'./test/a'模块
  59. console.log(e);
  60. });
  61. 很明显, __webpack_require__.e(/* import() */ 1) 会得到一个Promise,
  62. 如果这个Promise 状态变成了 fulfilled,才能往后执行.then(__webpack_require__.bind(null, 1)).then(e => ...);
  63. 这行代码__webpack_require__.bind(null, 1),我相信理解了单chunk部分的同学,会一看就明白,这里就是解析 1.bc77410.js 里面的模块, 然后 模拟module.exports,并return module.exports,然后后续的.then(e => ...); 就可以接收到参数了。
  64. 然后解析__webpack_require__.e(/* import() */ 1)
  65. 此处贴出这个函数 (看个大概就好,不用纠结细节)
  66. __webpack_require__.e = function requireEnsure(chunkId) {
  67. var promises = [];
  68. // JSONP chunk loading for javascript
  69. var installedChunkData = installedChunks[chunkId];
  70. if (installedChunkData !== 0) { // 0 means "already installed".
  71. // a Promise means "currently loading".
  72. if (installedChunkData) {
  73. promises.push(installedChunkData[2]);
  74. } else {
  75. // setup Promise in chunk cache
  76. var promise = new Promise(function (resolve, reject) {
  77. installedChunkData = installedChunks[chunkId] = [resolve, reject];
  78. });
  79. promises.push(installedChunkData[2] = promise);
  80. // start chunk loading
  81. var script = document.createElement('script');
  82. var onScriptComplete;
  83. script.charset = 'utf-8';
  84. script.timeout = 120;
  85. if (__webpack_require__.nc) {
  86. script.setAttribute("nonce", __webpack_require__.nc);
  87. }
  88. script.src = jsonpScriptSrc(chunkId);
  89. // create error before stack unwound to get useful stacktrace later
  90. var error = new Error();
  91. onScriptComplete = function (event) {
  92. // avoid mem leaks in IE.
  93. script.onerror = script.onload = null;
  94. clearTimeout(timeout);
  95. var chunk = installedChunks[chunkId];
  96. if (chunk !== 0) {
  97. if (chunk) {
  98. var errorType = event && (event.type === 'load' ? 'missing' : event.type);
  99. var realSrc = event && event.target && event.target.src;
  100. error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
  101. error.name = 'ChunkLoadError';
  102. error.type = errorType;
  103. error.request = realSrc;
  104. chunk[1](error);
  105. }
  106. installedChunks[chunkId] = undefined;
  107. }
  108. };
  109. var timeout = setTimeout(function () {
  110. onScriptComplete({type: 'timeout', target: script});
  111. }, 120000);
  112. script.onerror = script.onload = onScriptComplete;
  113. document.head.appendChild(script);
  114. }
  115. }
  116. return Promise.all(promises);
  117. };
  118. 看到 var script = document.createElement('script');
  119. document.head.appendChild(script); 这2行,相信大家都懂了,用script标签 去加载 js/1.bc77410.js (懒加载包)
  120. 另外 在更加上面的代码内, 在 webpackJsonpCallback函数内,有2行
  121. var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
  122. jsonpArray.push = webpackJsonpCallback;
  123. window["webpackJsonp"].push 被覆盖为了 webpackJsonpCallback 函数 (既保留了push的能力,也增加了下面的作用👇🏻)
  124. webpackJsonpCallback函数 可以把Promise的状态从pending变成了fulfilled

总结过程

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

篇幅有点长,要写清楚确实有点不容易,有问题可以留言,一时半会确实可能不好理解

需要的背景知识不少(commonjs,esm,2者的优缺点及浏览器兼容问题,commonjs之前的历史模块解决情况,webpack配置,第三方依赖vender的概念,代码分离,懒加载,自执行函数iife,promise(微任务,宏任务),代码调试能力)

笔者建议,最好自己上手打包 调试,得到的打包产物 并仔细分析。一时看不懂的话,也可以收藏本文,过段时间在看,先了解前置知识

最后,谢谢点赞!