webpack-【打包结果运行原理】

webpack 是一个模块打包器,在它看来,每一个文件都是一个模块。

无论你开发使用的是 CommonJS 规范还是 ES6 模块规范,打包后的文件都统一使用 webpack 自定义的模块规范来管理、加载模块。本文将从一个简单的示例开始,来讲解 webpack 模块加载原理。

CommonJS 规范

假设现在有如下两个文件:

// index.js
const test2 = require('./test2')

function test() {}

test()
test2()
// test2.js
function test2() {}

module.exports = test2

以上两个文件使用 CommonJS 规范来导入导出文件,打包后的代码如下(已经删除了不必要的注释):

(function(modules) { // webpackBootstrap
    // The module cache
    // 01 定义对象用来存放被加载过的模块
	var installedModules = {};

    // The require function
    // 02 下面方法用来替换 require 和 import加载操作, webpack 实现的 require() 函数 
    // 下面的这个方法就是 webpack 当中自定义的,它的核心作用就是返回模块的 exports 
	function __webpack_require__(moduleId) {
        // Check if module is in cache
        // 02-1判断当前缓存中是否已经存在当前加载模块内容,如果模块已经加载过,直接返回缓存
		if(installedModules[moduleId]) {
			return installedModules[moduleId].exports;
		}
        // Create a new module (and put it into the cache)
        // 02-2 如果缓存中不存在当前加载模块,则创建一个新模块,执行模块内容,并加载模块,并放入缓存
		var module = installedModules[moduleId] = {
			i: moduleId,
			l: false,
			exports: {}
		};

        // Execute the module function
        // 02-3 调用当前moduleId对应的模块函数,并完成内容的加载
        // call() 为了将this指向module.exports
		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

        // Flag the module as loaded
        // 02-4 当上述方法调用完成后,将 l 的值设置为 true, 表示当前模块内容已加载完成了
		module.l = true;

		// Return the exports of the module
        // 02-5 加载工作完成后,要将拿到的内容返回至调用位置
		return module.exports;
	}


    // expose the modules object (__webpack_modules__)
    // 03 定义 m 属性用于保存modules ,将所有的模块挂载到 require() 函数上
	__webpack_require__.m = modules;

    // expose the module cache
    // 04 定义 c 属性, 用于保存catch, 将缓存对象挂载到 require() 函数上
	__webpack_require__.c = installedModules;

	
    // Object.prototype.hasOwnProperty.call
    // 05 定义 0 方法,用于判断对象的身上是否存在指定的属性
	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };


	// define getter function for harmony exports
    // 06 定义 d 方法,用来在对象上添加指定的属性,同时给该属性提供一个getter
	__webpack_require__.d = function(exports, name, getter) {
		if(!__webpack_require__.o(exports, name)) {
			Object.defineProperty(exports, name, { enumerable: true, get: getter });
		}
	};

	// define __esModule on exports
    // 07 定义 r 方法,用来标识当前模块是否是ESModule类型
	__webpack_require__.r = function(exports) {
		if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
			Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
		}
		Object.defineProperty(exports, '__esModule', { value: true });
	};

	// 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;
	};

	
     // getDefaultExport function for compatibility with non-harmony modules
    // 08 定义一个 n 方法,用来设置具体的getter
	__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;
	};


	// __webpack_public_path__
    // 09 定义 p 属性,用于保存静态资源访问路径
	__webpack_require__.p = "";


    // Load entry module and return exports
    // 10 调用 _webapck_require_方法执行 模块导入 和 模块加载,加载入口模块,并返回模块对象
	return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
/************************************************************************/
({
  "./src/index.js": (function(module, exports, __webpack_require__) {
    eval("const test2 = __webpack_require__(/*! ./test2 */ \"./src/test2.js\")\r\n\r\nfunction test() {}\r\n\r\ntest()\r\ntest2()\n\n//# sourceURL=webpack:///./src/index.js?");
  }),
  
  "./src/test2.js": (function(module, exports) {
    eval("function test2() {}\r\n\r\nmodule.exports = test2\n\n//# sourceURL=webpack:///./src/test2.js?");
  })
});

