webapck 模块化原理

368 阅读6分钟

最近在阅读react-loadable源码时,发现import方法返回的结构是一个promise。于是就好奇webpack 是如何实现异步加载文件,并且加载完成后返回了一个promise。遂决定简单写一个demo 一探究竟

同步模块加载

首先准备两个文件

// index.js
const commonjs = require('./commonjs');
console.log('index::::');

// commonjs
module.exports.commonjs = true;

然后运行webpack 打包,生成bundle.js

(function(modules) { // webpackBootstrap
	// 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;
	}
	// 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 = "/dist";
	// Load entry module and return exports
	return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
/************************************************************************/
({

/***/ "./src/commonjs.js":
/*! no static exports found */
/***/ (function(module, exports) {

eval("module.exports.commonjs = true;\n\n//# sourceURL=webpack:///./src/commonjs.js?");

/***/ }),

/***/ "./src/index.js":
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

eval("const commonjs = __webpack_require__(/*! ./commonjs */ \"./src/commonjs.js\")\n\nconsole.log('index.js')\n\n//# sourceURL=webpack:///./src/index.js?");

/***/ })

});

精简代码后如下

(function(modules){
	function __webpack_require__(moduleId) {}
	return __webpack_require__(__webpack_require__.s = "./src/index.js"); 
})({
"./src/commonjs.js": 
/***/ (function(module, exports) {

eval("module.exports.commonjs = true;\n\n//# sourceURL=webpack:///./src/commonjs.js?");

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

eval("const commonjs = __webpack_require__(/*! ./commonjs */ \"./src/commonjs.js\")\n\nconsole.log('index.js')\n\n//# sourceURL=webpack:///./src/index.js?");

/***/ }),
})

由上面文件可知,webpack打包后生成一个自运行函数,自运行函数接受一个modules对象作为参数,modules对象以文件路径为key, 文件内容为value,函数接受moduleexportsrequire等参数。

再看这个自运行函数主要做了什么

  1. 定义一个installedModules对象,缓存各个模块
  2. 定义一个__webpack_require 方法,用于加载各个模块
  3. 使用__webpack_require 加载入口文件,**return __**webpack_require**(__webpack_require__.s = "./src/index.js");**

核心逻辑即定义__webpack_require,然后通过__webpack_require 加载模块代码。

webpack_require加载步骤

  1. 接口moduleId作为参数, 用installedModules 判断是否加载过,加载过就立即返回
  2. 定义一个module,并且赋值给installedModules[moduleId]
  3. 通过modules[moduleId] 运行该模块,并且传入 module, module.exports,__webpack_require等参数。
  4. 返回module.exports

同步加载主要逻辑就这些,下面来看一下函数内的其它代码

	// 通过m属性将自运行函数参数暴露出去
	__webpack_require__.m = modules;
	//通过c属性将加载过的模块暴露出去
	__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 });
		}
	};
	// 定义一个es6的module,用于加载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 });
	};
// 异步加载模块时会用到该方法
__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;
	};
	// 获取模块的默认值,兼容es6 和commonjs
	__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;
	};
	// 暴露一个hasOwnProperty方法
	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
	// 暴露模块的publicPath, 用于静态资源路径
	__webpack_require__.p = "/dist";

上面我们打包了两个commonjs代码,但webpack 还支持打包es6模块的代码,接下来我们就来打包es6模块代码试一下

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

console.log('index.js')

//es6.js
export default es6 = true;

打包后自运行函数无任何变化,但传入的modules发生了变化

eval("__webpack_require__.r(__webpack_exports__);\n/* harmony default export */ __webpack_exports__[\"default\"] = (es6 = true);\n\n");

代码最开始就用__webpack_require__.r 对__webpack_exports__ 进行了一个包装, 通过查看上面__webpack_require__.r的方法得知,r方法在咱们的模块上增加了__esModule属性,后续代码会通过__esModule来进行模块代码。

对比之前的commonjs打包生成的代码

eval("module.exports.commonjs = true;\n\n//# sourceURL=webpack:///./src/commonjs.js?");

得知,webpack在打包es6模块时,会通过__webpack_require__.r给模块代码增加__esModule属性,后续逻辑即通过该属性判断是es6模块 还是commonjs模块

上面我是用commonjs 去引入文件 所以webpack直接调用的__webpack_require__去加载文件,接下来我用es6的方式去试一下

// index.js
import  es6 from './es6'
console.log('index.js');

// es6.js
export default es6 = true;

下面是index.js模块打包后的代码


eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _es6__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./es6 */ \"./src/es6.js\");\n\nconsole.log(_es6__WEBPACK_IMPORTED_MODULE_0__[\"default\"]);\n\n//# sourceURL=webpack:///./src/index.js?");

使用import 引入 模块 该模块也转换为es6module,所以也使用了__webpack_require__.r进行包装,然后还是通过__webpack_require__导入es6模块,并通过.default访问导出的默认值。

接下来用es6模块加载commonjs试一下呢

// index.js
import commonjs from './commonjs'
console.log(commonjs);

// commonjs
module.exports.commonjs = true

打包后生成的index.js代码

eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _commonjs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./commonjs */ \"./src/commonjs.js\");\n/* harmony import */ var _commonjs__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_commonjs__WEBPACK_IMPORTED_MODULE_0__);\n\nconsole.log(_commonjs__WEBPACK_IMPORTED_MODULE_0___default.a);\n\n");

