# Webpack 的 runtime 运行时浅析

通过一个例子来分析下Webpack在运行时的文件加载与执行过程

// index.js entry
console.log('entry')
import(/* webpackChunkName: "sum" */'./sum.js').then(({sum})=>{
    console.log(sum(1,2)) 
})

// sum.js chunk
export function sum(a,b){
    return a + b
}

这个例子中入口文件按需依赖了一个chunk文件,剥离runtime进行打包后得到3个文件

  • 入口文件 main.js
  • 运行时 runtime.js
  • chunk 依赖 sum.chunk.js

# 运行时分析

先看下runtime.js的核心内容

var __webpack_modules__ = ({});
var __webpack_module_cache__ = {};

(()=>{
    // installedChunks 
    // 0 表示当前的chunk已经加载完成
    // 未加载完成的话 存储的是 chunk 加载的promise [resolve,reject,promise]
    var installedChunks = {
        "runtime": 0
    }

    // parentChunkLoadingFunction 在当前场景下 就是原始的 Array.prototype.push 方法
    // data : chunk data
    var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
    	var [chunkIds, moreModules, runtime] = data;
    	var moduleId, chunkId, i = 0;
    	if(chunkIds.some((id) => (installedChunks[id] !== 0))) {
    		for(moduleId in moreModules) {
                // __webpack_require__.o 判断属性是否是对象自身属性
    			if(__webpack_require__.o(moreModules, moduleId)) {
                    // __webpack_require__.m === __webpack_modules__
    				__webpack_require__.m[moduleId] = moreModules[moduleId];
    			}
    		}
            // 执行chunk的运行时
    		if(runtime) var result = runtime(__webpack_require__);
    	}
        // 执行原始的push方法
    	if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
    	for(;i < chunkIds.length; i++) {
    		chunkId = chunkIds[i];
    		if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
    			installedChunks[chunkId][0](); // 执行 installedChunks 中对应的 resolve 方法,完成chunk的加载
    		}
    		installedChunks[chunkId] = 0; // 标记chunk成已加载完成
    	}
        // 当前场景下直接返回 result
    	return __webpack_require__.O(result);
    }

    // 初始化一个全局对象下面 webpackJSONP 属性
    var chunkLoadingGlobal = self["webpackJSONP"] = self["webpackJSONP"] || [];
    // 对所有chunk执行 webpackJsonpCallback 方法
    chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
    // 重写 chunkLoadingGlobal.push 方法
    chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));

})()

上面的代码除了函数的预定义外,核心就做了2件事情

  1. 定义了全局的变量 webpackJSONP
  2. 修改了全局变量的 push方法

接着看下入口chunkmain.js的代码

(self['webpackJSONP'] = self['webpackJSONP'] || []).push([['main'],{
    './src/index.js':(__unused_webpack_module,__unused_webpack_exports,__webpack_require__)=>{
        console.log('entry')
        __webpack_require__.e('sum')
            .then(__webpack_require__.bind(__webpack_require__,'./src/sum.js'))
            .then(({sum})=>{ console.log(sum(1, 2)); })
    }
},(__webpack_require__)=>{
    var __webpack_exec__ = (moduleId) => __webpack_require__((__webpack_require__.s = moduleId));
    var __webpack_exports__ = __webpack_exec__('./src/index.js');
}])

入口chunk都是通过script脚本加载到我们的页面中的,而script脚本在加载完成后就会执行。这样就会执行到push方法从而执行到runtime中的webpackJsonpCallback方法。而webpackJsonpCallback会执行chunk的runtime方法,就是执行上面代码定义的第3个参数

(__webpack_require__)=>{
    var __webpack_exec__ = (moduleId) => __webpack_require__((__webpack_require__.s = moduleId));
    var __webpack_exports__ = __webpack_exec__('./src/index.js');
}

可以看到核心是执行了 __webpack_require__('./src/index.js') 这样的一行代码,看下__webpack_require__的实现

// __webpack_require__ 
function __webpack_require__(moduleId) {
    // 首先找缓存
    // 返回缓存的 exports 属性
    var cachedModule = __webpack_module_cache__[moduleId];
    if (cachedModule !== undefined) {
	    return cachedModule.exports;
    }
    // 创建一个模块对象 并且加入到缓存对象中
    var module = __webpack_module_cache__[moduleId] = {
        exports: {}
    }
    // 从 __webpack_modules__ 找到对应的模块函数 并且执行
    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
    return module.exports;
}

可以看到其实__webpack_require__的核心就是从__webpack_modules__找到对应的模块函数并且执行。模块函数的逻辑在上面提到的webpackJsonpCallback方法中添加,其实就是chunk函数中对应的第二个参数

