一图看懂Webpack打包流程

381 阅读12分钟

一、基本概念引入

早期前端开发中,项目经常面临一些问题:

  1. 如果需要引入一些文件,那么必须通过

  2. 大型项目未压缩的代码、未合并的请求都会导致加载速度慢等性能问题。

由此,webpack应运而生,它是一个现代 JavaScript 应用程序的静态模块打包工具,其核心目标是将分散的模块及其依赖关系整合成优化的静态资源(如 JS、CSS 文件,无需手动引入)。除此之外,还会处理一些项目中的性能问题比如代码分割等等。

通过这篇文章,大家可以了解到:

  1. webpack基本使用

  2. webpack打包的详细流程

  3. 熟悉和常用的高级功能和特性在webpack中的作用阶段

二、主要内容

webpack基本使用

首先来了解webpack的基本使用,偏向于实际应用层面,也就是我们熟悉的配置文件开始切入。

(这里具体配置的含义可以自行查阅,比较简单)

const path = require('path');//必要模块 用于绝对路径问题
const HtmlWebpackPlugin = require('html-webpack-plugin');//导入的plugin

module.exports = {
  // 入口文件路径(支持单入口或多入口)
  entry: './src/index.js', 
  
  // 输出配置
  output: {
    filename: 'bundle.js',           // 输出文件名(单入口适用)
    path: path.resolve(__dirname, 'dist'),  // 输出路径(必须为绝对路径)
    clean: true,                     // 自动清理输出目录(Webpack 5 特性)
  },
  // 模块处理规则
  module: {
    rules: [
      // 处理 JavaScript 文件(Babel 转译)
      {
        test: /\.js$/,               // 表明匹配所有 .js 文件
        exclude: /node_modules/,     // 表明排除 node_modules 目录
        use: 'babel-loader',         // 表明使用 Babel loader 转换 ES6+ 语法
      },
      // 处理 CSS 文件
      {
        test: /\.css$/,              // 匹配所有 .css 文件
        use: ['style-loader', 'css-loader'],  // 顺序从右到左执行
      },
      // 处理图片和字体文件
      {
        test: /\.(png|svg|jpg|jpeg|gif|woff|woff2|eot|ttf|otf)$/,
        type: 'asset/resource',      // Webpack 5 内置资源处理
      },
    ],
  },
  // 开发模式优化
  mode: 'development',// 可选 'development' 或 'production'
  plugins: [
  new HtmlWebpackPlugin({//插件的具体配置项
    template: './src/index.html', 
    filename: 'index.html',       
    chunks: ['main'],             
    minify: { collapseWhitespace: true } 
  })
]
};

由于webpack本身配置项和一些核心概念都有一些特有的术语,为了不造成理解上的问难,所以将常见的名词和解释整理为如下表格供参考。

了解了打包的基本配置,对于输入输出文件以及loader、plugin的概念都有一定的了解,那么我们的代码怎么变成最后的产物,中间经历了哪些具体的过程,就需要深层的探究webpack打包的流程。

webpack打包的详细流程

首先是一张流程图,通过流程图再进行一些细致的讲解:

图片展示了webpack工作流程可分为几个主要阶段以及每个阶段具体的流程,接下来解释一些关键的类和函数等来帮助理解。

