输出文件简单分析
本章节将简单揭秘一下,为什么通过Webpack输出的打包文件能直接在浏览器运行,其输出文件到底有什么魔法?
为了方便阅读输出文件的源码,在此在这里对输出文件进行了一些格式改造,但是并没有修改其功能,代码如下:
(function (modules) { // 启动匿名函数
// 从异步加载的文件中安装模块
function webpackJsonpCallback(data) {
// 异步加载的文件中存放的需要安装模块对应的Chunk ID
var chunkIds = data[0];
// 异步加载的文件中存放的需要安装的模块列表
var moreModules = data[1];
// 将moreModules中的所有模块添加到modules
// 将所有ChunkIds读经的模块标记为加载成功
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()();
}
};
// 缓存已经加载过的模块
var installedModules = {};
/**
* 缓存每个chunk的加载状态
* key为chunk的id
* value表示chunk的状态
* value = undefined:没有加载
* value = null:预取和预加载
* value = Promise: 加载中
* value = 0: 加载完毕
*/
var installedChunks = {
"main": 0
};
// 返回拼接好的文件路径
function jsonpScriptSrc(chunkId) {
return __webpack_require__.p + "" + ({}[chunkId] || chunkId) + ".js"
}
// 导入函数,代替node.js中的require函数
function __webpack_require__(moduleId) {
// 如果模块已经被安装过,直接返回模块的导出值
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 创建一个新的模块
var module = installedModules[moduleId] = {
// 模块id
i: moduleId,
// 是否被安装
l: false,
// 模块导出值
exports: {}
}
// 根据moduleId,从modules中获取对应执行函数
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 模块已经被加载
module.l = true;
// 返回模块的导出值
return module.exports;
}
// 加载被分割出去的需要异步加载的chunk对应文件
__webpack_require__.e = function requireEnsure(chunkId {
var promises = [];
// 异步加载文件
var installedChunkData = installedChunks[chunkId];
// Chunk没有被加载过
if (installedChunkData !== 0) { // 0 means "already installed".
// installedChunkData不为空不为0 表示正处于网络加载中
if (installedChunkData) {
promises.push(installedChunkData[2]);
} else {
// installedChunkData不为0为空 表示chunk没有被加载过,去加载chunk对应的文件
var promise = new Promise(function (resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push(installedChunkData[2] = promise);
// 进行DOM操作,创建一个script标签添加到head中,去加载chunk对应的文件
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);
var error = new Error();
// script加载和执行完成是的回调函数
onScriptComplete = function (event) {
// 防止IE中内存泄漏
script.onerror = script.onload = null;
clearTimeout(timeout);
// 加载chunid对应的chunk是安装成功,安装成功才会存在于installedChunks中
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);
};
// __webpack_require__上绑定所有模块
__webpack_require__.m = modules;
// __webpack_require__上绑定所有加载过的模块
__webpack_require__.c = installedModules;
// define getter function for harmony exports
__webpack_require__.d = function (exports, name, gette {
if (!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, {
enumerable: true, get:
getter
});
}
};
// 给模块的导出值定义_esModule属性,表示该模块ES模块
__webpack_require__.r = function (exports) {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, {
value: 'Module'
});
}
Object.defineProperty(exports, '__esModule', {
value: true
});
};
/**
* 创建一个伪命名空间对象
* mode & 1 value 是一个模块id,require(value)加载模块
* mode & 2 将value的所有属性合并到ns对象当中
* mode & 4 返回已经是nsobject到value
* mode & 8 | 1 绑定getter方法
*/
__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)自身属性是有指定的属性(property)
__webpack_require__.o = function (object, property) { returnObject.prototype.hasOwnProperty.call(object, pro perty); };
// puplicPath output.publicPath中设置的值
__webpack_require__.p = "";
// 异步加载出现错误的处理函数
__webpack_require__.oe = function (err) { console.error(err); throw err; };
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/index.js");
})
({
"./src/index.js":
(function (module, __webpack_exports__, __webpack_require__
) {
"use strict";
eval(`
__webpack_require__.r(__webpack_exports__);
var _name__webpack_IMPORTED_MODULE_0__ = _webpack_require__("./src/name.js");
setTimeout(()=>{
__webpack_require__.e(0)
.then(__webpack_require__.bind(null,"./src/async.js"))
.then(res=>{console.log(res);})
},100)
console.log(_name__webpack_IMPORTED_MODULE_0__["default"]);
`);
}),
"./src/name.js":
(function (module, __webpack_exports__, __webpack_require__) {
"use strict";
eval(`
__webpack_require__.r(__webpack_exports__);
__webpack_exports__["default"] = ('Render');
`);
})
});
// 从整体开始一步一步的分析,该文件究竟做了些什么。很显然的是,输出内容是一个自执行函数,如下所示:
(function (modules) {
// 模拟require函数
function __webpack_require__(moduleId) { }
})(
{
// 存放所有模块的对象
}
)
该函数通过定义了浏览器能够识别的 __webpack_require__函数,来代替Node.js中的require函数。
__webpack_require__的作用就是根据模块id,执行对应的模块可执行函数,然后返回模块的导出值,其函数功能如下:
- 根据模块id先从缓存中去读取对应的模块,找到则直接返回模块的导出值。
- 没有找到模块,则初始化一个新模块,根据模块id执行modules(这里的modules就是自执行函数传入的存放所有模块的对象)中对应的模块的执行函数,将新模块加入到缓存中,然后返回新模块的导出值。
自执行函数会通过如下代码,从入口文件开始执行__webpack_require__函数:
return __webpack_require__(__webpack_require__.s = "./src/index.js");
从这开始,看看输出文件是如何处理模块之间的依赖关系。根据上面分析的__webpack_require__功能,webpack_require(webpack_require.s = “./src/index.js”)代码语句会执行下面这个函数:
function (module, __webpack_exports__, __webpack_require__) {
"use strict";
eval(`
__webpack_require__.r(__webpack_exports__);
var _name__webpack_IMPORTED_MODULE_0__ = __webpack_require__("./src/name.js");
setTimeout(()=>{
__webpack_require__.e(0)
.then(__webpack_require__.bind(null, "./src/async.js"))
.then(res=>{console.log(res);})
},100)
console.log(_name__webpack_IMPORTED_MODULE_0__["default"]);
`);
}
函数的入参module, webpack_exports, __webpack_require__是__webpack_require__函数中传入的,对应代码如下:
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
因此才可以去得到入口文件依赖的name.js的模块返回值,如下所示:
var _name__webpack_IMPORTED_MODULE_0__ = __webpack_require__("./src/name.js");
如果name.js中依赖其他模块,那么就会递归的执行该操作,直到所有被依赖的模块都被处理,最后入口文件中就得到了最后的结果。
setTimeout(() => {
__webpack_require__.e(0)
.then(__webpack_require__.bind(null, "./src/async.js"))
.then(res => { console.log(res); })
}, 100)
是属于输出文件中异步加载模块的处理,webpack_require.e函数功能如下:
- 首先根据chunkId去缓存中去寻找对应的Chunk。
- 如果对应的Chunk正在加载中,就直接使用Promise处理该Chunk。
- 如果对应的Chunk没有被加载,那么就创建一个script标签去加载该Chunk。并把该Chunk加入到缓存中
- 返回对应的Chunk被Promise处理过的结果。
在这里chunkId = 0对应的是源码中的async模块,所以这里表示去加载async异步模块,其实从构建出来的0.js中也可以得到该结果,0.js内容如下:
(window["webpackJsonp"] = window["webpackJsonp"] || []).push(
[
[0],
{
"./src/async.js":
(function (module, __webpack_exports__, __webpack_require__) {
"use strict";
eval(`
__webpack_require__.r(__webpack_exports__);
__webpack_exports__["default"] = ('async');
`);
})
}
]
);
0.js中的window[“webpackJsonp”] = window[“webpackJsonp”] || []).push方法的功能是由如下代码实现:
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;
而webpackJsonpCallback函数用于如何处理异步加载的模块。
至此,我们分析了输出文件中如何加载同步模块和异步模块,已经如何处理模块之间的依赖。知道了为什么输出文件能够直接在浏览器中运行。并不是所有的输出文件内容都是如此,但是整体结构和思想都是一样的,如果你对输出文件感兴趣,可以针对不同项目去尝试分析不同的输出文件,你将受益匪浅。本章节提供生成输出文件的源码:https://gitee.com/mvc_ydb/webpack/blob/master/webpack_demo.zip