webpack的模块化处理(二)

859 阅读8分钟

webpack的模块化处理(二)

这一讲,我们开始讲解webpack异步加载的原理;在正式讲解之前,我们先熟悉一下jsonp。

JSONP:

在CORS没有流行之前,我们以往想解决跨域问题,常常会使用jsonp的方式进行跨域。它的主要实现步骤如下:

  1. 在客户端先预留回调函数(在函数内,接收通信数据参数,并处理参数);
  2. 在需要通信的时候才去动态加载js;
  3. 在加载的script中,调用预留的回调函数(并以通信数据为参数)。

我们按照jsonp的思想,写一个简易的jsonp,这里我们想要实现向 'www.baidu.xxx.com/xxx' 获取模块信息,并对模块信息进行处理:

let installedChunkData;
// 回调函数
function jsonpCallback(data) {
    console.log('注册模块数据', data); // 可以处理data;
    installedChunkData[0](data);    //这里实际调用的就是resolve
}

// 异步加载jsonp
function load({url}) {
    var promise = new Promise((resolve, reject) => {
       installedChunkData = [resolve, reject];
    });
    installedChunkData[2] = promise;
    let script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = `${url}?callback=jsonpCallback`;
    document.head.appendChild(script);
    var onScriptComplete = function(res) {
        document.head.removeChild(script);
    };
    script.onerror = script.onload = null;
    script.onload = onScriptComplete;
    script.onerror = onScriptComplete;
    return promise;
}

load({url: 'www.baidu.xxx.com/xxx'}).then((val) => {
    console.log('成功注册模块数据,开始加载模块数据', val);
});

服务端返回的www.baidu.xxx.com/xxx 对应的js静态静态资源内容:

    jsonpCallback('模块数据A') //对应jsonpCallback(data)

这里之所以写死回调函数(jsonpCallback)是为了模拟了webpack内部动态加载时的方式;在这种jsonp实现方式中,跟以往的jsonp传统实现方式略有不同:此时我们在调用load加载的时候,返回了一个promise;在动态加载成功时,会执行回调函数(jsonpCallback),执行resolve(表示数据已处理);调用then回调函数,执行 console.log('成功注册模块数据,开始加载模块数据', val);。其实这已经有了webpack内部处理异步加载的雏形。

下边我们来编写我们的代码:

在async_index.js中:

import('./async').then(val => {
    console.log('async', val.async);
});

在 async.js 中:

export const async = 'async';

经过webpack编译我们会发现我们 dist/js 的文件夹下中生成了两个文件,如果你打包后的异步文件名与我的文件名略有不同,并不是配置有误,而是因为我们在 output.chunkFilename 中配置了contenthash:

image.png

我们先看一下打包后的入口文件:

async_index.initial.js:

/*
 * ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
 * This devtool is neither made for production nor for readable output files.
 * It uses "eval()" calls to create a separate source file in the browser devtools.
 * If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
 * or disable the default devtool with "devtool: false".
 * If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
 */
