webpack 打包原理及流程解析

    • 1. 什么是webpack?
    • 2. 关键术语解析
    • 3. webpack核心概念
    • 4. webpack 构建流程
    • 5. webpack应用案例
    • 6. 打包分析

1. 什么是webpack?

友情提示:
a.前面会稍微有些枯燥,文字居多(建议还是过一遍),后面就劲爆了!!!

b.本文干货满满,非常详细,整理资料到发布文章耗时5个小时+,请大家耐心看

  • 本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图,其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
  • webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。
  • 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。
  • webpack 通过 Tapable 来组织这条复杂的生产线。 webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。
  • 官方的说法看不太懂的,可以看这个:
  • 在目前的项目中,我们会有很多依赖包,webpack负责将浏览器不能识别的文件类型、语法等转化为可识别的前端三剑客(html,css,js),并在这个过程中充当组织者与优化者的角色。

2. 关键术语解析

2.1 bundle

  • Bundle(捆绑包)是指将所有相关的模块和资源打包在一起形成的单个文件。它是应用程序的最终输出,可以在浏览器中加载和执行。
  • 捆绑包通常由Webpack根据入口点(entry)和它们的依赖关系自动创建。当你运行Webpack构建时,它会根据配置将所有模块和资源打包成一个或多个捆绑包。

2.2 Chunk

  • Chunk(代码块)是Webpack在打包过程中生成的中间文件,它代表着一个模块的集合

  • Webpack 根据代码的拓扑结构和配置将模块组织成不同的代码块。每个代码块可以是一个独立的文件,也可以与其他代码块组合成一个捆绑包。

  • Webpack使用代码分割(code splitting)技术将应用程序代码拆分成更小的代码块,以便在需要时进行按需加载。这有助于减小初始加载的文件大小,提高应用程序的性能。

  • 在Webpack中,捆绑包和代码块之间存在一对多的关系。一个捆绑包可以包含多个代码块,而一个代码块也可以属于多个不同的捆绑包。这取决于Webpack配置中的拆分点(split points)和代码块的依赖关系。

  • 总结起来,bundle 是Webpack打包过程的最终输出文件,而chunk是Webpack在打包过程中生成的中间文件,用于组织和按需加载模块

3. webpack核心概念

3.1 Entry

  • 入口起点(entry point)指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。
  • 进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。
  • 每个依赖项随即被处理,最后输出到称之为 bundles 的文件中。

3.2 Output

  • output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认值为 ./dist。
  • 基本上,整个应用程序结构,都会被编译到你指定的输出路径的文件夹中。

3.3 Module

  • 模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。

3.4 Chunk

  • 代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。

3.5 Loader

  • loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。
  • loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。
  • 本质上,webpack loader 将所有类型的文件,转换为应用程序的依赖图(和最终的 bundle)可以直接引用的模块。

3.6 Plugin

  • loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。
  • 插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。

4. webpack 构建流程

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程

  • 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。
  • 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。
  • 确定入口:根据配置中的 entry 找出所有的入口文件。
  • 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
  • 完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。
  • 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。
  • 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
  • 在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

5. webpack应用案例

5.1 前置条件

  • 首先检查node版本,建议使用 16.16.x 及以上版本(因为玩的是webpack 4.0+ 的版本)
  • 全局安装 => npm install webpack -g
  • 当然,也可以带版本号,如 webpack@4.4.0
  • 这里使用的是最新的(不带版本号默认安装最新版本)
  • 当然,也可以局部安装(全局可能会影响你的其它项目),5.2 步骤将会做的是局部安装

5.2 初始化项目

  • 整个空文件夹,执行npm init,然后一路回车,把回车摁烂!!!
  • 当然你也可以搞一些个性化配置,文件名,版本,描述,入口文件等等。
  • 局部安装webpack:npm i webpack webpack-cli -D
  • 然后你会看到 package.json 这个鸟样:

5.3 新建 webpack.config.js 配置文件(根目录)