可以看到 webpack 实现的模块加载系统非常简单,仅仅只有一百行代码。

1:   打包后的代码其实是一个立即执行函数,传入的参数是一个对象。

      这个对象 key:当前被加载模块名文件路径,以文件内容为 value,它包含了所有打包后的模块。

{
  "./src/index.js": (function(module, exports, __webpack_require__) {
    eval("const test2 = __webpack_require__(/*! ./test2 */ \"./src/test2.js\")\r\n\r\nfunction test() {}\r\n\r\ntest()\r\ntest2()\n\n//# sourceURL=webpack:///./src/index.js?");
  }),
  
  "./src/test2.js": (function(module, exports) {
    eval("function test2() {}\r\n\r\nmodule.exports = test2\n\n//# sourceURL=webpack:///./src/test2.js?");
  })
}

将这个立即函数化简一下,相当于:

(function(modules){
	// ...
})({
	path1: function1,
	path2: function2
})

再看一下这个立即函数做了什么:

  1. 定义了一个模块缓存对象 installedModules,作用是缓存已经加载过的模块。
  2. 定义了一个模块加载函数 __webpack_require__()
  3. ... 省略一些其他代码。
  4. 使用 __webpack_require__() 加载入口模块。

其中的核心就是 __webpack_require__() 函数,它接收的参数是 moduleId,其实就是文件路径。

它的执行过程如下:

  1. 判断模块是否有缓存,如果有则返回缓存模块的 export 对象,即 module.exports
  2. 新建一个模块 module,并放入缓存。
  3. 执行文件路径对应的模块函数。
  4. 将这个新建的模块标识为已加载。
  5. 执行完模块后,返回该模块的 exports 对象。
   // The require function
   // webpack 实现的 require() 函数
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;
}

从上述代码可以看到,在执行模块函数时传入了三个参数,分别为 modulemodule.exports__webpack_require__

其中 modulemodule.exports 的作用和 CommonJS 中的 modulemodule.exports 的作用是一样的,而 __webpack_require__ 相当于 CommonJS 中的 require

在立即函数的最后,使用了 __webpack_require__() 加载入口模块。并传入了入口模块的路径 ./src/index.js

__webpack_require__(__webpack_require__.s = "./src/index.js");

我们再来分析一下入口模块的内容。

(function(module, exports, __webpack_require__) {
    eval("const test2 = __webpack_require__(/*! ./test2 */ \"./src/test2.js\")\r\n\r\nfunction test() {}\r\n\r\ntest()\r\ntest2()\n\n//# sourceURL=webpack:///./src/index.js?");
  })

入口模块函数的参数正好是刚才所说的那三个参数,而 eval 函数的内容美化一下后和下面内容一样:

const test2 = __webpack_require__("./src/test2.js")
function test() {}
test()
test2()
//# sourceURL=webpack:///./src/index.js?

将打包后的模块代码和原模块的代码进行对比,可以发现仅有一个地方发生了变化,那就是 require 变成了 __webpack_require__

再看一下 test2.js 的代码:

function test2() {}
module.exports = test2
//# sourceURL=webpack:///./src/test2.js?

从刚才的分析可知,__webpack_require__() 加载模块后,会先执行模块对应的函数,然后返回该模块的 exports 对象。而 test2.js 的导出对象 module.exports 就是 test2() 函数。所以入口模块能通过 __webpack_require__() 引入 test2() 函数并执行。

到目前为止可以发现 webpack 自定义的模块规范完美适配 CommonJS 规范。

ES6 module

将刚才用 CommonJS 规范编写的两个文件换成用 ES6 module 规范来写,再执行打包。

// index.js
import test2 from './test2'

function test() {}

test()
test2()
// test2.js
export default function test2() {}

使用 ES6 module 规范打包后的代码和使用 CommonJS 规范打包后的代码绝大部分都是一样的。