/******/ (() => { // webpackBootstrap
/******/ 	var __webpack_modules__ = ({

/***/ "./src/async_index.js":
/*!****************************!*\
  !*** ./src/async_index.js ***!
  \****************************/
/***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {

eval("__webpack_require__.e(/*! import() */ \"src_async_js\").then(__webpack_require__.bind(__webpack_require__, /*! ./async */ \"./src/async.js\")).then(val => {\n    console.log('async', val.async);\n});\n\n\n//# sourceURL=webpack://webpack_module_analysis/./src/async_index.js?");

/***/ })

/******/ 	});
/************************************************************************/
/******/ 	// The module cache
/******/ 	var __webpack_module_cache__ = {};
/******/ 	
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/ 		// Check if module is in cache
/******/ 		var cachedModule = __webpack_module_cache__[moduleId];
/******/ 		if (cachedModule !== undefined) {
/******/ 			return cachedModule.exports;
/******/ 		}
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = __webpack_module_cache__[moduleId] = {
/******/ 			// no module.id needed
/******/ 			// no module.loaded needed
/******/ 			exports: {}
/******/ 		};
/******/ 	
/******/ 		// Execute the module function
/******/ 		__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/ 	
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/ 	
/******/ 	// expose the modules object (__webpack_modules__)
/******/ 	__webpack_require__.m = __webpack_modules__;
/******/ 	
/************************************************************************/
/******/ 	/* webpack/runtime/define property getters */
/******/ 	(() => {
/******/ 		// define getter functions for harmony exports
/******/ 		__webpack_require__.d = (exports, definition) => {
/******/ 			for(var key in definition) {
/******/ 				if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ 					Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ 				}
/******/ 			}
/******/ 		};
/******/ 	})();
/******/ 	
/******/ 	/* webpack/runtime/ensure chunk */
/******/ 	(() => {
/******/ 		__webpack_require__.f = {};
/******/ 		// This file contains only the entry chunk.
/******/ 		// The chunk loading function for additional chunks
/******/ 		__webpack_require__.e = (chunkId) => {
/******/ 			return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
/******/ 				__webpack_require__.f[key](chunkId, promises);
/******/ 				return promises;
/******/ 			}, []));
/******/ 		};
/******/ 	})();
/******/ 	
/******/ 	/* webpack/runtime/get javascript chunk filename */
/******/ 	(() => {
/******/ 		// This function allow to reference async chunks
/******/ 		__webpack_require__.u = (chunkId) => {
/******/ 			// return url for filenames based on template
/******/ 			return "js/async/" + chunkId + ".chunk." + chunkId + "." + "b02c89dcbbd94705698e" + ".js";
/******/ 		};
/******/ 	})();
/******/ 	
/******/ 	/* webpack/runtime/hasOwnProperty shorthand */
/******/ 	(() => {
/******/ 		__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ 	})();
/******/ 	
/******/ 	/* webpack/runtime/load script */
/******/ 	(() => {
/******/ 		var inProgress = {};
/******/ 		var dataWebpackPrefix = "webpack_module_analysis:";
/******/ 		// loadScript function to load a script via script tag
/******/ 		__webpack_require__.l = (url, done, key, chunkId) => {
/******/ 			if(inProgress[url]) { inProgress[url].push(done); return; }
/******/ 			var script, needAttach;
/******/ 			if(key !== undefined) {
/******/ 				var scripts = document.getElementsByTagName("script");
/******/ 				for(var i = 0; i < scripts.length; i++) {
/******/ 					var s = scripts[i];
/******/ 					if(s.getAttribute("src") == url || s.getAttribute("data-webpack") == dataWebpackPrefix + key) { script = s; break; }
/******/ 				}
/******/ 			}
/******/ 			if(!script) {
/******/ 				needAttach = true;
/******/ 				script = document.createElement('script');
/******/ 		
/******/ 				script.charset = 'utf-8';
/******/ 				script.timeout = 120;
/******/ 				if (__webpack_require__.nc) {
/******/ 					script.setAttribute("nonce", __webpack_require__.nc);
/******/ 				}
/******/ 				script.setAttribute("data-webpack", dataWebpackPrefix + key);
/******/ 				script.src = url;
/******/ 			}
/******/ 			inProgress[url] = [done];
/******/ 			var onScriptComplete = (prev, event) => {
/******/ 				// avoid mem leaks in IE.
/******/ 				script.onerror = script.onload = null;
/******/ 				clearTimeout(timeout);
/******/ 				var doneFns = inProgress[url];
/******/ 				delete inProgress[url];
/******/ 				script.parentNode && script.parentNode.removeChild(script);
/******/ 				doneFns && doneFns.forEach((fn) => (fn(event)));
/******/ 				if(prev) return prev(event);
/******/ 			}
/******/ 			;
/******/ 			var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);
/******/ 			script.onerror = onScriptComplete.bind(null, script.onerror);
/******/ 			script.onload = onScriptComplete.bind(null, script.onload);
/******/ 			needAttach && document.head.appendChild(script);
/******/ 		};
/******/ 	})();
/******/ 	
/******/ 	/* webpack/runtime/make namespace object */
/******/ 	(() => {
/******/ 		// define __esModule on exports
/******/ 		__webpack_require__.r = (exports) => {
/******/ 			if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ 				Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ 			}
/******/ 			Object.defineProperty(exports, '__esModule', { value: true });
/******/ 		};
/******/ 	})();
/******/ 	
/******/ 	/* webpack/runtime/publicPath */
/******/ 	(() => {
/******/ 		__webpack_require__.p = "";
/******/ 	})();
/******/ 	
/******/ 	/* webpack/runtime/jsonp chunk loading */
/******/ 	(() => {
/******/ 		// no baseURI
/******/ 		
/******/ 		// object to store loaded and loading chunks
/******/ 		// undefined = chunk not loaded, null = chunk preloaded/prefetched
/******/ 		// [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
/******/ 		var installedChunks = {
/******/ 			"async_index": 0
/******/ 		};
/******/ 		
/******/ 		__webpack_require__.f.j = (chunkId, promises) => {
/******/ 				// JSONP chunk loading for javascript
/******/ 				var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;
/******/ 				if(installedChunkData !== 0) { // 0 means "already installed".
/******/ 		
/******/ 					// a Promise means "currently loading".
/******/ 					if(installedChunkData) {
/******/ 						promises.push(installedChunkData[2]);
/******/ 					} else {
/******/ 						if(true) { // all chunks have JS
/******/ 							// setup Promise in chunk cache
/******/ 							var promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject]));
/******/ 							promises.push(installedChunkData[2] = promise);
/******/ 		
/******/ 							// start chunk loading
/******/ 							var url = __webpack_require__.p + __webpack_require__.u(chunkId);
/******/ 							// create error before stack unwound to get useful stacktrace later
/******/ 							var error = new Error();
/******/ 							var loadingEnded = (event) => {
/******/ 								if(__webpack_require__.o(installedChunks, chunkId)) {
/******/ 									installedChunkData = installedChunks[chunkId];
/******/ 									if(installedChunkData !== 0) installedChunks[chunkId] = undefined;
/******/ 									if(installedChunkData) {
/******/ 										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;
/******/ 										installedChunkData[1](error);
/******/ 									}
/******/ 								}
/******/ 							};
/******/ 							__webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId);
/******/ 						} else installedChunks[chunkId] = 0;
/******/ 					}
/******/ 				}
/******/ 		};
/******/ 		
/******/ 		// no prefetching
/******/ 		
/******/ 		// no preloaded
/******/ 		
/******/ 		// no HMR
/******/ 		
/******/ 		// no HMR manifest
/******/ 		
/******/ 		// no on chunks loaded
/******/ 		
/******/ 		// install a JSONP callback for chunk loading
/******/ 		var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
/******/ 			var [chunkIds, moreModules, runtime] = data;
/******/ 			// add "moreModules" to the modules object,
/******/ 			// then flag all "chunkIds" as loaded and fire callback
/******/ 			var moduleId, chunkId, i = 0;
/******/ 			if(chunkIds.some((id) => (installedChunks[id] !== 0))) {
/******/ 				for(moduleId in moreModules) {
/******/ 					if(__webpack_require__.o(moreModules, moduleId)) {
/******/ 						__webpack_require__.m[moduleId] = moreModules[moduleId];
/******/ 					}
/******/ 				}
/******/ 				if(runtime) var result = runtime(__webpack_require__);
/******/ 			}
/******/ 			if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
/******/ 			for(;i < chunkIds.length; i++) {
/******/ 				chunkId = chunkIds[i];
/******/ 				if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
/******/ 					installedChunks[chunkId][0]();
/******/ 				}
/******/ 				installedChunks[chunkId] = 0;
/******/ 			}
/******/ 		
/******/ 		}
/******/ 		
/******/ 		var chunkLoadingGlobal = self["webpackChunkwebpack_module_analysis"] = self["webpackChunkwebpack_module_analysis"] || [];
/******/ 		chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
/******/ 		chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
/******/ 	})();
/******/ 	
/************************************************************************/
/******/ 	
/******/ 	// startup
/******/ 	// Load entry module and return exports
/******/ 	// This entry module can't be inlined because the eval devtool is used.
/******/ 	var __webpack_exports__ = __webpack_require__("./src/async_index.js");
/******/ 	
/******/ })()
;