createdmounteduseEffect

  • 初始化阶段:准备工作,流程比较简单

    • 创建compiler对象:编译管理器,webpack启动后在初始化阶段自动创建,该对象一直存活到构建结束。

    • 执行compiler.compile方法:这一步非常关键,虽然没有实质性的功能逻辑,但是搭建了后续的流程框架。比如会在compile方法中创建compilation对象、触发make钩子(进入构建阶段的标志)、执行compilation.seal函数(进入生成阶段的标志)、触发afterCompile钩子执行收尾逻辑。

  • 构建阶段:读入并理解所有的原始代码,构建出项目整体module集合以及module之间的依赖关系图

    • 构建阶段的模块源码经历的流程:

      源码 => EntryDependency => Module => AST => (递归重复前面过程) => dependences => Modules×N

    • 遍历AST过程中触发的各种钩子构建依赖关系,具体过程:

      遇到import语句时触发exportImportSpecifier钩子

      另一个钩子监听该钩子并将依赖资源添加为Dependency对象

      调用module对象的addDependency将Dependency对象转为Module对象并添加到依赖数组dependences

      Why 钩子 监听 钩子 ?

      **疑问的产生:**在大多数系统中,钩子的设计都是单一触发的,也就是说钩子直接监听一些原生事件从而挂载到执行流程中独立处理逻辑。That's to say , 钩子之间一般不会出现层级嵌套,钩子的执行也不依赖于其他钩子的输出。(比如vue中的生命周期钩子,created在组件实例创建完成后触发,mounted在 DOM 挂载完成后触发;再比如 React 的useEffect 的依赖项数组为空,则仅在组件挂载时执行一次,不依赖任何其他状态或钩子)

      但是webpack构建流程中出现了钩子分层触发的机制,比如一个钩子会依赖另一个钩子的输出。常规钩子仅处理事件响应,而Webpack钩子需同时承担流程编排职责,这种设计使钩子成为流程控制节点而非纯粹事件触发器。(比如流程图中的compile钩子负责整个编译过程,其中就会有make、seal等钩子具体负责构建或者生成)

      **合理性解释:**这其实可以理解为Webpack在解耦与灵活性之间的折中,既保证功能的分离(make负责构建、seal负责生成),又确保了钩子间的触发顺序和时机(seal需要用到make阶段生成的依赖图从而确保先后顺序)

      (也依赖于tapable库的底层支持,提供瀑布流、熔断等多种类型的钩子)

      **辩证看待:**这样钩子之间产生了“联系”的方式虽然让流程控制更加严谨,但是也会带来一些问题,比如排查bug时,需要追踪钩子调用链来定位错误。

    • DependencyGraph对象:一个图结构存储模块依赖所有信息。

      “节点”——ModuleGraph**:记录 Dependency Graph 信息的容器**,记录构建过程中涉及到的所有 module、dependency 对象,以及这些对象互相之间的引用;

      **“指针”——ModuleGraphConnection:**记录模块间引用关系的数据结构,内部通过 originModule 属性记录引用关系中的父模块,通过 module 属性记录子模块;

      **“邻接矩阵”——ModuleGraphModule:**Module 对象在 Dependency Graph 体系下的补充信息,包含模块对象的 incomingConnections —— 指向模块本身的 ModuleGraphConnection 集合,即谁引用了模块自身;outgoingConnections —— 该模块对外的依赖,即该模块引用了其他那些模块。

  • 生成阶段:将Module对象拆分并编排到若干Chunk对象中,并输入最终产物

  • chunk & chunk group & chunk graph

    • chunk:根据模块依赖合并多个module,输出成资产文件

    • _chunk分类:_分为entry chunk、async chunk和runtime chunk。

    • Entry chunk:同一个entry下触达到的模块组织成一个chunk;

    • Async chunk:异步模块单独组织为chunk;

    • Runtime chunk:运行时模块单独组织成一个chunk;

    • chunk group:一个chunk group内包含一个或多个chunk对象;chunk groups之间形成父子关系

    • chunk graph:存储chunk之间、chunk group之间依赖关系的对象

  • 生成阶段详解

  1. 遍历entry配置,为每个入口创建空的chunk对象以及空的chunk group对象(这里叫entry point,一种特殊的chunk group对象),初步设置好chunk graph结构关系。类似初始化工作。

  2. 在buildChunkGraph函数内调用visitModule函数,遍历构建阶段创建好的ModuleGraph,将所有module按照依赖关系分配给chunk对象。如果遇到异步模块,则会创建新chunk对象和chunk group对象。

  3. 在buildChunkGraph函数中调用connectChunkGroups方法,建立chunkgroup之间和chunk之间的依赖关系,生成完整的chunkGraph对象。

  4. 自此modulegraph中的所有module都被分配到entry、async、runtime(后面深入)三种chunk对象中,并将chunk之间的关系存储到chunkgraph和chunkgroup中,后续可以在这些对象上继续修改分包策略,重新组织分配chunk结构实现分包优化。

有了关键类和函数,流程也就变得清晰了起来。那么我们熟悉的webpack高级特性(比如tree shaking和HMR)又是作用在流程的哪些阶段以及具体如何作用的呢,下面以HMR为例讲解一下。

