打包原理
- 简单讲就是生成ast语法树,根据语法树生成对应的js代码
- 这里仅分析打包输出的结果,从结果分析webpack对我们代码做了啥
分析
// main.js
// 通过CommonJS规范导入
const show = require('./show.js');
// 执行 show 函数
show('Webpack');
// show.js
// 操作 DOM 元素,把 content 显示到网页上
function show(content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
}
// 通过 CommonJS 规范导出 show 函数
module.exports = show;
// bundle.js
(
// 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;
})
]
);
// 以上看上去复杂的代码其实是一个立即执行函数,可以简写为如下:
(function(modules) {
// 模拟 require 语句
function __webpack_require__() {
}
// 执行存放所有模块数组中的第0个模块
__webpack_require__(0);
})([/*存放所有模块的数组*/])
- 可以看到
bundle.js
是一个自执行函数,入参就是main.js
和show.js
改造后的代码块所构成的数组 - 自执行函数里运行了
__webpack_require__
这个函数,入参是0,0其实就是代码块数组中对应的入参,表示第一个代码块 - 再来看
__webpack_require__
函数,首先执行的是缓存判断,通过moduleId
判断之前是否已经加载过,如果加载过,直接返回直接的加载结果exports
,mouduleId
就是不同代码模块在入参数组中的index - 而如果没有加载过,则新建一个对象,重要的是这个对象中的
exports
属性,里面存放的就是加载模块后,对应模块export出来的东西 - 然后用这个
exports
作为上下文去执行对应的代码块,传递参数为刚才新建的module,module里的exports,以及__webpack_require__
这个方法本身 - 然后看到
main.js
中的require被改造成了__webpack_require__
,__webpack_require__(1)
代表加载第二个代码块 - 第二个代码块中,定义了show这个方法,然后show会作为module.exports的导出,也就是赋值给了
installedModules[0].module.exports
,也就是这个导出已经被缓存起来了,下次再有别的地方用到,会直接被导出 - 这就是webpack大致的打包思路,将各个单独的模块改造成数组作为入参,传给自执行函数,同时维护一个
installedModules
记录加载过的模块,利用模块数组中的index作为key值,exports记录导出对象
按需加载
- 由于单页应用也会有路由这个概念,在没有切换到对应路由之前,可能并不希望浏览器对这部分页面的js进行下载,从而提升首页打开的速度,就涉及到一个懒加载,即按需加载的问题
- webpack的按需加载是通过
import(XXX)
实现的,import()是一个提案,而webpack支持了它
// 异步加载 show.js
import(/* webpackChunkName: 'show' */ './show').then((module) => {
// 执行 show 函数
const show = module.default;
show('Webpack');
});
- 通过这种方式打包,我们可以发现最终打包出来的文件分成了两个,
bundle.js
和show.xxx.js
- 其中
/* webpackChunkName: 'show' */
是专门注释给webpack看的,为的是指定按需加载的包的名字,同时记得在webpack的配置文件的entry中,配置chunkFilename: '[name].[hash].js'
,不然这个指定不会生效 - 先来看入口文件,将暂时没有用到的函数都隐藏后如下:
(function (modules) {
// webpackJsonp 用于从异步加载的文件中安装模块
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
// ... 先省略
};
// 缓存已经安装的模块
var installedModules = {};
// 存储每个 Chunk 的加载状态;
// 键为 Chunk 的 ID,值为0代表已经加载成功
var installedChunks = {
1: 0
};
// 模拟 require 语句,和上面介绍的一致
function __webpack_require__(moduleId) {
// ... 省略和上面一样的内容
}
// 用于加载被分割出去的,需要异步加载的 Chunk 对应的文件
__webpack_require__.e = function requireEnsure(chunkId) {
// ... 先省略
};
// 加载并执行入口模块,和上面介绍的一致
return __webpack_require__(__webpack_require__.s = 0);
})
(
// 存放所有没有经过异步加载的,随着执行入口文件加载的模块
[
// main.js 对应的模块
(function (module, exports, __webpack_require__) {
// 通过 __webpack_require__.e 去异步加载 show.js 对应的 Chunk
__webpack_require__.e('show').then(__webpack_require__.bind(null, 'show')).then((show) => {
// 执行 show 函数
show('Webpack');
});
})
]
);
- 可以看到
import(xxx).then
被替换成了__webpack_require__.e(0).then
,__webpack_require__.e(0)
返回了一个promise - 第一个then里相当于执行了
__webpack_require__(1)
,但很明显可以看到自执行函数的入参数组只有一个元素,不存在[1],这个[1]是什么时候被插入的呢 - 看一下
__webpack_require__.e
的实现
__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;
};
- 首先判断这个chunkId是否已经加载过,如果是的话,直接返回一个resolve的promise
- 如果不为空又不为0,说明正在加载中,这里的
installedChunks[chunkId]
是一个数组,里面保存着[resovle, reject]
,是在发起网络请求的时候赋值的 - 如果上面两个判断都没击中,说明是没有加载过,下面开始构造加载方法,主要是通过jsonp的形式
- 首先新建一个promise,并对
installedChunks[chunkId]
赋值,把这个promise以及他的resolve和reject保存在里面,这也是上面为什么可以通过判断installedChunks[chunkId]
不为空又不为0即正处于请求当中,直接返回数组第三个值,即新建的promise,让后续操作可以在这个promise上进行回调的注册 - 然后后面的方法就是通过构造一个script标签,插入到head中,保证代码能马上被下载,同时定义代码执行完毕时的回调,判断是已经加载了代码,如果加载成功清除监听等,如果加载失败,抛出异常
- 最后返回这个promise,供外部注册回调
- 而这里通过jsonp加载的代码就是打包分离出来的另一个文件
show.xx.js
,也就是异步加载的show.js
相关的代码
webpackJsonp(
// 在其它文件中存放着的模块的 ID
['show'],
// 本文件所包含的模块
{// show.js 所对应的模块
show: (function (module, exports) {
function show(content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
}
module.exports = show;
})
}
);
- 接着看webpackJsonp这个方法是怎么定义的
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()();
}
};
chunkIds
代表自己这个文件的id名,因为动态加载的时候,是利用动态加载的文件名构成script标签进行下载的,这里传入这个id是为了触发后续promise的resolve以及标记模块以及被加载moreModules
就是对应的代码模块集合executeModules
就是加载完成后需要被执行模块的index- 首先遍历
installedChunks
,前面提到过installedChunks[chunkId]
通过网络下载的时候,回赋予三个值,代表其对应的promise,这里取出第一个resolve,保存起来,同时将加载标记置为0,表示已加载 - 然后遍历动态加载的模块,把代码块塞到modules数组里
- 最后执行之前保存下来的resolve函数,触发
__webpack_require__.e(0).then
的执行 - 这样动态加载的代码通过构造jsonp进行下载,并且将对应代码传到
bundle.js
的modules中进行保存,然后在then函数中通过__webpack_require__
执行模块,缓存输出 - 这里为了便于理解,有对代码做一定调整,真实的输出情况,可以通过具体打包输出查看,这里仅描述具体打包思路
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。