我们可以观察到,对比上一讲的同步加载的方式,在入口文件中,__webpack_require__多挂载了如下几个属性和方法:

__webpack_require__.m:m 是 moduleFactories 的缩写;对__webpack_modules__的引用;

__webpack_require__.e:e 是 ensureChunk 的缩写; 加载异步 chunk 的方法;

__webpack_require__.u: u 是 getChunkScriptFilename 的缩写;获取加载chunk资源脚本部分的文件名(由ouput.chunkFilename配置确定)

__webpack_require__.l: l 是 loadScript 的缩写; 用来加载script标签;

__webpack_require__.p: p 是 publicPath 的缩写;获取公共路径(由output.publicPath配置确定);

__webpack_require__.f.j: f 是 ensureChunkHandlers 的缩写;加载chunk的JSONP方法。

我们先看一下异步加载前和编译后的代码对比:

源文件:

import('./async').then(val => {
    console.log('async', val.async);
});

编译后(为了让大家看的更清楚明了,进行了手动转义和清除了注释和换行):

__webpack_require__.e("src_async_js")
    .then(__webpack_require__.bind(__webpack_require__, "./src/async.js"))
    .then(val => {console.log('async', val.async);});

经过webpack的编译,有三个地方编译的部分值得我们去注意:
1. import()函数转换成了__webpack_require__.e().then()的结构;
2. 引用的相对路径'./async'也替换成了'src_async_js'(替换为异步chunk的chunkId);
3. __webpack_require__.e调用后,在.then回调函数内进行了webpack模块调用的操作(参照上讲__webpack_require__)。

