虽然在前面的章节中你学会了如何使用 Webpack ,也大体知道其工做原理,但是你想过 Webpack 输出的 bundle.js
是什么样子的吗? 为何原来一个个的模块文件被合并成了一个单独的文件?为何 bundle.js
能直接运行在浏览器中? 本节将解释清楚以上问题。javascript
先来看看由 1-3安装与使用 中最简单的项目构建出的 bundle.js
文件内容,代码以下:html
(
// webpackBootstrap 启动函数
// modules 即为存放全部模块的数组,数组中的每个元素都是一个函数
function (modules) {
// 安装过的模块都存放在这里面
// 做用是把已经加载过的模块缓存在内存中,提高性能
var installedModules = {};
// 去数组中加载一个模块,moduleId 为要加载模块在数组中的 index
// 做用和 Node.js 中 require 语句类似
function __webpack_require__(moduleId) {
// 若是须要加载的模块已经被加载过,就直接从内存缓存中返回
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 若是缓存中不存在须要加载的模块,就新建一个模块,并把它存在缓存中
var module = installedModules[moduleId] = {
// 模块在数组中的 index
i: moduleId,
// 该模块是否已经加载完毕
l: false,
// 该模块的导出值
exports: {}
};
// 从 modules 中获取 index 为 moduleId 的模块对应的函数
// 再调用这个函数,同时把函数须要的参数传入
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 把这个模块标记为已加载
module.l = true;
// 返回这个模块的导出值
return module.exports;
}
// Webpack 配置中的 publicPath,用于加载被分割出去的异步代码
__webpack_require__.p = "";
// 使用 __webpack_require__ 去加载 index 为 0 的模块,而且返回该模块导出的内容
// index 为 0 的模块就是 main.js 对应的文件,也就是执行入口模块
// __webpack_require__.s 的含义是启动模块对应的 index
return __webpack_require__(__webpack_require__.s = 0);
})(
// 全部的模块都存放在了一个数组里,根据每一个模块在数组的 index 来区分和定位模块
[
/* 0 */
(function (module, exports, __webpack_require__) {
// 经过 __webpack_require__ 规范导入 show 函数,show.js 对应的模块 index 为 1
const show = __webpack_require__(1);
// 执行 show 函数
show('Webpack');
}),
/* 1 */
(function (module, exports) {
function show(content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
}
// 经过 CommonJS 规范导出 show 函数
module.exports = show;
})
]
);
复制代码
以上看上去复杂的代码实际上是一个当即执行函数,能够简写为以下:java
(function(modules) {
// 模拟 require 语句
function __webpack_require__() {
}
// 执行存放全部模块数组中的第0个模块
__webpack_require__(0);
})([/*存放全部模块的数组*/])
复制代码
bundle.js
能直接运行在浏览器中的缘由在于输出的文件中经过 __webpack_require__
函数定义了一个能够在浏览器中执行的加载函数来模拟 Node.js 中的 require
语句。webpack
原来一个个独立的模块文件被合并到了一个单独的 bundle.js
的缘由在于浏览器不能像 Node.js 那样快速地去本地加载一个个模块文件,而必须经过网络请求去加载还未获得的文件。 若是模块数量不少,加载时间会很长,所以把全部模块都存放在了数组中,执行一次网络加载。web
若是仔细分析 __webpack_require__
函数的实现,你还有发现 Webpack 作了缓存优化: 执行加载过的模块不会再执行第二次,执行结果会缓存在内存中,当某个模块第二次被访问时会直接去内存中读取被缓存的返回值。数组
在采用了 4-12 按需加载 中介绍过的优化方法时,Webpack 的输出文件会发生变化。promise
例如把源码中的 main.js
修改成以下:浏览器
// 异步加载 show.js
import('./show').then((show) => {
// 执行 show 函数
show('Webpack');
});
复制代码
从新构建后会输出两个文件,分别是执行入口文件 bundle.js
和 异步加载文件 0.bundle.js
。缓存
其中 0.bundle.js
内容以下:网络
// 加载在本文件(0.bundle.js)中包含的模块
webpackJsonp(
// 在其它文件中存放着的模块的 ID
[0],
// 本文件所包含的模块
[
// show.js 所对应的模块
(function (module, exports) {
function show(content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
}
module.exports = show;
})
]
);
复制代码
bundle.js
内容以下:
(function (modules) {
/*** * webpackJsonp 用于从异步加载的文件中安装模块。 * 把 webpackJsonp 挂载到全局是为了方便在其它文件中调用。 * * @param chunkIds 异步加载的文件中存放的须要安装的模块对应的 Chunk ID * @param moreModules 异步加载的文件中存放的须要安装的模块列表 * @param executeModules 在异步加载的文件中存放的须要安装的模块都安装成功后,须要执行的模块对应的 index */
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
// 把 moreModules 添加到 modules 对象中
// 把全部 chunkIds 对应的模块都标记成已经加载成功
var moduleId, chunkId, i = 0, resolves = [], result;
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];
}
}
while (resolves.length) {
resolves.shift()();
}
};
// 缓存已经安装的模块
var installedModules = {};
// 存储每一个 Chunk 的加载状态;
// 键为 Chunk 的 ID,值为0表明已经加载成功
var installedChunks = {
1: 0
};
// 模拟 require 语句,和上面介绍的一致
function __webpack_require__(moduleId) {
// ... 省略和上面同样的内容
}
/** * 用于加载被分割出去的,须要异步加载的 Chunk 对应的文件 * @param chunkId 须要异步加载的 Chunk 对应的 ID * @returns {Promise} */
__webpack_require__.e = function requireEnsure(chunkId) {
// 从上面定义的 installedChunks 中获取 chunkId 对应的 Chunk 的加载状态
var installedChunkData = installedChunks[chunkId];
// 若是加载状态为0表示该 Chunk 已经加载成功了,直接返回 resolve Promise
if (installedChunkData === 0) {
return new Promise(function (resolve) {
resolve();
});
}
// installedChunkData 不为空且不为0表示该 Chunk 正在网络加载中
if (installedChunkData) {
// 返回存放在 installedChunkData 数组中的 Promise 对象
return installedChunkData[2];
}
// installedChunkData 为空,表示该 Chunk 尚未加载过,去加载该 Chunk 对应的文件
var promise = new Promise(function (resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
installedChunkData[2] = promise;
// 经过 DOM 操做,往 HTML head 中插入一个 script 标签去异步加载 Chunk 对应的 JavaScript 文件
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.charset = 'utf-8';
script.async = true;
script.timeout = 120000;
// 文件的路径为配置的 publicPath、chunkId 拼接而成
script.src = __webpack_require__.p + "" + chunkId + ".bundle.js";
// 设置异步加载的最长超时时间
var timeout = setTimeout(onScriptComplete, 120000);
script.onerror = script.onload = onScriptComplete;
// 在 script 加载和执行完成时回调
function onScriptComplete() {
// 防止内存泄露
script.onerror = script.onload = null;
clearTimeout(timeout);
// 去检查 chunkId 对应的 Chunk 是否安装成功,安装成功时才会存在于 installedChunks 中
var chunk = installedChunks[chunkId];
if (chunk !== 0) {
if (chunk) {
chunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));
}
installedChunks[chunkId] = undefined;
}
};
head.appendChild(script);
return promise;
};
// 加载并执行入口模块,和上面介绍的一致
return __webpack_require__(__webpack_require__.s = 0);
})
(
// 存放全部没有通过异步加载的,随着执行入口文件加载的模块
[
// main.js 对应的模块
(function (module, exports, __webpack_require__) {
// 经过 __webpack_require__.e 去异步加载 show.js 对应的 Chunk
__webpack_require__.e(0).then(__webpack_require__.bind(null, 1)).then((show) => {
// 执行 show 函数
show('Webpack');
});
})
]
);
复制代码
这里的 bundle.js
和上面所讲的 bundle.js
很是类似,区别在于:
__webpack_require__.e
用于加载被分割出去的,须要异步加载的 Chunk 对应的文件;webpackJsonp
函数用于从异步加载的文件中安装模块。在使用了 CommonsChunkPlugin 去提取公共代码时输出的文件和使用了异步加载时输出的文件是同样的,都会有 __webpack_require__.e
和 webpackJsonp
。 缘由在于提取公共代码和异步加载本质上都是代码分割。
本实例提供项目完整代码