const path = require('path');module.exports = {// 入口entry: path.resolve(__dirname, 'src/index.js'),// 出口output: {path: path.resolve(__dirname, 'dist'),filename: 'bundle.js',publicPath: './'}}
  • publicPath:指定基础路径,开发环境一般是项目的根路径,上线之后一般是CDN的路径。
  • __dirname:表示项目所在目录的根路径。

5.4 src/index.js 与 test.js

  • 新建入口文件src/index.js,随便整点代码:
const test = require('./test');const a = 12const b = 12function add(x, y) {return x + y}const c = add(a,b)console.log(c)test();
  • src/test.js,非主入口文件用来测试打包的
functiontest(){console.log(2);}module.exports = test;

5.5 测试打包

  • package.json 下的 scripts 中添加打包命令:
"build": "webpack --mode development"
  • 这里说明一下,不加 –mode development,默认打包是生产环境,打包出来的代码会默认压缩,看不得,辣眼睛
  • 运行 npm run build,进行打包。
  • 如果不出意外的话,控制台就成功的打包了两个js文件,同时目录中会生成 dist 文件夹(出意外的话看下上面的步骤)

6. 打包分析

  • 刚刚的步骤是一个非常简单的打包示例,打包后你会发现两个js文件变成了一个
  • 试运行一下,也是没问题的,如下

6.1 源码分析

  • 源码实际上很好懂,代码量也不多,不妨来解读一下:
/* * ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development"). * This devtool is neither made for production nor for readable output files. * It uses "eval()" calls to create a separate source file in the browser devtools. * If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/) * or disable the default devtool with "devtool: false". * If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/). *//******/ (() => { // webpackBootstrap/******/ var __webpack_modules__ = ({/***/ "./src/index.js":/*!**********************!*\!*** ./src/index.js ***!\**********************//***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {eval("const test = __webpack_require__(/*! ./test */ \"./src/test.js\");\r\nconst a = 12\r\nconst b = 12\r\nfunction add(x, y) {\r\nreturn x + y\r\n}\r\nconst c = add(a,b)\r\nconsole.log(c)\r\ntest();\n\n//# sourceURL=webpack://blog/./src/index.js" />);/***/ }),/***/ "./src/test.js":/*!*********************!*\!*** ./src/test.js ***!\*********************//***/ ((module) => {eval("functiontest(){\r\nconsole.log(2);\r\n}\r\n\r\nmodule.exports = test;\r\n\n\n//# sourceURL=webpack://blog/./src/test.js?");/***/ })/******/ });/************************************************************************//******/ // The module cache/******/ var __webpack_module_cache__ = {};/******/ /******/ // The require function/******/ function __webpack_require__(moduleId) {/******/ // Check if module is in cache/******/ var cachedModule = __webpack_module_cache__[moduleId];/******/ if (cachedModule !== undefined) {/******/ return cachedModule.exports;/******/ }/******/ // Create a new module (and put it into the cache)/******/ var module = __webpack_module_cache__[moduleId] = {/******/ // no module.id needed/******/ // no module.loaded needed/******/ exports: {}/******/ };/******/ /******/ // Execute the module function/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);/******/ /******/ // Return the exports of the module/******/ return module.exports;/******/ }/******/ /************************************************************************//******/ /******/ // startup/******/ // Load entry module and return exports/******/ // This entry module can't be inlined because the eval devtool is used./******/ var __webpack_exports__ = __webpack_require__("./src/index.js");/******/ /******/ })();
  • 可以看到最简单的场景下 webpack 实现的模块加载系统非常简洁,仅仅只有60多行代码
  • 打包后的代码其实是一个立即执行函数,在Webpack打包过程中,每个模块都会被转换为一个独立的函数,并通过__webpack_modules__对象进行注册和管理。这个对象以文件路径为 key,以文件内容为 value,它包含了所有打包后的模块。
  • 当模块被引用或加载时,Webpack会使用__webpack_modules__来查找和执行相应的模块函数。通过使用__webpack_modules__,Webpack可以管理模块之间的依赖关系,并在需要时按需加载和执行模块。
  • 接着定义了一个模块加载函数 webpack_require()它接收的参数是 moduleId,其实就是文件路径。
  • 它的执行过程如下:
    • 判断模块是否有缓存,如果有则返回缓存模块的 export 对象,即 module.exports。
    • 新建一个模块 module,并放入缓存。
    • 执行文件路径对应的模块函数。
    • 执行完模块后,返回该模块的 exports 对象。
  • 其中 module、module.exports 的作用和 CommonJS 中的 module、module.exports 的作用是一样的,而 webpack_require 相当于 CommonJS 中的 require。
  • 在立即函数的最后,使用了 webpack_require() 加载入口模块。并传入了入口模块的路径 ./src/index.js。
  • 我们再来分析一下入口模块的内容:
((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {eval("const test = __webpack_require__(/*! ./test */ \"./src/test.js\");\r\nconst a = 12\r\nconst b = 12\r\nfunction add(x, y) {\r\nreturn x + y\r\n}\r\nconst c = add(a,b)\r\nconsole.log(c)\r\ntest();\n\n//# sourceURL=webpack://blog/./src/index.js?");})
  • 入口模块函数的参数正好是刚才所说的那三个参数(module、module.exports 、require),而 eval 函数的内容美化一下后和下面内容一样:
const test = __webpack_require__(/*! ./test */ "./src/test.js");function test(){console.log(2);}test();
  • 将打包后的模块代码和原模块的代码进行对比,可以发现仅有一个地方发生了变化,那就是 require 变成了 webpack_require。
  • 从刚才的分析可知,webpack_require() 加载模块后,会先执行模块对应的函数,然后返回该模块的 exports 对象。
  • 而 test.js 的导出对象 module.exports 就是 test() 函数。所以入口模块能通过 webpack_require() 引入 test() 函数并执行
  • 到目前为止可以发现 webpack 自定义的模块规范完美适配 CommonJS 规范。

1. 希望本文能对大家有所帮助,如有错误,敬请指出

2. 原创不易,还请各位客官动动发财的小手支持一波(关注、评论、点赞、收藏)
3. 拜谢各位!后续将继续奉献优质好文
4. 如果存在疑问,可以私信我(主页有V)