Webpack之模块加载

Webpack之模块加载

webpack作为前端打包工具受到大多数的前端开发者的青睐,在使用webpack的过程我们通过webpack自带的模块化功能实现了项目代码的模块化,方便了我们管理和维护,那么webpack是怎么实现各个模块之间的划分和加载的呢?

模块划分

在了解模块加载之前,我们需要首先看下webpack是怎么将一个个文件划分为模块的。我们有一个入口文件index.js以及模块a.js、b.js,代码如下

// index.js
import A from './a.js'
function hello () {
    console.log(A)
}
hello()
export default hello

// a.js
export default {
    a: 'a'
}
// b.js
export const B = function () {
    console.log('B')
}

在经过了webpack的各种编译之后,会产生一个bundle.js的文件,我们来看下里面的大体结构。

(function(modules){
  ......
  ......
  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;
  return __webpack_require__(__webpack_require__.s = "./src/main.js");
})(
 {
   "./src/a.js": (function(module, __webpack_exports__, __webpack_require__){
       .......
       .......
   }),
   "./src/main.js": (function(module, __webpack_exports__, __webpack_require__){
       .......
       .......
   })
 }
)

我们可以看到webpack最终打包出来的文件其实一个立即执行函数,函数的参数是一个modules对象。webpack将一个文件作为一个模块,用文件的相对路径来作为module的key,把文件的内容用一个闭包包裹作为module的值。将modules传入上面的函数之后,执行到return webpack_require(webpack_require.s = “./src/main.js”); 部分,加载我们的入口文件main.js。所以我们可以看出webpack进行模块加载的关键就是__webpack_require__函数了。

模块加载

webpack_require

前面有提到__webpack_require__函数是webpack中模块加载的关键函数,下面看下它的作用是什么。

// 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 = {
	0: 0
};
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] = {
		    // id
			i: moduleId,
			// 是否记载的标志变量
			l: false,
			// module的值
			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;
}

__webpack_require__是一个通过moduleId来加在对应模块的函数,首先会去全局的installedModules查找该模块是否已经加载过了,若之前加载过了,则使用之前的缓存,若未加载,则会定义一个包含i、l、exports的module变量,并将该变量缓存到installedModules中去,接下来就执行我们上面提到的modules里对应key的函数,module, module.exports, __webpack_require__作为该函数的参数,我们拿main.js来举例接着来看modules里面的函数做了什么

   "./src/main.js": (function(module, __webpack_exports__, __webpack_require__){
       
        "use strict";
        __webpack_require__.r(__webpack_exports__);
        /* harmony import */ var _a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a.js */ "./src/a.js");
      })
      function hello() {
            console.log(_a_js__WEBPACK_IMPORTED_MODULE_0__["default"]);
       }
      __webpack_exports__["default"] = (hello)
}

在webpack编译出的main.js文件中,我们用来引入模块的import已经被转换成了__webpack_require__。在该函数里面主要做的事情就是给之前我们定义的module.exports赋值,若导出模块使用的是export default的话,就会给module.exports定义一个default属性,若是使用export的话,则会给module.exports定义一个导出的key的属性。

在执行完这个函数之后,再在上面的__webpack_require__函数中把赋值module.exports的导出,最后就完成了对应模块的定义和导入。

在上面的函数中,webpack有使用到一些自定义的工具函数和属性,如:webpack_require.r等,我们来看一下这些工具函数的的大致用途。

属性名 类型 用途
webpack_require.e Function 用于异步加载,后面的内容会提到
webpack_require.m Array modules
webpack_require.c Array 已经加载过的module的cache
webpack_require.d Function 给module的exports定义属性
webpack_require.r Function 给给module的exports定义__esModule属性
webpack_require.o Function 作用等同于hasOwnProperty

异步加载

webpack的异步加载的过程和普通模块加载的过程有些许不同,webpack 在编译时,会静态地解析代码中的异步加载相关的代码,同时将模块添加到一个分开的 chunk 当中,这时候就不能使用我们同步加载模块的方式了。我们通过编译过后的独立chunk的代码来看下webpack的异步加载是怎么实现的。

// 原代码
() => import('./src/c.js')

// 编译后代码
function () {
    return __webpack_require__.e(/*! import() */ 1).then(__webpack_require__.bind(null, /*! ./src/c.js */ "./src/c.js"));
}

我们使用了import()进行异步加载后,webpack将其编译成了__webpack_require__.e去加载chunkId为0的chunk。然后在通过__webpack_require__加载对应的模块,我们先看下__webpack_require__.e是怎么加载chunk的。

扫描二维码关注公众号,回复: 5716117 查看本文章
__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);

				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;
							var error = new Error('Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')');
							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);
	};

简单的说就是,动态地添加script标签,去加载chunkId对应的js,加载完成之后放入promise中,等待resolve,等到调用了resolve才标志着chunk加载完成,接着我们看下chunk1里面的js主要内容。

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[1],{
  "./src/c.js":  (function(module, __webpack_exports__, __webpack_require__) {
       "use strict";
       __webpack_require__.r(__webpack_exports__);
       function c() {
            console.log("c");
       }
      __webpack_exports__["default"] = (c)
  })
}])

可以认为,在chunk里面其实主要就是执行了(window[“webpackJsonp”] = window[“webpackJsonp”] || []).push代码,这个push和一般的数组的不同,这个是自定义的push函数,我们回到最上面的主chunk代码中。

  var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
  var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
  jsonpArray.push = webpackJsonpCallback;

我们发现有这么一段代码,初始化了window[“webpackJsonp”]这个变量,并将该变量的push属性变成了webpackJsonpCallback。

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

	};

webpackJsonpCallback的主要工作就是把我们push的参数(chunkId,对应的module)注入到全局当中,最后执行resolve,表示模块已经注入成功,这个时候,我们使用__webpack_require__就可以导入分离出来的chunk中的module了。

猜你喜欢

转载自blog.csdn.net/qq_34179086/article/details/88086366