1、准备代码
//foo.js
'use strict';
exports.foo = function foo() {
return 'foo';
}
//index.js
'use strict';
import(/* webpackChunkName: "foo" */ './foo').then(foo => {
console.log(foo);
});
webpack配置文件
var path = require("path");
module.exports = {
mode: 'development',
entry: path.join(__dirname, './src/index.js'),
output: {
path: path.join(__dirname, 'dist'),
filename: 'index.js',
chunkFilename: '[name].bundle.js', //动态加载模块名称
publicPath: path.join(__dirname, 'dist/'), //动态加载模块路径
},
};
打包后的到的foo.bundle.js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
["foo"],
{
"./src/foo.js":
(function(module, exports, __webpack_require__) {
"use strict";
eval("\nexports.foo = function foo() {\n return 'foo';\n}\n\n\n//# sourceURL=webpack:///./src/foo.js?");
})
}
]);
打包后的index.js
(function(modules) {
...
})({
"./src/index.js":
(function(module, exports, __webpack_require__) {
"use strict";
eval("\n\n__webpack_require__.e(/*! import() | foo */ \"foo\").then(__webpack_require__.t.bind(null, /*! ./foo */ \"./src/foo.js\", 7)).then(foo => {\n console.log(foo);\n});\n\n//# sourceURL=webpack:///./src/index.js?");
})
});
2、分析
index.js
import(/* webpackChunkName: "foo" */ './foo').then(foo => {
console.log(foo);
});
打包后index.js
`__webpack_require__.e(/*! import() | foo */ "foo").then(__webpack_require__.t.bind(null, /*! ./foo */ "./src/foo.js", 7)).then(foo => {
console.log(foo);
})
__webpack_require__.e实现异步加载模块。实际使用script标签加载foo.bundle.js文件,返回promise。
__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);
};
foo.bundle.js文件加载完后,执行内部代码
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
['foo'],
{"./src/foo.js": (function(module, exports, __webpack_require__) {...})}
]);
window["webpackJsonp"]执行的push方法。在打包后的index.js中,push被改写成webpackJsonpCallback方法
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
installedChunks是缓存chunk,注册模块到modules变量中。 在webpackJsonpCallback中调用resolve,更新异步加载的状态
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()();//更改异步加载的状态
}
};
3、疑惑分析
1、我们知道 import('./foo')会返一个 Promise 实例 promise,在 webpack 打包出来的最终文件中是如何处理这个 promise 的?
在加载 foo.bundle.js 之前会在全局 installedChunks 中先存入了一个 promise 对象。__webpack_require__.e执行中
installedChunks[chunkId] = [resolve, reject, promise]
resolve 这个值在 webpackJsonpCallback 中会被用到,这时就会进入到我们写的 import('./foo').then() 的 then 语句中了。
2、在 index.js 中处理 webpackJsonp 过程中还有一段特殊的逻辑
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;
也就是说如果之前已经存在全局的 window["webpackJsonp"] 那么在替换其 push 函数之前会将原有的 push 方法保存为 oldJsonpFunction,同时将已存在于 window["webpackJsonp"] 中的内容,一一执行 webpackJsonpCallback。并且在 webpackJsonpCallback 中也将异步加载的内容也会在 parentJsonpFunction 中同样执行一次
if(parentJsonpFunction) parentJsonpFunction(data);
总结
1、__webpack_require__.e
- 根据 installedChunks 检查是否加载过该 chunk
- 假如没加载过,则发起一个 JSONP 请求去加载 chunk
- 设置一些请求的错误处理,然后返回一个 Promise。
3、window["webpackJsonp"]
用自定义的 webpackJsonpCallback 函数替换了 window["webpackJsonp"] 的 push 方法。所以说,我们之前 foo.js 执行的 push 其实就是执行了自定义的 webpackJsonpCallback 函数
2、webpackJsonpCallback
可以看到,webpackJsonpCallback 做了2件事情。
- 执行 installedChunks 中的 resolve , 让 import() 得以继续执行。
- 将 chunk 中含有的 模块全部注册到 modules 变量中。
