# webpack打包📦出来的代码

webpack@4.43.0

webpack可以兼容多种模块化机制, 先来看一下webpack打包esModule输出的代码. 直接看打包之后的代码, 能对webpack有一个初步感性的认识.

# 总览

webpack.config.js

module.exports = {
  mode: 'development',
  devtool: 'source-map'
}
1
2
3
4

src/index.js

import defaultB, {namedB} from './b'
console.log(defaultB, namedB)

export const namedA = 'namedA'
export default 'defaultA'

1
2
3
4
5
6

src/b.js

export const namedB = 'b'
export default 'defaultB'
1
2

默认webpack打包出来的是可以直接运行在web上的代码

webpackIIFE

dist/main.js

/******/ (function(modules) { // webpackBootstrap
//...
/******/ 	// Load entry module and return exports
/******/ 	return __webpack_require__(__webpack_require__.s = "./src/index.js");
/******/ })
/************************************************************************/
/******/ ({

/***/ "./src/b.js":
/*!******************!*\
  !*** ./src/b.js ***!
  \******************/
/*! exports provided: namedB, default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
// ...
/***/ }),

/***/ "./src/index.js":
/*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
/*! exports provided: namedA, default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
// ...
/***/ })

/******/ });
//# sourceMappingURL=main.js.map
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

可以看到打包后的代码是IIFE(immediately invoked function expression), 函数的输出是entryModule, IIFE的输入是包含一系列依赖模块的对象字面量, 每个模块都被类似如下的结构所包裹.

(function(module, __webpack_exports__, __webpack_require__) {
// ...
}),
1
2
3

webpackBootstrap则是能够收集,缓存依赖, 让模块拥有导出和导入其他模块的能力. 这和nodejs实现commonjs的方式非常类似. webpackBootstrap的存在, 也使得webpack和nodejs实现的commonjs一样, 可以处理循环依赖.

之前捣鼓vscode插件打包的时候, 遇到rollup处理不了循环依赖, 最后还是用了webpack

# Module和webpackBootstrap

首先看一下, 一些必要的webpackBootstrap代码

# __webpack_require__

 	// The module cache
 	var installedModules = {};

 	// The require function
 	function __webpack_require__(moduleId) {

 		// Check if module is in cache
 		if(installedModules[moduleId]) {
 			return installedModules[moduleId].exports;
 		}
 		// Create a new module (and put it into the cache)
 		var module = installedModules[moduleId] = {
 			i: moduleId,
 			l: false,
 			exports: {}
 		};

 		// Execute the module function
 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

 		// Flag the module as loaded
 		module.l = true;

 		// Return the exports of the module
 		return module.exports;
 	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

内部使用installedModules来缓存模块, __webpack_require__流程很清晰,

  1. 首先检查缓存, 若存在直接返回已经缓存的模块
  2. 创建新的模块, installedModules[moduleId]也指向这个新模块

TIP

这一步对于解决循环依赖很关键(var module = installedModules[moduleId] = ...), 因为执行下面的module function时, 如果循环依赖到了自身, 那依赖到的自身就是上面的installedModules[moduleId]

  1. 执行module function

  2. 返回模块的exports

# __webpack_require__.d

// define getter function for harmony exports
__webpack_require__.d = function(exports, name, getter) {
	if(!__webpack_require__.o(exports, name)) {
		Object.defineProperty(exports, name, { enumerable: true, get: getter });
	}
};
1
2
3
4
5
6

# __webpack_require__.o

	// Object.prototype.hasOwnProperty.call
	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
1
2

__webpack_require__.d通过定义getter从而让内部值的变化能够反应到外部. 这点和commonjs不同. babel和typescript实现esModule这个性质的方式和webpack不同, 它们是在编译阶段实现的.

# __webpack_require__.r


// define __esModule on exports
__webpack_require__.r = function(exports) {
	if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
		Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
	}
	Object.defineProperty(exports, '__esModule', { value: true });
};
1
2
3
4
5
6
7
8

Symbol.toStringTag定义模块输出的toString[object Module], 并在exports上定义__esModule属性, 来表明该模块是由ES模块转化而来.

告诉别人该模块是ES模块转化过来的, 意味着什么呢?