一样的地方是指 webpack 自定义模块规范的代码一样,唯一不同的是上面两个文件打包后的代码不同。

{
 	"./src/index.js":(function(module, __webpack_exports__, __webpack_require__) {
		"use strict";
		eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _test2__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./test2 */ \"./src/test2.js\");\n\r\n\r\nfunction test() {}\r\n\r\ntest()\r\nObject(_test2__WEBPACK_IMPORTED_MODULE_0__[\"default\"])()\n\n//# sourceURL=webpack:///./src/index.js?");
	}),
	
	"./src/test2.js": (function(module, __webpack_exports__, __webpack_require__) {
		"use strict";
		eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return test2; });\nfunction test2() {}\n\n//# sourceURL=webpack:///./src/test2.js?");
	})
}

可以看到传入的第二个参数是 __webpack_exports__,而 CommonJS 规范对应的第二个参数是 exports。将这两个模块代码的内容美化一下:

// index.js
__webpack_require__.r(__webpack_exports__);
 var _test2__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/test2.js");
 function test() {}
 test()
 Object(_test2__WEBPACK_IMPORTED_MODULE_0__["default"])()
 //# sourceURL=webpack:///./src/index.js?
// test2.js
 __webpack_require__.r(__webpack_exports__);
 __webpack_require__.d(__webpack_exports__, "default", function() { return test2; });
 function test2() {}
 //# sourceURL=webpack:///./src/test2.js?

可以发现,在每个模块的开头都执行了一个 __webpack_require__.r(__webpack_exports__) 语句。并且 test2.js 还多了一个 __webpack_require__.d() 函数。

我们先来看看 __webpack_require__.r() 和 __webpack_require__.d() 是什么。

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 });
	}
};

原来 __webpack_require__.d() 是给 __webpack_exports__ 定义导出变量用的。例如下面这行代码:

__webpack_require__.d(__webpack_exports__, "default", function() { return test2; });

它的作用相当于 __webpack_exports__["default"] = test2。这个 "default" 是因为你使用 export default 来导出函数,如果这样导出函数:

export function test2() {}

它就会变成 __webpack_require__.d(__webpack_exports__, "test2", function() { return test2; });

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 });
};

__webpack_require__.r() 函数的作用是给 __webpack_exports__ 添加一个 __esModule 为 true 的属性,表示这是一个 ES6 module

添加这个属性有什么用呢?

主要是为了处理混合使用 ES6 moduleCommonJS 的情况。

例如导出使用 CommonJS module.export = test2 导出函数,导入使用 ES6 module import test2 from './test2

打包后的代码如下:

// index.js
__webpack_require__.r(__webpack_exports__);
var _test2__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/test2.js");
var _test2__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(_test2__WEBPACK_IMPORTED_MODULE_0__);
function test() {}
test()
_test2__WEBPACK_IMPORTED_MODULE_0___default()()
//# sourceURL=webpack:///./src/index.js?
// test2.js
 function test2() {}
 module.exports = test2
 //# sourceURL=webpack:///./src/test2.js?

从上述代码可以发现,又多了一个 __webpack_require__.n() 函数:

__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. 将 __webpack_exports__ 导出对象标识为 ES6 module
  2. 加载 test2.js 模块,并将该模块的导出对象作为参数传入 __webpack_require__.n() 函数。
  3. __webpack_require__.n 分析该 export 对象是否是 ES6 module,如果是则返回 module['default'] 即 export default 对应的变量。如果不是 ES6 module 则直接返回 export

按需加载【文件懒加载】

按需加载,也叫异步加载、动态导入,即只在有需要的时候才去下载相应的资源文件。

webpack 中可以使用 import 和 require.ensure 来引入需要动态导入的代码,例如下面这个示例:

// index.js
function test() {}

test()


let oBtn = document.getElementById('btn')

oBtn.addEventListener('click', function () {
    // 动态导入模块(懒加载单个文件)
    import('./test2').then((test2) => {
        console.log(test2)
    })
})
// test2.js
export default function test2() {return "懒加载导出内容"}
index.html

    