HMR与webpack

  1. 概念:

    在 HMR 之前,应用的加载、更新都是一种页面级别的操作,即使只是单个代码文件发生变更,都需要刷新整个页面,才能将最新代码映射到浏览器上,这会丢失之前在页面执行过的所有交互与状态(比如计数器状态丢失或者弹框消失等等)

    HMR能够在保持页面状态不变(无刷新)的情况下动态替换、删除、添加代码模块,提供丝滑顺畅的 Web 页面开发体验。

    基本实现:

    module.exports = {
      // ...
      devServer: {
        // 必须设置 devServer.hot = true,启动 HMR 功能
        hot: true
      }
    };
    
    // 示例:监听特定模块更新
    if (module.hot) {
      module.hot.accept('./moduleA.js', () => {
        console.log('模块A已更新');
        // 手动执行更新逻辑(如重新渲染组件)
      });
    }
    
  2. 原理:

    1. 注入客户端运行时(webpack初始化阶段)

    Webpack Dev Server(WDS)(webpack官方提供的工具,通过在本地搭建一个具备实时更新能力的开发服务器,帮助开发者快速调试代码)在启动时,通过插件(如 HotModuleReplacementPlugin)向浏览器注入一段负责通信模块更新的代码(HMR Runtime)。这段代码会随应用主文件(如 main.js)一起加载到浏览器中。

    1. 建立双向通信通道(webpack初始化阶段)

    浏览器加载页面后,HMR Runtime(负责通信的部分)与 WDS 通过 WebSocket 建立长连接。这个通道用于传输更新通知和模块变更信息。

    WebSocket

    WebSocket 通过 单条持久化 TCP 连接 实现客户端与服务器的双向实时通信,突破了 HTTP 的“请求-响应”单向模式

    1. 文件修改与增量构建(构建阶段:watch监听)

    当开发者修改代码时,Webpack 的 watch 模式 会监听到文件变化,并仅重新编译受影响的模块(而非整个应用)。编译完成后生成两个补丁文件:

    • Manifest(清单):JSON 文件,记录哪些模块需要更新

    • Chunk 文件:包含更新后的模块代码

    随后,WDS 通过 WebSocket 发送 hash 事件(表明需要更新)。

    1. 客户端拉取更新清单

    浏览器收到 hash 事件后,根据版本号向 WDS 请求 manifest 文件,确认需要更新的模块范围

    1. 加载新模块代码

    HMR Runtime(负责模块更新)根据清单中的模块列表,通过 JSONP 或 AJAX 请求新的模块文件(以chunk为单位)(如 main.[hash].hot-update.js),并将其加载到浏览器内存中。

    1. 模块替换与状态保留

    HMR Runtime 遍历旧模块的依赖链,找到需要更新的模块:

    • 若模块定义了 module.hot.accept,执行回调函数(如重新渲染组件)

    module.hot.accept 是 HMR 运行时暴露给用户代码的重要接口之一。

    它在 Webpack HMR 体系中开了一个口子,让用户能够自定义模块热替换的逻辑。

    _module.hot.accept(path?: string, callback?: function)_

    • 若未定义,则向上冒泡至父模块,直至找到可处理更新的节点

    旧模块会被卸载(dispose),新模块代码被插入,但应用全局状态(如变量、DOM)保持不变

    1. 容错与兜底机制

    若模块更新失败(如代码冲突),HMR 会回退到 整页刷新(Live Reload),确保应用最终状态正确

    也可以手动在accept回调函数中编写兜底机制~~

三、总结

  • 关于webpack学习

  • 由此抽象出的 : 发现问题—>解决问题 的学习思路

首先对于一个问题,特别是工程化相关的问题,从原理入手难免有些枯燥和复杂。因此从应用层面入手会显得稍微简单和清晰一点。通过实际的应用从而对其概念和优点有一定的了解,后续学习也会有一定印象。比如:在学习webpack初期,从具体的配置文件入手,看看开发模式和生产模式产出的代码到底有什么区别等等。

其次,在了解了应用层面之后需要多问为什么,比如:loader中的参数是怎么获取到的?plugin中的hooks是怎么由webpack注入和触发的?以问题为导向去理解深层次的原理,就可以帮助自己理解的更透彻。

最后,解决了问题需要再次回归实践,去检验自己到底有没有弄明白流程或者原理,在此过程中可能会发现新问题,那么就需要对应的去解决,以此循环,从而掌握知识,并能有自己的理解。比如:自己尝试开发一个webpack插件,过程中可能会遇到以前没有详细了解过的关键钩子,那么就需要对应去深入了解各种常用hooks和对应的上下文参数等等。

最后感谢大家的观看~~欢迎指正不对和需要改进的地方~~