这等于在说, hi, 我的默认导出在‘default’属性上. 这在处理模块之间的兼容有重要作用. 比如一个es6的模块通过webpack打包成了commonjs, 然后被拿去别人用, 别人是通过babel用这个包的.

# entryModule

首先看下entryModule

/*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
/*! exports provided: namedA, default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "namedA", function() { return namedA; });
/* harmony import */ var _b__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./b */ "./src/b.js");

console.log(_b__WEBPACK_IMPORTED_MODULE_0__["default"], _b__WEBPACK_IMPORTED_MODULE_0__["namedB"])

const namedA = 'a'
/* harmony default export */ __webpack_exports__["default"] = ('defaultA');
/***/ })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

esModule是声明式的, webpack所做的就是把这些声明式的代码, 替换为命令式的代码.

入口模块中, 使用__webpack_require__.d来定义模块的named export, 使用__webpack_require__来加载所依赖的模块.

# default export

default export则是通过一个default属性来实现. 有意思是默认导出是一个值的复制, 并不像__webpack_require__.d.

更有意思的是, 如果代码是这样写的,

const defaultA = 'defaultA'
export {
  defaultA as default
}
1
2
3
4

default则是用getter来定义的, 这个暂时还不清楚, 虽然阮一峰老师的文章说export {defaultA as default}等同于export default defaultA. 需要研究下ES6的规范和webpack的实现.

类似的场景还有,

export default function () {
  //...
}
// 其打包后

/* harmony default export */ __webpack_exports__["default"] = (function (){

});
1
2
3
4
5
6
7
8
export default function c () {
// ...
}
// 其打包后

/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return c; });

function c() {}

/***/ })
1
2
3
4
5
6
7
8
9
10

TODO: 这个需要具体分析webpack/lib/dependencies/HarmonyExportSpecifierDependency.jswebpack/lib/dependencies/HarmonyExportExpressionDependency.js

# 使用ESModule语法导入commonjs模块

commonjs和esModule本身是不兼容的

esModule有两种导出方式, named export, default export, 而commonjs只有一种导出方式, 就是通过module.exports/ exports

假如有个commonjs规范的代码如下, example.js

module.exports = function () {}
module.exports.a = 'a'
module.exports.b = 'b'
1
2
3

那esModule的语法要怎么去加载这个模块呢? 实际上是如下方式

import example, {a, b} from './example.js'
1

我们要用esModule的语法导入commonjs模块, 就需要把esModule的name export和default export两个概念映射到commonjs中. 那么module.exports的整体导出就对应default export, 通过module.exports上的属性named export. 可以看到这两个概念在commonjs中重叠使用了module.exports

使用如下的例子打包,

调整一下src/b.js, 改为commonjs, webpack也是可以搞定的

src/index.js

import defaultB, {namedB} from './b'
console.log(defaultB, namedB)
1
2

src/b.js

const c = require('./c.js')
module.exports = function b() {
  c()
}
module.exports.namedB = 'namedB'

1
2
3
4
5
6

src/c.js

module.exports = function c() {console.log('defaultC')}
1

其依赖关系如下,

esmodule_commonjs

打包后 src/index.js中主要关注导入模块b的代码

























 
 

 







({

 "./src/b.js":
 (function(module, exports) {
const c = __webpack_require__(/*! ./c.js */ "./src/c.js")
module.exports = function b() {
  c()
}
module.exports.namedB = 'namedB'

 }),

 "./src/c.js":
 (function(module, exports) {

  module.exports = function c() {console.log('defaultC')}

 }),

 "./src/index.js":
 (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _b__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./b */ "./src/b.js");
/* harmony import */ var _b__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_b__WEBPACK_IMPORTED_MODULE_0__);

console.log(_b__WEBPACK_IMPORTED_MODULE_0___default.a, _b__WEBPACK_IMPORTED_MODULE_0__["namedB"])



 })

});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

对于需要导入的namedB, 是直接从src/b.jsmodule.exports上去取(和前一个例子相同), 而对于defaultB, 则需要用__webpack_require__.n来处理.

# __webpack_require__.n

	// getDefaultExport function for compatibility with non-harmony modules
	__webpack_require__.n = function(module) {
		var getter = module && module.__esModule ?
			function getDefault() { return module['default']; } :
			function getModuleExports() { return module; };
		__webpack_require__.d(getter, 'a', getter);
		return getter;
	};