其中使用 import 导入的 test2.js 文件在打包时会被单独打包成一个文件,而不是和 index.js 一起打包到 bundle.js

在这里插入图片描述
这个 0.bundle.js 对应的代码就是动态导入的 test2.js 的代码。 

接下来看看这两个打包文件的内容:

// bundle.js
(function(modules) { // webpackBootstrap
	// install a JSONP callback for chunk loading
	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];
			}
		}
		if(parentJsonpFunction) parentJsonpFunction(data);

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

	};


	// 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 = {
		"main": 0
	};

	// script path function
	function jsonpScriptSrc(chunkId) {
		return __webpack_require__.p + "" + chunkId + ".bundle.js"
	}

	// 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;
	}

	// This file contains only the entry chunk.
	// The chunk loading function for additional chunks
	__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.\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);
	};

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

	// expose the module cache
	__webpack_require__.c = installedModules;

	// 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 });
		}
	};

	// 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 });
	};

	// 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;
	};

	// 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;
	};

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

	// __webpack_public_path__
	__webpack_require__.p = "";

	// on error function for async loading
	__webpack_require__.oe = function(err) { console.error(err); throw err; };

	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;

	// Load entry module and return exports
	return __webpack_require__(__webpack_require__.s = "./src/index.js");
})({
  "./src/index.js":
  (function(module, exports, __webpack_require__) {
     eval("function test() {}
       test()
       let oBtn = document.getElementById('btn')
       oBtn.addEventListener('click', function () {
          __webpack_require__.e(/*! import() */ 0).then(__webpack_require__t.bind(null, /*!  ./test2 */"./src/test2.js", 7)).then((test2) => {
             console.log(test2)
          })
       }
       //# sourceURL=webpack:///./src/index.js?"
     );
  })
});
// 0.bundle.js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push(
  [
    [0],
    {
      "./src/test2.js":(function(module, __webpack_exports__, __webpack_require__) {
        "use strict";
        eval("__webpack_require__.r(__webpack_exports__);
            __webpack_require__.d(__webpack_exports__, \"default\", function() { 
                 return test2; 
            });
            function test2() {
                return "懒加载导出内容"
            }
            //# sourceURL=webpack:///./src/test2.js?"
        );
      })
    }
  ]
);

这次打包的代码量有点膨胀,bundle.js 代码居然有 200 行。我们来看看相比于同步加载的 webpack 模块规范,它有哪些不同:

  1. 定义了一个对象 installedChunks,作用是缓存动态模块。
  2. 定义了一个辅助函数 jsonpScriptSrc(),作用是根据模块 ID 生成 URL
  3. 定义了两个新的核心函数 __webpack_require__.e() 和 webpackJsonpCallback()
  4. 定义了一个全局变量 window["webpackJsonp"] = [],它的作用是存储需要动态导入的模块。
  5. 重写 window["webpackJsonp"] 数组的 push() 方法为 webpackJsonpCallback()。也就是说 window["webpackJsonp"].push() 其实执行的是 webpackJsonpCallback()

而从 0.bundle.js 文件可以发现,它正是使用 window["webpackJsonp"].push() 来放入动态模块的。动态模块数据项有两个值,第一个是 [0],它是模块的 ID;第二个值是模块的路径名和模块内容。

然后我们再看一下打包后的入口模块的代码,经过美化后:

function test() {}
test()
__webpack_require__.e(0).then(__webpack_require__.bind(null, "./src/test2.js"))
//# sourceURL=webpack:///./src/index.js?

原来模块代码中的 import('./test2') 被翻译成了 __webpack_require__.e(0).then(__webpack_require__.bind(null, "./src/test2.js"))

那 __webpack_require__.e() 的作用是什么呢?

webpack_require.e()

