揭秘 Webpack
我们所有人肯定都曾使用过 webpack。它是迄今为止最受欢迎的打包工具,因为它拥有海量的加载器,并且为打包过程带来了可定制性。在某种程度上,webpack 影响了某些 JavaScript 生态系统的发展。但我们有多少次想过打开打包文件,了解打包过程中发生了什么。我那个包含数百个独立文件的应用程序是如何从一个打包文件中如此美观且有凝聚力地运行的?让我们分解一下 webpack 的概念,了解打包过程中发生了什么。我不会详细讨论 webpack 配置中的元素,因为它们在 webpack 文档中已经提到过了,而是会讨论 webpack 的核心概念。
什么是捆绑器?
在进一步了解之前,我们先来了解一下什么是打包器。打包器是一个实用程序/程序,它能将多个文件打包在一起,并以一种不改变代码运行方式的方式将它们组合在一起。这允许你以模块化的方式编写代码,但将它们作为一个整体文件来使用。
为什么我们需要一个捆绑器?
如今,为了兼顾可维护性和可复用性,我们越来越多地采用模块化的方式编写代码。如果应用程序规模较小,这种模块化风格可以很好地工作。但随着应用程序规模和复杂性的扩大,在运行模块化代码的同时管理日益增多的依赖项和代码变得越来越困难。例如,假设您正在创建一个包含 50 个 JS 模块的 HTML/JavaScript 应用程序。现在,您的 HTML 代码无法承受在页面中使用 50 个脚本标签。这时,打包器就派上用场了,它会将这 50 个文件打包在一起,生成一个文件,您只需在 HTML 文件中使用一个脚本标签即可使用该文件。
揭秘 webpack
好的,基础知识已经足够了,现在让我们深入研究 webpack。
考虑三个文件
// A.js
const B = require('./B');
B.printValue();
// B.js
const C = require('./C.js')
const printValue = () => {
console.log(`The value of C.text is ${C.text}`);
};
module.exports = {
printValue,
};
// C.js
module.exports = {
text: 'Hello World!!!',
};
我把A.js
webpack 的入口点和输出都定义为一个打包文件。运行 webpack build 时,就会发生这两件事。
- 形成依赖图
- 解决依赖关系图和 Tree-Shaking
形成依赖图
webpack 首先会分析现有的模块,并形成依赖图。依赖图是一个有向图,它描述了每个模块如何连接到另一个模块。它在 npm、maven、snap 等包管理器中非常流行。它从入口点开始A.js
,我们的依赖图最初看起来像这样,只有一个节点。
然后 webpack 知道这B.js
是必需的A.js
,所以它会在图中创建从 A 到 B 的链接。
现在分析B.js
发现它C.js
也需要。所以在图中它再次创建了从 B 到 C 的链接。
现在假设A.js
需要另一个名为的文件,D.js
而该文件又需要C.js
图表
瞧,这东西相对简单。现在C.js
webpack 意识到它不再有模块依赖了,所以输出了完整的依赖图。
解析模块
好了,现在 Webpack 有了依赖图和模块。它必须将它们全部放入一个文件中,因此它会从依赖图的根节点 开始,一次取出一个节点A.js
。它将 的内容复制A.js
到输出文件,将节点标记为已解析,然后转到 的子节点A.js
。假设之前已经解析过的模块再次出现,它会直接跳过它。同样,它会不断将模块的内容添加到输出文件,直到完成依赖图的遍历。
摇树
Tree-Shaking 是从输出中移除死代码的过程。webpack 在创建代码图时,还会标记该模块是否被使用。如果该模块在任何地方都没有被使用,则会被移除,因为它实际上是死代码。需要注意的是,webpack 仅在生产模式下执行此操作。
我们来看一下上面三个文件的捆绑代码。
/******/ (function(modules) {
// webpackBootstrap
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports, __webpack_require__) {
// A.js
const B = __webpack_require__(1);
B.printValue();
/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {
// B.js
const C = __webpack_require__(2)
const printValue = () => {
console.log(`The value of C.text is ${C.text}`);
};
module.exports = {
printValue,
};
/***/ }),
/* 2 */
/***/ (function(module, exports) {
// C.js
module.exports = {
text: 'Hello World!!!',
};
/***/ })
/******/ ]);
你一眼就能看出这是一个 IIFE(立即执行函数)。该函数接受一个模块列表作为参数,并执行每个模块的命令。我们可以看到,第一个模块是我们的入口文件,A.js
第二个是B.js
,第三个是C.js
。并且我们可以看到,每个模块都被修改为可执行的函数。
参数module
是默认节点对象的替代品module
。exports
是对象的替代品exports
,是程序中使用的__webpack_require__
替代品。包含函数的实现,相当长。我们只看require
// webpackBootstrap
__webpack_require__
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;
/******/ }
这段代码很容易理解,它接受一个参数moduleId
并检查该模块是否存在于installedModules
缓存中。如果不存在,则会在缓存中创建一个条目。下一行代码modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
实际上执行了我们之前传递给父函数的 modules 数组中的 module 函数。根据语法,fn.call()
我们可以推断出module
是之前创建的对象,exports
scopethis
是exports
创建的模块对象的对象,而__webpack_require__
是函数本身。然后,它将模块设置为已加载到缓存中,并返回模块的导出。
好了,以上就是 Webpack 的基本工作原理。Webpack 还有很多更强大的功能,比如通过特定方式对模块进行排序来最小化初始负载,我强烈建议大家去探索一下。
在开始使用一个实用程序之前,最好先了解它的工作原理。这有助于我们根据所使用的实用程序的内部工作原理和限制,编写出更优化的代码。
文章来源:https://dev.to/sadarshannaiynar/demystifying-webpack-2f5n