1
2
3
4
5
6
7
8

因为我们加载的commonjs有可能是通过打包工具从esModule转化过来的, 这种从esModule转化过来的commonjs模块, 会有__esModule属性, 这种模块, 它的default export就在‘default’属性上, 而对于传统的commonjs来说, 我们就需要加载其module.exports作为其default export. (假如我们的src/b.js中的代码是网上找来的代码, 别人是通过babel打包出来的, 那它就可能有__esModule属性Jj)

babel/ts的处理和webpack类似, 但是比webpack更为周到.

考虑下面这种情况

src/index.js

import * as f from './b'
console.log(f())
1
2

src/b.js

module.exports = function d() {console.log('defaultB')}
1

import * as f from './b'在esModule中是将所有的named export加载到f上, f就是一个命名空间, f是不能作为一个函数去调用的.

webpack打出的src/index.js代码中关键部分如下

/* harmony import */ var _b__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./b */ "./src/b.js");

console.log(_b__WEBPACK_IMPORTED_MODULE_0__())
1
2
3

webpack打包的代码, f是可以执行的, 这违背esModule的规范. 而babel/ts在对这种情况做了处理.

在ts2.7的版本以前, 其处理方式和webpack类似, import * as ..直接等同于require(), 而在2.7版本中新增的esModuleInterop编译选项 (opens new window)就是针对这种情况的.

# 使用commonjs语法导入ESModule

直接用require()去理解就行,

src/b.js

export default 'defaultB'

export const namedB = 'namedB'
1
2
3

src/index.js

const b = require('./b')
console.log(b)
1
2

我们拿到的结果就是

Object [Module] { namedB: [Getter], default: 'defaultB' }
// 这个是node的输出, 没显示不可枚举的`__esModule`属性
1
2

所在有些情况下, 你会看到类似的代码require(...).default

# 混用commonjs 和esModule可能存在的问题

当然, 强烈建议不要混用两种不同的规范

有种情况是有问题, 下面举个例子

src/b.js

export default 'defaultB'

export const namedB = 'namedB'
1
2
3

src/index.js

import defaultB, {namedB} from './b'

console.log(defaultB, namedB)

// exports.test = 2 // 不会有效
module.exports = { // 会报错
  test: 2
}
1
2
3
4
5
6
7
8

会什么这种情况会报错呢, 看一下打包后的代码

{
"./node_modules/webpack/buildin/harmony-module.js":
(function(module, exports) {
  //...
}),

 "./src/b.js":
 (function(module, __webpack_exports__, __webpack_require__) {
   //...
 }),

 "./src/index.js":
 (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* WEBPACK VAR INJECTION */(function(module) {/* harmony import */ var _b__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./b */ "./src/b.js");


console.log(_b__WEBPACK_IMPORTED_MODULE_0__["default"], _b__WEBPACK_IMPORTED_MODULE_0__["namedB"])

// exports.test = 2 // 不会有效
// module.exports.test = 2 // 会报错
module.exports = { // 会报错
  test: 2
}



/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../node_modules/webpack/buildin/harmony-module.js */ "./node_modules/webpack/buildin/harmony-module.js")(module)))

 })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

我们看到, 有一个harmony-module.js的代码被执行了, 这个模块中有一句关键代码, exportswritablefalse, module.exports是只读的

		Object.defineProperty(module, "exports", {
			enumerable: true
		});
1
2
3

因为我们使用了esModule的导入语法, 所以webpack判断当前是一个es module, 而上面提到, esModule的导出语法本身是和commonjs的导出语法是不兼容的. 所以webpack不允许这样混用.

TIP

在使用vue cli的时候碰到一个问题 (opens new window), 就是由这个造成的

# External

在打包库代码的时候, external是常用到的一个配置项

假如我们现在有一个库lib-b, 入口文件代码如下

src/index.js

const c = require('./c.js')
module.exports = function b() {
  c()
}
module.exports.namedB = 'namedB'
1
2
3
4
5

src/c.js

module.exports = function c() {console.log('defaultC')}
1

我们现在要打包库lib-b, 假如./c.js已经被提取成了一个npm包lib-c, 配置如下,

webpack.config.js

module.exports = {
  mode: 'development', // 仅为了说明, 使用dev
  devtool: 'source-map',
  output: {
    libraryTarget: 'commonjs2',
  },
  externals: {
    './c.js': 'commonjs2 lib-c'
  }
}
1
2
3
4
5
6
7
8
9
10