__webpack_require__.e = function requireEnsure(chunkId) {
	var promises = [];
	// JSONP chunk loading for javascript
	var installedChunkData = installedChunks[chunkId];
    
    //01 先查看该模块 ID 对应缓存的值是否为 0,0 代表已经加载成功了,第一次取值为 undefined。
	if(installedChunkData !== 0) { // 0 means "already installed".
		// a Promise means "currently loading".
        // 02 如果不为 0 并且不是 undefined 代表已经是加载中的状态,然后将这个加载中的 Promise 推入 promises 数组
		if(installedChunkData) {
			promises.push(installedChunkData[2]);
		} else {
             // setup Promise in chunk cache
            // 03 如果不为 0 并且是 undefined 就新建一个 Promise,用于加载需要动态导入的模块。	
			var promise = new Promise(function(resolve, reject) {
				installedChunkData = installedChunks[chunkId] = [resolve, reject];
			});
			promises.push(installedChunkData[2] = promise);

			// start chunk loading
           // 04 生成一个 script 标签
			var script = document.createElement('script');
			var onScriptComplete;

			script.charset = 'utf-8';
			script.timeout = 120; // 06 为这个 script 标签设置一个 2 分钟的超时时间
			if (__webpack_require__.nc) {
				script.setAttribute("nonce", __webpack_require__.nc);
			}
            // 05 URL 使用 jsonpScriptSrc(chunkId) 生成,即需要动态导入模块的 URL
			script.src = jsonpScriptSrc(chunkId);

			// create error before stack unwound to get useful stacktrace later
			var error = new Error();
           // 07 设置onScriptComplete() 函数,用于处理超时错误
			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.\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;
            // 08 然后添加到页面中,开始加载模块
			document.head.appendChild(script);
		}
	}
	return Promise.all(promises);
};

它的处理逻辑如下:

  1. 先查看该模块 ID 对应缓存的值是否为 00 代表已经加载成功了,第一次取值为 undefined
  2. 如果不为 0 并且不是 undefined 代表已经是加载中的状态。然后将这个加载中的 Promise 推入 promises 数组。
  3. 如果不为 0 并且是 undefined 就新建一个 Promise,用于加载需要动态导入的模块。
  4. 生成一个 script 标签,URL 使用 jsonpScriptSrc(chunkId) 生成,即需要动态导入模块的 URL
  5. 为这个 script 标签设置一个 2 分钟的超时时间,并设置一个 onScriptComplete() 函数,用于处理超时错误。
  6. 然后添加到页面中 document.head.appendChild(script),开始加载模块。
  7. 返回 promises 数组。

JS 文件下载完成后,会自动执行文件内容。

也就是说下载完 0.bundle.js 后,会执行 window["webpackJsonp"].push()

由于 window["webpackJsonp"].push() 已被重置为 webpackJsonpCallback() 函数。

所以这一操作就是执行 webpackJsonpCallback() 

接下来我们看看 webpackJsonpCallback() 做了哪些事情。

webpackJsonpCallback()

     对这个模块 ID 对应的 Promise 执行 resolve()

     同时将缓存对象中的值置为 0,表示已经加载完成了。

     相比于 __webpack_require__.e(),这个函数还是挺好理解的。

小结

总的来说,动态导入的逻辑如下:

  1. 重写 window["webpackJsonp"].push() 方法。
  2. 入口模块使用 __webpack_require__.e() 下载动态资源。
  3. 资源下载完成后执行 window["webpackJsonp"].push(),即 webpackJsonpCallback()
  4. 将资源标识为 0,代表已经加载完成。由于加载模块使用的是 Promise,所以要执行 resolve()
  5. 再看一下入口模块的加载代码 __webpack_require__.e(0).then(__webpack_require__.bind(null, "./src/test2.js"))
  6. ,下载完成后执行 then() 方法,调用 __webpack_require__() 真正开始加载代码,__webpack_require__() 在上文已经讲解过,如果不了解,建议再阅读一遍。

手写webpack打包文件,myBundle.js