{
    './src/index.js':(__unused_webpack_module,__unused_webpack_exports,__webpack_require__)=>{
        console.log('entry')
        __webpack_require__.e('sum')
            .then(__webpack_require__.bind(__webpack_require__,'./src/sum.js'))
            .then(({sum})=>{ console.log(sum(1, 2)); })
    }
}

可以看到,./src/index.js对应的其实就是一个函数。这个函数首先执行了console.log('entry')这段定义在index.js中的代码,接着调用__webpack_require__.e来实现按需加载的一个调用,来看下这个函数的实现

__webpack_require__.f = {};
__webpack_require__.e = (chunkId) => {
	return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
		__webpack_require__.f[key](chunkId, promises);
		return promises;
	}, []));
};

可见此方法就是执行__webpack_require__.f对象中所有的方法,并将chunkId作为第一个参数传入,此场景下__webpack_require__.f只有一个方法属性

__webpack_require__.f.j = (chunkId, promises) => {

    var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;
    // 0 表示已经加载完成
    if(installedChunkData !== 0) {
        // 如果chunk正在加载 直接返回对应的 promise
        if(installedChunkData) {
            promises.push(installedChunkData[2]);
        }else{
            // chunk 没有加载 且不是 runtime chunk
            if("runtime" != chunkId) {
                // 初始化 installedChunks[chunkId] = [resolve,reject,promise] 
                var promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject]));
                promises.push(installedChunkData[2] = promise);
                // 获取chunk的地址
                // 核心就是 当前逻辑就是 location + chunkId + '.chunk.js'
                var url = __webpack_require__.p + __webpack_require__.u(chunkId);

                // 定义 chunk 资源加载完成的回调函数
                // 处理 chunk 加载完成后,chunk的jsonp没有按预期执行的 错误处理
                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 的核心逻辑是 通过插入 script 标签来获取 script 脚本
                __webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId);
            }else{
                // runtime 
                installedChunks[chunkId] = 0;
            }
        }
    }
}

所以回到__webpack_require__.e方法,这个返回通过Promise.all返回了一个promise,而promise.all的参数是一个动态插入script标签,并且在标签加载并执行完成后,再执行对应的resolve方法的。所以可以得到main.js中的__webpack_require__.e会在sum.chunk.js加载完成后在执行对应的then方法。 接着看下最后的一个文件

// sum.chunk.js
(self['webpackJSONP'] = self['webpackJSONP'] || []).push([['sum'],{
     "./src/sum.js":(__unused_webpack_module, __webpack_exports__, __webpack_require__)=>{
        __webpack_require__.r(__webpack_exports__);
        __webpack_require__.d(__webpack_exports__, {
           "sum": () => (/* binding */ sum)
        });
        function sum(a,b){
            return a+b
        }
     }
})

sum.chunk.js在加载完成后同样会执行到webpackJsonpCallback方法,从而执行到以下方法

(__unused_webpack_module, __webpack_exports__, __webpack_require__)=>{
     __webpack_require__.r(__webpack_exports__);
     __webpack_require__.d(__webpack_exports__, {
        "sum": () => (/* binding */ sum)
     });
     function sum(a,b){
        return a+b
    }
 }

看下关键的r d2个方法的定义

__webpack_require__.r = (exports) => {
    // 修改 Symbol.toStringTag 实现 toString 方法的修改
	if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
		Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
	}
    // 定义 __esModule 属性
	Object.defineProperty(exports, '__esModule', { value: true });
};

__webpack_require__.d = (exports, definition) => {
	for(var key in definition) {
		if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
            // 将 definition 的属性 绑定到 exports 对象上
			Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
		}
	}
}

代码很简单,其实核心就是给 __webpack_exports__ 对象绑定上d函数的第二个参数中的属性。回到main.js中来,

 __webpack_require__
        .e(/*! import() | sum */ 'sum')
        .then(
          __webpack_require__.bind(
            __webpack_require__,
             './src/sum.js',
          ),
        )
        .then(({ sum }) => {
          console.log(sum(1, 2));
        });

e方法执行完成,就会执行到__webpack_require__方法,这个方法会返回之前定义的module.exports属性,最后就完成了整个按需加载的流程

# 总结

  • webpack的运行时在项目最开始被加载,除了初始化一些函数和变量外。全局定义了一个jsonp的变量数组,并且劫持了这个数据的push方法。在这个push方法内完成了chunk运行时的执行和chunk获取状态promise的确定
  • chunk文件被包裹成成了一个全局jsonp数组push方法的函数,当加载完成了会触发执行,从而将该chunk添加到已安装的chunk数组内
  • webpack实现了自己的require方法,当出现动态加载时,先实例一个chunk加载状态的promise,并通过script脚本去加载chunk,加载完成后,触发chunk的执行回调,将chunk内容添加到已安装的chunk数组内,并修改promise的状态。然后再通过__webpack_require__方法,动态的从已安装的模块中拿到对应的chunk内容