//翻译后
_webpack_require__.r(__webpack_exports__);
var _commonjs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/commonjs.js");
var _commonjs__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(_commonjs__WEBPACK_IMPORTED_MODULE_0__);
console.log(_commonjs__WEBPACK_IMPORTED_MODULE_0___default.a);

可以看到

  1. 通过__webpack_require__ 加载commonjs
  2. webpack_require.n包装返回的模块
  3. 然后通过_commonjs__WEBPACK_IMPORTED_MODULE_0___default.a获取默认值

webpack_require.n 函数可分为两步

  1. 定义一个获取getter, 如果是es6 就返回module.__esModule, 否则直接返回module
  2. 将该getter赋值给getter.a属性,并且返回getter

以上是webpack打包同步加载模块代码,webpack针对es6 模块和commonjs 模块会有一些不同的处理

  1. es6 模块会给module加上__esModule属性
  2. es6 加载commonjs 时 会通过__webpack_require__.n 包装一下返回的模块。

webpack异步加载模块

// index.js
import('./lazy').then((value) => {
    console.log(value)
});
// lazy
module.exports = function() {
    console.log('lazyLoad:::');
}

打包查看文件 生成了一个bundle.js 和一个0.bundle.js

// 0.bundle.js
// 调用window["webpackJsonp"].push 处理modules数据
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{

/***/ "./src/lazy.js":
/*!*********************!*\
  !*** ./src/lazy.js ***!
  \*********************/
/*! no static exports found */
/***/ (function(module, exports) {

eval("module.exports = function() {\n    console.log('lazyLoad:::');\n}\n\n");

/***/ })

查看bundle.js 发现自运行函数多了三块代码

// 异步js 模块加载完成后会调用window["webpackJsonp"].push 也就是该方法
	function webpackJsonpCallback(data) {
		// chunkid
		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]) {
				// 将该异步模块的promise的resolve 存储到数组上去
				resolves.push(installedChunks[chunkId][0]);
			}
			// 赋值为0 表示异步加载模块成功, __webpack_require__.e方法有用到这个值做判断
			installedChunks[chunkId] = 0;
		}
		// 将异步modules 存储到modules对象上去 
		for(moduleId in moreModules) {
			if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
				modules[moduleId] = moreModules[moduleId];
			}
		}
		if(parentJsonpFunction) parentJsonpFunction(data);
	  // 加载成功 执行resolve,走后续流程
		while(resolves.length) {
			resolves.shift()();
		}

	};
	
// 异步加载模块通过该方法加载, 通过jsonp加载
	__webpack_require__.e = function requireEnsure(chunkId) {
		var promises = [];

		var installedChunkData = installedChunks[chunkId];
		
		// 值为0 表示已成功加载过
		if(installedChunkData !== 0) { 

			// 有值表示正在加载,直接pushinstalledChunkData[2]
			if(installedChunkData) { 
				promises.push(installedChunkData[2]);
			} else {
				// 生成一个promise,并将resolve,reject放进installedChunks
				var promise = new Promise(function(resolve, reject) {
					installedChunkData = installedChunks[chunkId] = [resolve, reject];
				});
				// 存储promsie
				promises.push(installedChunkData[2] = promise);

				// 创建script标签
				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();
				// 定义onload onerror
				onScriptComplete = function (event) {
					// avoid mem leaks in IE.
					script.onerror = script.onload = null;
					clearTimeout(timeout);
					var chunk = installedChunks[chunkId];
          // chunk 没被改为0 表示失败 此时chunk 的值为[resolve, reject, promise]
					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;
							// 执行reject 
							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);
			}
		}
		// 执行该promise
		return Promise.all(promises);
	};
	
	// 定义window["webpackJsonp"] 为一个数组
	var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
	var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
	// 重写该数组方法 调用window["webpackJsonp"].push方法时就执行上面定义的webpackJsonpCallback方法 进行模块数据处理
	jsonpArray.push = webpackJsonpCallback;
	jsonpArray = jsonpArray.slice();
	for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
	var parentJsonpFunction = oldJsonpFunction;

webpackJsonpCallback 方法加载代码完成后的执行, 0.bundle.js文件里面会调用到该方法

__webpack_require__.e 通过使用jsonp方式来加载异步模块代码

接下来看index.js是如何调用的

// index.js
eval("__webpack_require__.e(/*! import() */ 0).then(__webpack_require__.t.bind(null, /*! ./lazy */ \"./src/lazy.js\", 7)).then((value) => {\n    console.log(value)\n})\n\n");

// 翻译后
__webpack_require__.e(0)
	.then(__webpack_require__.t.bind(null, "./src/lazy.js\", 7))
	.then((value) => {
			console.log(value)
	});

可以看到调用__webpack_require__.e(0) 后再执行了__webpack_require__.t 去modules 中加载模块

  1. webpack_require.e(0) 中的0 就是chunkid 对应window["webpackJsonp"].push中的第一个参数
  2. webpack_require.t 又会去调用__webpack_require__ 去加载对应的模块, 此时modules经过webpackJsonpCallback的处理 已经包含"./src/lazy.js\"的代码 所以就能获取到该模块代码
  3. 然后调用then 就能拿到该模块的结果

小结:

  1. webpack 兼容处理了commonjs 和 es6模块,通过__esModule来判断是否是es6模块
  2. 异步加载使用jsonp 的方式加载js文件,里面会定义一个promise
  3. 异步代码调用了window["webpackJsonp"].push,该方法被webpackJsonpCallback重写
  4. webpackJsonpCallback 方法会处理加载到的模块代码,然后运行resolve,将控制权交给了__webpack_require__.t ,通过__webpack_require__.t 去运行模块代码