最近在阅读react-loadable源码时,发现import方法返回的结构是一个promise。于是就好奇webpack 是如何实现异步加载文件,并且加载完成后返回了一个promise。遂决定简单写一个demo 一探究竟
同步模块加载
首先准备两个文件
// index.js
const commonjs = require('./commonjs');
console.log('index::::');
// commonjs
module.exports.commonjs = true;
然后运行webpack 打包,生成bundle.js
(function(modules) { // webpackBootstrap
// The module cache
var installedModules = {};
// The require function
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] = {
i: moduleId,
l: false,
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;
}
// expose the modules object (__webpack_modules__)
__webpack_require__.m = modules;
// expose the module cache
__webpack_require__.c = installedModules;
// define getter function for harmony exports
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};
// define __esModule on exports
__webpack_require__.r = function(exports) {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
// create a fake namespace object
// mode & 1: value is a module id, require it
// mode & 2: merge all properties of value into the ns
// mode & 4: return value when already ns object
// mode & 8|1: behave like require
__webpack_require__.t = function(value, mode) {
if(mode & 1) value = __webpack_require__(value);
if(mode & 8) return value;
if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
var ns = Object.create(null);
__webpack_require__.r(ns);
Object.defineProperty(ns, 'default', { enumerable: true, value: value });
if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
return ns;
};
// getDefaultExport function for compatibility with non-harmony modules
__webpack_require__.n = function(module) {
var getter = module && module.__esModule ?
function getDefault() { return module['default']; } :
function getModuleExports() { return module; };
__webpack_require__.d(getter, 'a', getter);
return getter;
};
// Object.prototype.hasOwnProperty.call
__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
// __webpack_public_path__
__webpack_require__.p = "/dist";
// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
/************************************************************************/
({
/***/ "./src/commonjs.js":
/*! no static exports found */
/***/ (function(module, exports) {
eval("module.exports.commonjs = true;\n\n//# sourceURL=webpack:///./src/commonjs.js?");
/***/ }),
/***/ "./src/index.js":
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
eval("const commonjs = __webpack_require__(/*! ./commonjs */ \"./src/commonjs.js\")\n\nconsole.log('index.js')\n\n//# sourceURL=webpack:///./src/index.js?");
/***/ })
});
精简代码后如下
(function(modules){
function __webpack_require__(moduleId) {}
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})({
"./src/commonjs.js":
/***/ (function(module, exports) {
eval("module.exports.commonjs = true;\n\n//# sourceURL=webpack:///./src/commonjs.js?");
/***/ }),
"./src/index.js":
/***/ (function(module, exports, __webpack_require__) {
eval("const commonjs = __webpack_require__(/*! ./commonjs */ \"./src/commonjs.js\")\n\nconsole.log('index.js')\n\n//# sourceURL=webpack:///./src/index.js?");
/***/ }),
})
由上面文件可知,webpack打包后生成一个自运行函数,自运行函数接受一个modules对象作为参数,modules对象以文件路径为key, 文件内容为value,函数接受module,exports, require等参数。
再看这个自运行函数主要做了什么
- 定义一个installedModules对象,缓存各个模块
- 定义一个__webpack_require 方法,用于加载各个模块
- 使用__webpack_require 加载入口文件,
**return __**webpack_require**(__webpack_require__.s = "./src/index.js");**
核心逻辑即定义__webpack_require,然后通过__webpack_require 加载模块代码。
webpack_require加载步骤
- 接口moduleId作为参数, 用installedModules 判断是否加载过,加载过就立即返回
- 定义一个module,并且赋值给installedModules[moduleId]
- 通过modules[moduleId] 运行该模块,并且传入 module, module.exports,__webpack_require等参数。
- 返回module.exports
同步加载主要逻辑就这些,下面来看一下函数内的其它代码
// 通过m属性将自运行函数参数暴露出去
__webpack_require__.m = modules;
//通过c属性将加载过的模块暴露出去
__webpack_require__.c = installedModules;
// define getter function for harmony exports
// 定义一个访问器属性
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};
// 定义一个es6的module,用于加载es6模块
__webpack_require__.r = function(exports) {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
// 异步加载模块时会用到该方法
__webpack_require__.t = function(value, mode) {
if(mode & 1) value = __webpack_require__(value);
if(mode & 8) return value;
if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
var ns = Object.create(null);
__webpack_require__.r(ns);
Object.defineProperty(ns, 'default', { enumerable: true, value: value });
if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
return ns;
};
// 获取模块的默认值,兼容es6 和commonjs
__webpack_require__.n = function(module) {
var getter = module && module.__esModule ?
function getDefault() { return module['default']; } :
function getModuleExports() { return module; };
__webpack_require__.d(getter, 'a', getter);
return getter;
};
// 暴露一个hasOwnProperty方法
__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
// 暴露模块的publicPath, 用于静态资源路径
__webpack_require__.p = "/dist";
上面我们打包了两个commonjs代码,但webpack 还支持打包es6模块的代码,接下来我们就来打包es6模块代码试一下
// index.js
const es6 = require('./es6')
console.log('index.js')
//es6.js
export default es6 = true;
打包后自运行函数无任何变化,但传入的modules发生了变化
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony default export */ __webpack_exports__[\"default\"] = (es6 = true);\n\n");
代码最开始就用__webpack_require__.r 对__webpack_exports__ 进行了一个包装, 通过查看上面__webpack_require__.r的方法得知,r方法在咱们的模块上增加了__esModule属性,后续代码会通过__esModule来进行模块代码。
对比之前的commonjs打包生成的代码
eval("module.exports.commonjs = true;\n\n//# sourceURL=webpack:///./src/commonjs.js?");
得知,webpack在打包es6模块时,会通过__webpack_require__.r给模块代码增加__esModule属性,后续逻辑即通过该属性判断是es6模块 还是commonjs模块
上面我是用commonjs 去引入文件 所以webpack直接调用的__webpack_require__去加载文件,接下来我用es6的方式去试一下
// index.js
import es6 from './es6'
console.log('index.js');
// es6.js
export default es6 = true;
下面是index.js模块打包后的代码
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _es6__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./es6 */ \"./src/es6.js\");\n\nconsole.log(_es6__WEBPACK_IMPORTED_MODULE_0__[\"default\"]);\n\n//# sourceURL=webpack:///./src/index.js?");
使用import 引入 模块 该模块也转换为es6module,所以也使用了__webpack_require__.r进行包装,然后还是通过__webpack_require__导入es6模块,并通过.default访问导出的默认值。
接下来用es6模块加载commonjs试一下呢
// index.js
import commonjs from './commonjs'
console.log(commonjs);
// commonjs
module.exports.commonjs = true
打包后生成的index.js代码
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _commonjs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./commonjs */ \"./src/commonjs.js\");\n/* harmony import */ var _commonjs__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_commonjs__WEBPACK_IMPORTED_MODULE_0__);\n\nconsole.log(_commonjs__WEBPACK_IMPORTED_MODULE_0___default.a);\n\n");
//翻译后
_webpack_require__.r(__webpack_exports__);
var _commonjs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/commonjs.js");
var _commonjs__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(_commonjs__WEBPACK_IMPORTED_MODULE_0__);
console.log(_commonjs__WEBPACK_IMPORTED_MODULE_0___default.a);
可以看到
- 通过__webpack_require__ 加载commonjs
- webpack_require.n包装返回的模块
- 然后通过_commonjs__WEBPACK_IMPORTED_MODULE_0___default.a获取默认值
webpack_require.n 函数可分为两步
- 定义一个获取getter, 如果是es6 就返回module.__esModule, 否则直接返回module
- 将该getter赋值给getter.a属性,并且返回getter
以上是webpack打包同步加载模块代码,webpack针对es6 模块和commonjs 模块会有一些不同的处理
- es6 模块会给module加上__esModule属性
- es6 加载commonjs 时 会通过__webpack_require__.n 包装一下返回的模块。
webpack异步加载模块
// index.js
import('./lazy').then((value) => {
console.log(value)
});
// lazy
module.exports = function() {
console.log('lazyLoad:::');
}
打包查看文件 生成了一个bundle.js 和一个0.bundle.js
// 0.bundle.js
// 调用window["webpackJsonp"].push 处理modules数据
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
/***/ "./src/lazy.js":
/*!*********************!*\
!*** ./src/lazy.js ***!
\*********************/
/*! no static exports found */
/***/ (function(module, exports) {
eval("module.exports = function() {\n console.log('lazyLoad:::');\n}\n\n");
/***/ })
查看bundle.js 发现自运行函数多了三块代码
// 异步js 模块加载完成后会调用window["webpackJsonp"].push 也就是该方法
function webpackJsonpCallback(data) {
// chunkid
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]) {
// 将该异步模块的promise的resolve 存储到数组上去
resolves.push(installedChunks[chunkId][0]);
}
// 赋值为0 表示异步加载模块成功, __webpack_require__.e方法有用到这个值做判断
installedChunks[chunkId] = 0;
}
// 将异步modules 存储到modules对象上去
for(moduleId in moreModules) {
if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
if(parentJsonpFunction) parentJsonpFunction(data);
// 加载成功 执行resolve,走后续流程
while(resolves.length) {
resolves.shift()();
}
};
// 异步加载模块通过该方法加载, 通过jsonp加载
__webpack_require__.e = function requireEnsure(chunkId) {
var promises = [];
var installedChunkData = installedChunks[chunkId];
// 值为0 表示已成功加载过
if(installedChunkData !== 0) {
// 有值表示正在加载,直接pushinstalledChunkData[2]
if(installedChunkData) {
promises.push(installedChunkData[2]);
} else {
// 生成一个promise,并将resolve,reject放进installedChunks
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
// 存储promsie
promises.push(installedChunkData[2] = promise);
// 创建script标签
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();
// 定义onload onerror
onScriptComplete = function (event) {
// avoid mem leaks in IE.
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
// chunk 没被改为0 表示失败 此时chunk 的值为[resolve, reject, promise]
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;
// 执行reject
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);
}
}
// 执行该promise
return Promise.all(promises);
};
// 定义window["webpackJsonp"] 为一个数组
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
// 重写该数组方法 调用window["webpackJsonp"].push方法时就执行上面定义的webpackJsonpCallback方法 进行模块数据处理
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;
webpackJsonpCallback 方法加载代码完成后的执行, 0.bundle.js文件里面会调用到该方法
__webpack_require__.e 通过使用jsonp方式来加载异步模块代码
接下来看index.js是如何调用的
// index.js
eval("__webpack_require__.e(/*! import() */ 0).then(__webpack_require__.t.bind(null, /*! ./lazy */ \"./src/lazy.js\", 7)).then((value) => {\n console.log(value)\n})\n\n");
// 翻译后
__webpack_require__.e(0)
.then(__webpack_require__.t.bind(null, "./src/lazy.js\", 7))
.then((value) => {
console.log(value)
});
可以看到调用__webpack_require__.e(0) 后再执行了__webpack_require__.t 去modules 中加载模块
- webpack_require.e(0) 中的0 就是chunkid 对应window["webpackJsonp"].push中的第一个参数
- webpack_require.t 又会去调用__webpack_require__ 去加载对应的模块, 此时modules经过webpackJsonpCallback的处理 已经包含
"./src/lazy.js\"的代码 所以就能获取到该模块代码 - 然后调用then 就能拿到该模块的结果
小结:
- webpack 兼容处理了commonjs 和 es6模块,通过__esModule来判断是否是es6模块
- 异步加载使用jsonp 的方式加载js文件,里面会定义一个promise
- 异步代码调用了window["webpackJsonp"].push,该方法被webpackJsonpCallback重写
- webpackJsonpCallback 方法会处理加载到的模块代码,然后运行resolve,将控制权交给了__webpack_require__.t ,通过__webpack_require__.t 去运行模块代码