webpack启动代码源码解读
欢迎关注我的公众号睿Talk
,获取我最新的文章:
一、前言
虽然每天都在用webpack,但一直觉得隔着一层神秘的面纱,对它的工作原理一直似懂非懂。它是如何用原生JS实现模块间的依赖管理的呢?对于按需加载的模块,它是通过什么方式动态获取的?打包完成后那一堆/******/
开头的代码是用来干什么的?本文将围绕以上3个问题,对照着源码给出解答。
如果你对webpack的配置调优感兴趣,可以看看我之前写的这篇文章:webpack调优总结
二、模块管理
先写一个简单的JS文件,看看webpack打包后会是什么样子:
// main.js console.log('Hello Dickens'); // webpack.config.js const path = require('path'); module.exports = { entry: './main.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') } };
在当前目录下运行webpack
,会在dist目录下面生成打包好的bundle.js文件。去掉不必要的干扰后,核心代码如下:
// webpack启动代码 (function (modules) { // 模块缓存对象 var installedModules = {}; // webpack实现的require函数 function __webpack_require__(moduleId) { // 检查缓存对象,看模块是否加载过 if (installedModules[moduleId]) { return installedModules[moduleId].exports; } // 创建一个新的模块缓存,再存入缓存对象 var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} }; // 执行模块代码 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); // 将模块标识为已加载 module.l = true; // 返回export的内容 return module.exports; } ... // 加载入口模块 return __webpack_require__(__webpack_require__.s = 0); }) ([ /* 0 */ (function (module, exports) { console.log('Hello Dickens'); }) ]);
代码是一个立即执行函数,参数modules
是由各个模块组成的数组,本例子只有一个编号为0的模块,由一个函数包裹着,注入了module
和exports
2个变量(本例没用到)。
核心代码是__webpack_require__
这个函数,它的功能是根据传入的模块id,返回模块export的内容。模块id由webpack根据文件的依赖关系自动生成,是一个从0开始递增的数字,入口文件的id为0。所有的模块都会被webpack用一个函数包裹,按照顺序存入上面提到的数组实参当中。
模块export的内容会被缓存在installedModules
中。当获取模块内容的时候,如果已经加载过,则直接从缓存返回,否则根据id从modules
形参中取出模块内容并执行,同时将结果保存到缓存对象当中。缓存对象数据结构如下:
我们再添加一个文件,在入口文件处导入,再来看看生成的启动文件是怎样的。
// main.js import logger from './logger'; console.log('Hello Dickens'); logger(); //logger.js export default function log() { console.log('Log from logger'); }
启动文件的模块数组:
[ /* 0 */ (function (module, __webpack_exports__, __webpack_require__) { "use strict"; Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__logger__ = __webpack_require__(1); console.log('Hello Dickens'); Object(__WEBPACK_IMPORTED_MODULE_0__logger__["a" /* default */ ])(); }), /* 1 */ (function (module, __webpack_exports__, __webpack_require__) { "use strict"; /* harmony export (immutable) */ __webpack_exports__["a"] = log; function log() { console.log('Log from logger'); } }) ]
可以看到现在有2个模块,每个模块的包裹函数都传入了module, __webpack_exports__, __webpack_require__
三个参数,它们是通过上文提到的__webpack_require__
注入的:
// 执行模块代码 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
执行的结果也保存在缓存对象中了。
执行流程如下图所示:
三、按需加载
再对代码进行改造,来研究webpack是如何实现动态加载的:
// main.js console.log('Hello Dickens'); import('./logger').then(logger => { logger.default(); });
logger文件保持不变,编译后比之前多出了1个chunk。
bundle_asy的内容如下:
(function (modules) { // 加载成功后的JSONP回调函数 var parentJsonpFunction = window["webpackJsonp"]; // 加载成功后的JSONP回调函数 window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) { var moduleId, chunkId, i = 0, resolves = [], result; for (; i < chunkIds.length; i++) { chunkId = chunkIds[i]; // installedChunks[chunkId]不为0且不为undefined,将其放入加载成功数组 if (installedChunks[chunkId]) { // promise的resolve resolves.push(installedChunks[chunkId][0]); } // 标记模块加载完成 installedChunks[chunkId] = 0; } // 将动态加载的模块添加到modules数组中,以供后续的require使用 for (moduleId in moreModules) { if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { modules[moduleId] = moreModules[moduleId]; } } if (parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules); while (resolves.length) { resolves.shift()(); } }; // 模块缓存对象 var installedModules = {}; // 记录正在加载和已经加载的chunk的对象,0表示已经加载成功 // 1是当前模块的编号,已加载完成 var installedChunks = { 1: 0 }; // require函数,跟上面的一样 function __webpack_require__(moduleId) { if (installedModules[moduleId]) { return installedModules[moduleId].exports; } var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} }; modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); module.l = true; return module.exports; } // 按需加载,通过动态添加script标签实现 __webpack_require__.e = function requireEnsure(chunkId) { var installedChunkData = installedChunks[chunkId]; // chunk已经加载成功 if (installedChunkData === 0) { return new Promise(function (resolve) { resolve(); }); } // 加载中,返回之前创建的promise(数组下标为2) if (installedChunkData) { return installedChunkData[2]; } // 将promise相关函数保持到installedChunks中方便后续resolve或reject var promise = new Promise(function (resolve, reject) { installedChunkData = installedChunks[chunkId] = [resolve, reject]; }); installedChunkData[2] = promise; // 启动chunk的异步加载 var head = document.getElementsByTagName('head')[0]; var script = document.createElement('script'); script.type = 'text/javascript'; script.charset = 'utf-8'; script.async = true; script.timeout = 120000; if (__webpack_require__.nc) { script.setAttribute("nonce", __webpack_require__.nc); } script.src = __webpack_require__.p + "" + chunkId + ".bundle_async.js"; script.onerror = script.onload = onScriptComplete; var timeout = setTimeout(onScriptComplete, 120000); function onScriptComplete() { script.onerror = script.onload = null; clearTimeout(timeout); var chunk = installedChunks[chunkId]; // 正常的流程,模块加载完后会调用webpackJsonp方法,将chunk置为0 // 如果不为0,则可能是加载失败或者超时 if (chunk !== 0) { if (chunk) { // 调用promise的reject chunk[1](new Error('Loading chunk ' + chunkId + ' failed.')); } installedChunks[chunkId] = undefined; } }; head.appendChild(script); return promise; }; ... // 加载入口模块 return __webpack_require__(__webpack_require__.s = 0); }) ([ /* 0 */ (function (module, exports, __webpack_require__) { console.log('Hello Dickens'); // promise resolve后,会指定加载哪个模块 __webpack_require__.e /* import() */(0) .then(__webpack_require__.bind(null, 1)) .then(logger => { logger.default(); }); }) ]);
这里用户记录异步模块加载状态的对象installedChunks
的数据结构如下:
当chunk加载完成后,对应的值是0。在加载过程中,对应的值是一个数组,数组内保存了promise的相关信息。
挂在到window下面的webpackJsonp函数是动态加载模块代码下载后的回调,它会通知webpack模块下载完成并将模块加入到modules
当中。
__webpack_require__.e
函数是动态加载的核心实现,它通过动态创建一个script标签来实现代码的异步加载。加载开始前会创建一个promise存到installedChunks对象当中,加载成功则调用resolve,失败则调用reject。resolve后不会传入模块本身,而是通过__webpack_require__
来加载模块内容,require的模块id由webpack来生成:
__webpack_require__.e /* import() */(0) .then(__webpack_require__.bind(null, 1)) .then(logger => { logger.default(); });
这里之所以要加上default是因为遇到按需加载时,如果使用的是ES Module,webpack会将export default
编译成__webpack_exports__
对象的default
属性(感谢@MrCanJu的指正)。详细请看动态加载的chunk的代码,0.bundle_asy的内容如下:
webpackJsonp([0], [ /* 0 */ , /* 1 */ (function (module, __webpack_exports__, __webpack_require__) { "use strict"; Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); /* harmony export (immutable) */ __webpack_exports__["default"] = log; function log() { console.log('Log from logger'); } }) ]);
代码非常好理解,加载成功后立即调用上文提到的webpackJsonp
方法,将chunkId和模块内容传入。这里要分清2个概念,一个是chunkId,一个moduleId。这个chunk的chunkId是0,里面只包含一个module,moduleId是1。一个chunk里面可以包含多个module。
执行流程如下图所示:
四、总结
本文通过分析webpack生成的启动代码,讲解了webpack是如何实现模块管理和动态加载的,希望对你有所帮助。
如果你对webpack的配置调优感兴趣,可以看看我之前写的这篇文章:webpack调优总结