(function (modules) {
    // 01 定义对象用于将来缓存被加载过的模块
    let installedModules = {}
  
    // 02 定义一个 __webpack_require__ 方法来替换 import require 加载操作
    function __webpack_require__(moduleId) {
      // 2-1 判断当前缓存中是否存在要被加载的模块内容,如果存在则直接返回
      if (installedModules[moduleId]) {
        return installedModules[moduleId].exports
      }
  
      // 2-2 如果当前缓存中不存在则需要我们自己定义{} 执行被导入的模块内容加载
      let module = installedModules[moduleId] = {
        i: moduleId,
        l: false,
        exports: {}
      }
  
      // 2-3 调用当前 moduleId 对应的函数,然后完成内容的加载
      modules[moduleId].call(module.exports, module, module.exports, __webpack_require__)
  
      // 2-4 当上述的方法调用完成之后,我们就可以修改 l 的值用于表示当前模块内容已经加载完成了
      module.l = true
  
      // 2-5 加载工作完成之后,要将拿回来的内容返回至调用的位置 
      return module.exports
    }
    
    // 14 webpackJsonpCallback 实现:合并模块定义,改变 promise 状态执行后续行为
    function webpackJsonpCallback (data) {
         // 01 获取需要被动抬加载的模块 id
         let chunkIds = data[0]
 
         // 02 获取需要被动加载的模块依赖关系对象
         let moreModules = data[1]
         let chunkId, resolves = []
      
         // 03 循环判断 chunkIds 里对应的模块内容是否已完成加载
         for (let i; i < chunkIds.length; i++) {
              chunkId = chunkIds[i]
             if (Object.prototype.hasOwnProperty.call(inStalledChunks, chunkId) && installedChunks[chunkId]) {
                resolves.push(installedChunks[chunkId][0])
             }
          // 更新当前的 chunk 状态
         installedChunks[chunkId] = 0
      }
      for (moduleId in moreModules) {
        if (Object.prototype.hasOwnProperty.call(moreModules, moduleId) && moreModules[moduleId]) {
           modules[moduleId] = moreModules[moduleId]
        }
      }
      while (resolves.length) {
        resolves.shift()()
      }
    }

    
    // 15 定义 inStalledChunks 用于标记某个 chunkId 对应的 chunk 是否完成了加载
    // undefined: 没有被加载;null: 预加载; Promise:加载中; 0: 被加载过;
    var installedChunks = {
 		"main": 0
 	}
    
    // 17 定义jsonpScriptSrc 实现 src 的加载
    function jsonpScriptSrc(chunkId) {
 		return __webpack_require__.p + "" + chunkId + ".built.js"
 	}
     
    // 16 定义e方法用于实现: 利用jsonp来加载内容, promise 来实现异步加载操作
    __webpack_require__.e = function requireEnsure(chunkId) {
         // 00 定义一个数组,用来存放 promise
         var promises = [];
	     // 获取 chunkId 对于的 chunk是否已经完成了加载
	     var installedChunkData = installedChunks[chunkId];
    
         //01 先查看该模块 ID 对应缓存的值是否为 0,0 代表已经加载成功了,第一次取值为 undefined。
	     if(installedChunkData !== 0) { // 0 means "already installed".
            // 02 如果不为 0 并且不是 undefined 代表已经是加载中的状态,然后将这个加载中的 Promise 推入 promises 数组
		 if(installedChunkData) {
			promises.push(installedChunkData[2]);
		 } else {
            // 03 如果不为 0 并且是 undefined 就新建一个 Promise,用于加载需要动态导入的模块。	
			var promise = new Promise(function(resolve, reject) {
				installedChunkData = installedChunks[chunkId] = [resolve, reject];
			});
			promises.push(installedChunkData[2] = promise);
 
           // 04 创建一个 script 标签
			var script = document.createElement('script');
			var onScriptComplete;
 
			script.charset = 'utf-8';
			script.timeout = 120; // 06 为这个 script 标签设置一个 2 分钟的超时时间
			if (__webpack_require__.nc) {
				script.setAttribute("nonce", __webpack_require__.nc);
			}
            // 05 URL 使用 jsonpScriptSrc(chunkId) 生成,即需要动态导入模块的 URL
			script.src = jsonpScriptSrc(chunkId);
 
			// create error before stack unwound to get useful stacktrace later
			var error = new Error();
           // 07 设置onScriptComplete() 函数,用于处理超时错误
			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.\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;
            // 08 然后添加到页面中,开始加载模块
			document.head.appendChild(script);
		}
	  }
	  return Promise.all(promises);
   }
  
    // 03 定义 m 属性用于保存 modules 
    __webpack_require__.m = modules
  
    // 04 定义 c 属性用于保存 cache 
    __webpack_require__.c = installedModules
  
    // 05 定义 o 方法用于判断对象的身上是否存在指定的属性
    __webpack_require__.o = function (object, property) {
      return Object.prototype.hasOwnProperty(object, property)
    }
  
    // 06 定义 d 方法用于在对象的身上添加指定的属性,同时给该属性提供一个 getter 
    __webpack_require__.d = function (exports, name, getter) {
      if (!__webpack_require__.o(exports, name)) {
        Object.defineProperty(exports, name, { enumerable: true, get: getter })
      }
    }
  
    // 07 定义 r 方法用于标识当前模块是 es6 类型
    __webpack_require__.r = function (exports) {
      if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
        Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" })
      }
      Object.defineProperty(exports, '__esModule', { value: true })
    }
  
    // 08 定义 n 方法,用于设置具体的 getter 
    __webpack_require__.n = function (module) {
      let getter = module && module.__esModule ?
        function getDefault() { return module['default'] } :
        function getModuleExports() { return module }
  
      __webpack_require__.d(getter, 'a', getter)
  
      return getter
    }

    // 011 定义t方法 用于加载指定 value 的模块内容, 之后对内容进行处理并返回
    __webpack_require__t = function (value, mode) {
        /**
         * 01: 接收2个参数,一个是value,一般用于表达被加载的模块id, 第二个值 mode 是一个二进制的数值
         * 02: t 方法内部做的第一件事情就是调用自定义的 require 方法,加载value对应的模块内容(value 一般就是模块 id),
         *     加载之后的内容又重新赋值给 value
         * 03: 当获取到这个value 值之后,余下的 8, 4,ns , 2都是对当前的内容进行加工处理,然后返回使用
         * 04: 当 mode & 8 成立是直接将 value 返回 (commonJS)
         * 05: 当 mode & 4 成立是直接将 value 返回(esModule)
         * 
         * 06: 当上述条件都不成立时,还是要继续处理 value.,定义一个 ns{},
         *     06-1: 如果拿到value是一个可以直接使用的内容,例如 是一个字符串,将它挂载到 ns 的 default 属性上
         */
         if (mode & 1) {
            value = __webpack_require__(value)
         }

         if (mode & 8) return value

         if ((mode & 4) && typeof value == 'object' && value && value.__esModule){
            return value
         }
         
         // 当 8 和 4 都没有成立则需要定义 ns 来通过 default 属性返回内容
         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
    }
  
    // 09 定义 P 属性,用于保存资源访问路径
    __webpack_require__.p = ""
    
    // 定义变量,存放数组
    let jsonpArray = window['webpackJsonp'] = window['webpackJsonp'] || []
    
    // 12 保存原生的push方法
    let oldJsonpFunction = jsonpArray.push.bind(jsonpArray)

    // 13 重写原生push
    jsonpArray.push = webpackJsonpCallback
  
    // 10 调用 __webpack_require__ 方法执行模块导入与加载操作
    return __webpack_require__(__webpack_require__.s = './src/index.js')
  
  })
    ({
      "./src/index.js":
        (function (module, exports, __webpack_require__) {
          let name = __webpack_require__.t(/*! ./login.js */ "./src/login.js", 0b0111)
          console.log('index.js执行')
          console.log(name)
        }),
      "./src/login.js":
        (function (module, exports) {
          module.exports = '好好学习'
        })
    })

你可能感兴趣的:(webpack,webpack,前端)