那么打包后的结果如下, ./c.js的代码没有被打包进来.

module.exports =
(function(modules) { // webpackBootstrap
  // ...
	// Load entry module and return exports
	return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
({

 "./c.js":
 (function(module, exports) {

module.exports = require("lib-c");

 }),

 "./src/index.js":
 (function(module, exports, __webpack_require__) {

const c = __webpack_require__(/*! ./c.js */ "./c.js")
module.exports = function b() {
  c()
}
module.exports.namedB = 'namedB'

 })

 });
//# sourceMappingURL=main.js.map
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

假如我们的应用现在要使用这个lib-b,

src/index.js

import defaultB, {namedB} from 'lib-b'
console.log(defaultB)
console.log(namedB)
1
2
3

那现在的场景就是这样, 我们要使用esModule语法去加载一个commonjs模块, 和上文提到的场景'使用esmodule语法导入commonjs模块'是一样的. 但是, 我们使用的这个commonjs模块是webpack打包出来的, 通过npm来安装它, 它有一点点臃肿, 因为它包含了webpack bootstrap代码, 同时它依赖了lib-c这个npm包. lib-b包肯定是会把lib-c包加到其package.json的dependencies列表内的.

打包后的部分代码如下



 




































 








 














{

  "./node_modules/lib-b/index.js": (function (module, exports, __webpack_require__) {

    module.exports =
      (function (modules) { // webpackBootstrap
        // ...
        // Load entry module and return exports
        return __webpack_require__(__webpack_require__.s = "./src/index.js");
      })
      ({

        "./c.js":
          /*!************************!*\
            !*** external "lib-c" ***!
            \************************/
          /*! no static exports found */
          /***/
          (function (module, exports) {

            module.exports = __webpack_require__( /*! lib-c */ "./node_modules/lib-c/index.js");

          }),

        "./src/index.js": (function (module, exports, __webpack_require__) {

          const c = __webpack_require__( /*! ./c.js */ "./c.js")
          module.exports = function b() {
            c()
          }
          module.exports.namedB = 'namedB'

        })

      });


  }),

  "./node_modules/lib-c/index.js": (function (module, exports) {

    module.exports = function c() {
      console.log('defaultC')
    }


  }),

  "./src/index.js": (function (module, __webpack_exports__, __webpack_require__) {

    "use strict";
    __webpack_require__.r(__webpack_exports__);
    /* harmony import */
    var lib_b__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__( /*! lib-b */ "./node_modules/lib-b/index.js");
    /* harmony import */
    var lib_b__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/ __webpack_require__.n(lib_b__WEBPACK_IMPORTED_MODULE_0__);

    console.log(lib_b__WEBPACK_IMPORTED_MODULE_0___default.a)
    console.log(lib_b__WEBPACK_IMPORTED_MODULE_0__["namedB"])
  })

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

可以看到./node_modules/lib-b/index.js这个包是由webpack打包的, 它的代码是有点点臃肿的, 因为它自带webpackBootstrap代码.

而在lib-b包中external掉的lib-c, 其代码也被webpack处理过了,

module.exports = require("lib-c");
1

处理后变为

  module.exports = __webpack_require__( /*! lib-c */ "./node_modules/lib-c/index.js");
1

而这个__webpack_require__并非来自lib-b的webpackBootstrap, 而是来自外层我们当前所打包的这个应用的__webpack_require__

整体代码结构如下,

external_structure

模块的依赖图如下,

external_dep

我们可以发现, 这个模块依赖图和上文提到的场景'使用esmodule语法导入commonjs模块'中的依赖图非常类似. 我们通过打包发布npm包的形式, 达到共享代码, 而这个npm包中的一些通用代码, 通过external的方式避免将代码硬编码到自己的包中, 而最终由webpack来组织这些代码.

无论是通过使用npm包的方式, 还是使用自身代码的方式, 对于webpack打包都没有区别, 无非就是npm包的代码在本地node_modules文件夹中. 对于webpack来说, 他们就是一个个模块, 只是磁盘位置不同.

# 需要注意的

可以看到webpack打包的库lib-b是有一点臃肿的, webpack在生产环境下有ModuleConcatenationPlugin来做优化, rollup在这方面也有一定优势, 所以通常打包一些库文件, rollup是比较受欢迎的.

# Dynamic Import / Code Split

src/index.js

import('./b').then(moduleB => {
  console.log(moduleB.default, moduleB.namedB)
})
1
2
3

src/b.js

export const namedB = 'b'
export default 'defaultB'
1
2

打包后, 得到main.js, 0.js(即src/b.js的代码)

首先看0.js

(window['webpackJsonp'] = window['webpackJsonp'] || []).push([
  [0],
  {
    './src/b.js': function(module, __webpack_exports__, __webpack_require__) {
      'use strict'
      __webpack_require__.r(__webpack_exports__)
      /* harmony export (binding) */ __webpack_require__.d(
        __webpack_exports__,
        'namedB',
        function() {
          return namedB
        }
      )
      const namedB = 'b'
      /* harmony default export */ __webpack_exports__['default'] = 'defaultB'
    }
  }
])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

window['webpackJsonp']在入口js文件的webpackBootstrap代码中有定义, 而且其push`方法也是通过装饰者模式修改过的.

window['webpackJsonp']

 	var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
 	var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
 	jsonpArray.push = webpackJsonpCallback;
 	jsonpArray = jsonpArray.slice();
 	for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
 	var parentJsonpFunction = oldJsonpFunction;
1
2
3
4
5
6

如果全局作用域下有window['webpackJsonp']就直接使用, 否则创建个新的空数组. 并将数组原生push替换成webpackJsonpCallback, 原生push保存在parentJsonpFunction, 装饰者模式.

起初jsonArray, window['webpackJsonp']都指向push被修改过的数组.

在调用jsonpArray = jsonpArray.slice();之后, jsonpArray重新指向了push为原生的数组. 而window['webpackJsonp']push仍旧是被修改过的.

0.js中的代码执行, 将chunk push到window['webpackJsonp']的时候, 就执行了webpackJsonpCallback

入口模块的代码将会在执行import('./b.js')的时候, 将0.js通过script标签插入到DOM中并执行,

{
  './src/index.js': function(module, exports, __webpack_require__) {
    __webpack_require__
      .e(/*! import() */ 0)
      .then(__webpack_require__.bind(null, /*! ./b */ './src/b.js'))
      .then(moduleB => {
        console.log(moduleB.default, moduleB.namedB)
      })
  }
}
1
2
3
4
5
6
7
8
9
10
  1. __webpack_require__.e(/*! import() */ 0)将会执行0.js中的代码, 会将chunk模块添加到modules变量中
  2. __webpack_require__.bind(null, /*! ./b */ './src/b.js')将会加载./b.js中的模块, 并返回模块输出, 模块输出将会传递给下个then的回调函数, 即我们自己手写的回调函数.

# __webpack_require__.e

  // installedChunks中管理了所有已加载/正在加载的chunk
 	var installedChunks = {
 		"main": 0
 	};
 	__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".
       // 该chunk正在加载, installedChunkData为[resolve, reject, promise]
       // 当使用`import()`重复加载同一个模块, 会有可能进到这个逻辑的
 			if(installedChunkData) {
 				promises.push(installedChunkData[2]);
 			} else {
         // chunk未加载, 加载该chunk
 				// setup Promise in chunk cache
 				var promise = new Promise(function(resolve, reject) {
 					installedChunkData = installedChunks[chunkId] = [resolve, reject];
         });
        // 当installedChunks[chunkId][0]调用的时候, 该promise resolve
 				promises.push(installedChunkData[2] = promise);

        // 通过script标签加载code split的代码
 				// start chunk loading
 				var script = document.createElement('script');
 				var onScriptComplete;

 				script.charset = 'utf-8';
        script.timeout = 120;
        // Content-Security-Policy, 见https://webpack.js.org/guides/csp/
 				if (__webpack_require__.nc) {
 					script.setAttribute("nonce", __webpack_require__.nc);
        }
        // 获取chunk路径
 				script.src = jsonpScriptSrc(chunkId);

 				// create error before stack unwound to get useful stacktrace later
         var error = new Error();
         // 当chunk中的代码执行完后, 会执行该回调
 				onScriptComplete = function (event) {
 					// avoid mem leaks in IE.
 					script.onerror = script.onload = null;
           clearTimeout(timeout);
           // 可以想象, 如果chunk正常加载, 正常执行, 其执行代码肯定会将installedChunks[chunkId]赋值为0
          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.\n(' + 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);
 	};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

总结下__webpack_require__.e将会加载chunk代码, 并在installedChunks中记录下该chunk的状态, 若该chunk为已加载, 其为0, 若正在加载, 其为[resolve, reject, promise], __webpack_require__.e返回Promise, 当installedChunks[chunkId][0]被调用的时候, 其变为resolve状态.

加载chunk代码的时候, 将执行webpackJsonpCallback, installedChunks[chunkId][0]将被调用

# webpackJsonpCallback

webpackJsonpCallback

 	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中标记为0, 意味chunk成功加载
 			installedChunks[chunkId] = 0;
 		}
 		for(moduleId in moreModules) {
       // 将chunk中的模块添加到`modules`中, `modules`中记录了应用依赖的模块
 			if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
 				modules[moduleId] = moreModules[moduleId];
 			}
     }
     // 执行原生push方法, 将chunk信息记录到window['webpackJsonp']中
 		if(parentJsonpFunction) parentJsonpFunction(data);

    // 执行installedChunks[chunkId][0], __webpack_require__.e返回的Promise将resolve.
 		while(resolves.length) {
 			resolves.shift()();
 		}

 	};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
    __webpack_require__
      .e(/*! import() */ 0)
      .then(__webpack_require__.bind(null, /*! ./b */ './src/b.js'))
      .then(moduleB => {
        console.log(moduleB.default, moduleB.namedB)
      })
1
2
3
4
5
6

webpackJsonpCallback执行完后, __webpack_require__.bind(null, /*! ./b */ './src/b.js')将执行, 从modules中拿到./src/b.js并执行其中的模块代码. 这样dynamic import的流程就走完了

若我们把b.js该为commonjs模块

src/b.js

module.exports = {
  namedB: 'b'
}
1
2
3

那带出来的代码是类似的, 但是modules有点小区别

{
  './src/index.js': function(module, exports, __webpack_require__) {
    __webpack_require__
      .e(/*! import() */ 0)
      .then(__webpack_require__.t.bind(null, /*! ./b */ './src/b.js', 7))
      .then(moduleB => {
        console.log(moduleB.default, moduleB.namedB)
      })
  }
}
1
2
3
4
5
6
7
8
9
10

执行的是__webpack_require__.t, 而不是__webpack_require__

因为之前有提到加载commonjs模块需要判断其是否有__esModule属性, 而__webpack_require__.t就是为了这个操作

翻看前面的代码, 我们是用__webpack_require__.n__webpack_require__配合处理commonjs模块的, 对于dynamic import, 则使用__webpack_require__.t则更灵活, 因为dynamic import还有更复杂的场景, 比如

let m = 'b'
import(`./${b}.js`)
1
2

# __webpack_require__.t

// create a fake namespace object
// mode & 1: value is a module id, require it
// mode & 2: merge all properties of value into the ns
// mode & 4: return value when already ns object
// mode & 8|1: behave like require
__webpack_require__.t = function(value, mode) {
  if (mode & 1) value = __webpack_require__(value)
  if (mode & 8) return value
  if (mode & 4 && typeof value === 'object' && value && value.__esModule)
    return value
  var ns = Object.create(null)
  __webpack_require__.r(ns)
  Object.defineProperty(ns, 'default', { enumerable: true, value: value })
  if (mode & 2 && typeof value != 'string')
    for (var key in value)
      __webpack_require__.d(
        ns,
        key,
        function(key) {
          return value[key]
        }.bind(null, key)
      )
  return ns
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

__webpack_require__.t.bind(null, /*! ./b */ './src/b.js', 7), mode 7将会执行(mode & 1), (mode & 4), (mode & 2)的代码, 处理源代码为commonjs的模块

mode 9将执行(mode & 1), (mode & 8), mode 9 处理项目中源代码为esModule的那些模块

# flowchart

dynamic_import_flowchart

# 参考

https://zhuanlan.zhihu.com/p/97737035

https://medium.com/webpack/webpack-4-import-and-commonjs-d619d626b655

https://zhuanlan.zhihu.com/p/84204506

https://developer.aliyun.com/article/755252