webpack 是一个模块打包器,在它看来,每一个文件都是一个模块。
无论你开发应用的是 CommonJS 标准还是 ES6 模块标准,打包后的文件都对立应用 webpack 自定义的模块标准来治理、加载模块。本文将从一个简略的示例开始,来解说 webpack 模块加载原理。
CommonJS 标准
假如当初有如下两个文件:
// index.jsconst test2 = require('./test2')function test() {}test()test2()
// test2.jsfunction test2() {}module.exports = test2
以上两个文件应用 CommonJS 标准来导入导出文件,打包后的代码如下(曾经删除了不必要的正文):
(function(modules) { // webpackBootstrap // The module cache // 模块缓存对象 var installedModules = {}; // 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; } // expose the modules object (__webpack_modules__) // 将所有的模块挂载到 require() 函数上 __webpack_require__.m = modules; // expose the module cache // 将缓存对象挂载到 require() 函数上 __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 = ""; // Load entry module and return exports // 加载入口模块,并返回模块对象 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 实现的模块加载零碎非常简单,仅仅只有一百行代码。
打包后的代码其实是一个立刻执行函数,传入的参数是一个对象。这个对象以文件门路为 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})
再看一下这个立刻函数做了什么:
- 定义了一个模块缓存对象
installedModules
,作用是缓存曾经加载过的模块。 - 定义了一个模块加载函数
__webpack_require__()
。 - ... 省略一些其余代码。
- 应用
__webpack_require__()
加载入口模块。
其中的外围就是 __webpack_require__()
函数,它接管的参数是 moduleId
,其实就是文件门路。
它的执行过程如下:
- 判断模块是否有缓存,如果有则返回缓存模块的
export
对象,即module.exports
。 - 新建一个模块
module
,并放入缓存。 - 执行文件门路对应的模块函数。
- 将这个新建的模块标识为已加载。
- 执行完模块后,返回该模块的
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;}
从上述代码能够看到,在执行模块函数时传入了三个参数,别离为 module
、module.exports
、__webpack_require__
。
其中 module
、module.exports
的作用和 CommonJS 中的 module
、module.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.jsimport test2 from './test2'function test() {}test()test2()
// test2.jsexport 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 module 和 CommonJS 的状况。
例如导出应用 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;};
先来剖析一下入口模块的解决逻辑:
- 将
__webpack_exports__
导出对象标识为 ES6 module。 - 加载
test2.js
模块,并将该模块的导出对象作为参数传入__webpack_require__.n()
函数。 __webpack_require__.n
剖析该export
对象是否是 ES6 module,如果是则返回module['default']
即export default
对应的变量。如果不是 ES6 module 则间接返回export
。
按需加载
按需加载,也叫异步加载、动静导入,即只在有须要的时候才去下载相应的资源文件。
在 webpack 中能够应用 import
和 require.ensure
来引入须要动静导入的代码,例如上面这个示例:
// index.jsfunction test() {}test()import('./test2')
// test2.jsexport default function test2() {}
其中应用 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() {}\r\n\r\ntest()\r\n__webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./test2 */ \"./src/test2.js\"))\n\n//# 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__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return test2; });\nfunction test2() {}\n\n//# sourceURL=webpack:///./src/test2.js?"); }) } ]);
这次打包的代码量有点收缩,bundle.js
代码竟然有 200 行。咱们来看看相比于同步加载的 webpack 模块标准,它有哪些不同:
- 定义了一个对象
installedChunks
,作用是缓存动静模块。 - 定义了一个辅助函数
jsonpScriptSrc()
,作用是依据模块 ID 生成 URL。 - 定义了两个新的外围函数
__webpack_require__.e()
和webpackJsonpCallback()
。 - 定义了一个全局变量
window["webpackJsonp"] = []
,它的作用是存储须要动静导入的模块。 - 重写
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]; 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);};
它的解决逻辑如下:
- 先查看该模块 ID 对应缓存的值是否为 0,0 代表曾经加载胜利了,第一次取值为
undefined
。 - 如果不为 0 并且不是
undefined
代表曾经是加载中的状态。而后将这个加载中的 Promise 推入promises
数组。 - 如果不为 0 并且是
undefined
就新建一个 Promise,用于加载须要动静导入的模块。 - 生成一个
script
标签,URL 应用jsonpScriptSrc(chunkId)
生成,即须要动静导入模块的 URL。 - 为这个
script
标签设置一个 2 分钟的超时工夫,并设置一个onScriptComplete()
函数,用于解决超时谬误。 - 而后增加到页面中
document.head.appendChild(script)
,开始加载模块。 - 返回
promises
数组。
当 JS 文件下载实现后,会主动执行文件内容。也就是说下载完 0.bundle.js
后,会执行 window["webpackJsonp"].push()
。
因为 window["webpackJsonp"].push()
已被重置为 webpackJsonpCallback()
函数。所以这一操作就是执行 webpackJsonpCallback()
,接下来咱们看看 webpackJsonpCallback()
做了哪些事件。
webpackJsonpCallback()
对这个模块 ID 对应的 Promise 执行 resolve()
,同时将缓存对象中的值置为 0,示意曾经加载实现了。相比于 __webpack_require__.e()
,这个函数还是挺好了解的。
小结
总的来说,动静导入的逻辑如下:
- 重写
window["webpackJsonp"].push()
办法。 - 入口模块应用
__webpack_require__.e()
下载动静资源。 - 资源下载实现后执行
window["webpackJsonp"].push()
,即webpackJsonpCallback()
。 - 将资源标识为 0,代表曾经加载实现。因为加载模块应用的是 Promise,所以要执行
resolve()
。 - 再看一下入口模块的加载代码
__webpack_require__.e(0).then(__webpack_require__.bind(null, "./src/test2.js"))
,下载实现后执行then()
办法,调用__webpack_require__()
真正开始加载代码,__webpack_require__()
在上文曾经解说过,如果不理解,倡议再浏览一遍。