我们从调用__wepback_reruire__.e开始分析:

__webpack_require__.e:

image.png

先分析__webpack_require__.f.j所在的立即执行函数:

在定义__webpack_require__.f.j的立即执行函数内,缓存了installedChunks和webpackJsonpCallback; image.png

还记得上一章节讲的chunk吗?在当前的模式下,会产生两个chunk,一个是入口文件构成的chunkId为"async_index"的chunk,和异步加载的chunkId为"src_async_js"的chunk(下面会提到)。

其中installedChunks存储了已安装(已安装并不代表已加载)chunk的加载状态,其中key为chunkId,value保存了当前chunk的当前状态;当前installedChunks挂载了入口文件("src_index"),0表示入口文件已加载,当然 value 除0之外还可能为其他值,这里罗列接下来会用到的几种值和它代表的加载状态含义:

  1. value为undefined: 代表该模块未加载;
  2. value为数组: 代表加载中;
  3. value为0: 代表已加载。

webpack的编译过程如下:

image.png webpackJsonpCallback为webpack的JSONP回调函数;这其实跟文章开头写的JSONP例子很相似。我们接着往下分析:

继续分析__webpack_require__.f.j:

image.png

可以看到,webpack是利用 __webpack_require__p 和 __webpack_require__.u 拼接来确定加载的url;

__webpack_require__.p(根据output.publicPath配置):

/******/ 	/* webpack/runtime/publicPath */
/******/ 	(() => {
/******/ 		__webpack_require__.p = "";
/******/ 	})();

__webpack_require__.u(根据output.chunkFilename配置):

/******/ 	/* webpack/runtime/get javascript chunk filename */
/******/ 	(() => {
/******/ 		// This function allow to reference async chunks
/******/ 		__webpack_require__.u = (chunkId) => {
/******/ 			// return url for filenames based on template
/******/ 			return "js/async/" + chunkId + ".chunk." + chunkId + "." + "b02c89dcbbd94705698e" + ".js";
/******/ 		};
/******/ 	})();

image.png

其中name和id都配置为chunkId 是因为 webpack配置时,默认[name] 会被预先替换为 [id];

  • 另外值得一提的是,我们在工作中有时可能会单独将项目打包给其他人使用,或是利用qiankuan来构建微前端架构系统,可能会出现加载资源的路径不正确的问题:
    • 大概率是因为publicPath配置时设置了相对路径的原因;导致加载资源时的路径错误;
    • 我们可以利用设置__webpack_public_path__来重置publicPath;其原理就是webpack编译时;会将__webpack_public_path__编译成__webpack__require__.p。

我们最终调用的是__webpack_require__.l:

__webpack_require__.l:

image.png 此时我们的异步加载的文件src_async_js.chunk.src_async_js.b02c89dcbbd94705698e.js就加载进来了,我们看一下它的编译结果:

"use strict";
/*
 * ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
 * This devtool is neither made for production nor for readable output files.
 * It uses "eval()" calls to create a separate source file in the browser devtools.
 * If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
 * or disable the default devtool with "devtool: false".
 * If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
 */
(self["webpackChunkwebpack_module_analysis"] = self["webpackChunkwebpack_module_analysis"] || []).push([["src_async_js"],{

/***/ "./src/async.js":
/*!**********************!*\
  !*** ./src/async.js ***!
  \**********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   \"async\": () => (/* binding */ async)\n/* harmony export */ });\nconst async = 'async';\n\n\n//# sourceURL=webpack://webpack_module_analysis/./src/async.js?");

/***/ })

}]);

image.png

在async_index.initial.js中: image.png

可见再当前的场景,调用self["webpackChunkwebpack_module_analysis"].push,最终调用的就是webpackJsonpCallback;我们最后看一下执行回调函数又会做哪些事情:

image.png 我们可以看到,我们现在只是注册了模块,并没有立即执行模块;那是什么时候执行模块的呢?还记得我们webpack编译后多出来的一个.then回调函数吗? .then(__webpack_require__.bind(__webpack_require__, "./src/async.js")) 就是这段代码完成了对同步加载的"./src/async.js"模块的调用。这样做有将加载bundle,挂载chunk的逻辑和调用module的逻辑解耦的好处。至此关于webpack对异步加载的处理也到此结束了;

总结:

webpack就是利用类似JSONP的加载方式,根据chunkId确认目标文件url,动态加载js(目标chunk对应的bundle文件),在执行js文件的过程中经历加载chunk,加载module,最后执行目标模块(函数);完成了异步加载。