Webpack打包流程简述——新手向

Webpack核心流程

Webpack的最核心的功能:静态模块打包能力 ,Webpack是现代JavaScript应用程序的静态模块打包工具。Webpack 的核心思想是 "一切皆模块",传统来讲,模块指的就是一段可复用的代码,在Webpack中,任何类型的资源都会被视为一个模块,好处是统一前端资源的管理方式

Webpack核心功能中提到了"静态"二字,该如何理解?

什么是静态?

Webpack的核心工作原理是:在构建阶段(而非运行时),通过静态分析确定所有模块的依赖关系。

静态分析:通过将源代码转换为AST,对AST遍历和分析。

静态分析特点

  1. 源代码中必须明确依赖(import,require)。
  2. 依赖路径必须是静态字符串,不能是动态字符串,如通过用户输入的变量拼接而成等。
js 复制代码
// 动态:路径依赖变量,只有运行时才知道具体加载哪个模块 
const moduleName = getModuleName(); // 函数返回值在运行时确定 
const module = require(`./modules/${moduleName}`);

// 静态
import './a.js'

通俗来说,就是代码之间的依赖关系要在代码运行前就确定好。比如说,如果引用路径里包含变量,这种情况下,只有等代码运行起来,知道了变量的具体值,才能确定实际的引用路径,这就不符合这种提前确定依赖的要求了。这个例子的描述就可以感受到Webpack概念中关于"静态"的理解。

为什么采用静态分析而不是动态分析

静态分析能在构建阶段就梳理清楚所有依赖关系 ,最终将代码打包成浏览器可直接执行的形式。这样一来,代码运行时就无需再处理依赖相关的逻辑,只需专注于响应用户操作即可。

而动态分析则意味着浏览器在运行代码的过程中,还需要实时解析依赖关系并加载相应模块,这无疑会增加页面的加载时间和运行时开销。

Webpack模块

Webpack 能够将各种类型的资源 ------ 包括图片、音视频、CSS、JavaScript 代码等,通通转译、组合、拼接、生成标准的、能够在不同版本浏览器兼容执行的 JavaScript 代码文件,这一特性能够轻易抹平开发 Web 应用时处理不同资源的逻辑差异,使得开发者以一致的心智模型开发、消费这些不同的资源文件。在构建流程中的模块的具体表现就是module对象,module是构建流程的核心数据,后便会多次提到。

CSS文件经过css-loader处理:

css 复制代码
源CSS代码:
body {
  background: #fff;
  color: #333;
}
.title {
  font-size: 20px;
}


经过转换之后的:
// 模块id假设为3
__webpack_modules__[3] = (module, exports, __webpack_require__) => {
  // 导出一个对象,包含CSS内容及模块信息
  exports = module.exports = __webpack_require__(/* ! css-loader */).default;
  // 核心:CSS内容以数组形式存储,包含模块id、CSS字符串、媒体查询(空表示无)
  exports.push([    module.id,     "body {\n  background: #fff;\n  color: #333;\n}\n.title {\n  font-size: 20px;\n}",     ""  ]);
};

小图片经过url-loader进行处理(base64)

js 复制代码
// 模块id假设为4,图片被转为了base64的格式
__webpack_modules__[4] = (module, exports) => {
  // 导出base64编码的图片字符串
  module.exports = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAA...";
};

大图片经过file-loader进行处理(文件路径)

javascript 复制代码
// 模块id假设为5
__webpack_modules__[5] = (module, exports) => {
  // 导出图片的输出路径(由file-loader生成,含hash防止缓存)
  module.exports = "./dist/images/banner.8a3b2d.png";
};

可以看到上述的非JavaScript的内容都会转换为JavaScript代码:

  1. 统一了导入导出的逻辑,用一致的方式处理所有的资源。
  2. 同时转换为JavaScript代码也为后续的AST分析依赖打下基础。

Webpack核心流程

Webpack主要流程就是从入口文件出发,分析模块(文件)之间的依赖,最终打包输出文件产物,整个流程可以分为三个步骤。

初始化阶段 :合并配置参数创建Compiler对象,并注册插件。

  1. 合并配置参数,包括Webpack内部默认参数,process.args参数 和 webpack.config.js文件,创建Compiler对象。
  2. 遍历配置中的plugins数组,注册插件,调用插件的apply方法,方法中会调用插件系统注册的钩子函数,当构建流程走到相应的阶段,会触发插件的回调代码。

Compliler :代表Webpack实例,是全局唯一,贯穿整个生命周期,Compliler的作用是保存全局参数,提供钩子机制,允许插件进行扩展。

从形态上看,插件通常是一个带有 apply 函数的类,如:

js 复制代码
class SomePlugin {
    apply(compiler) {

    }
}

初始化阶段 注册插件会调用插件对象的apply方法,并以参数的形式传递Compiler对象,前边我们说过Compiler对象提供了钩子机制,所可以通过Compiler对象注册Webpack的钩子回调。

Webpack的钩子机制可以类比于Vue的生命周期,在打包的过程中,会经历很多阶段,有些阶段会触发过回调函数。下边是例子:

js 复制代码
class SomePlugin {
  apply(compiler) {
    compiler.hooks.thisCompilation.tap("SomePlugin", (compilation) => {
      compilation.addModule(/* ... */);
    });
  }
}
  1. 在初始化注册插件阶段,会调用SomePlugin插件的apply方法。
  2. 插件通过compiler.hooks.thisCompilation.tap注册了一个回调函数,绑定在thisCompilation钩子上。
  3. 当Webpack构建流程到达thisCompilation(在当前编译流程(Compilation 实例)被创建后、但还未开始处理模块和依赖前触发)会触发回调函数,在回调函数中会调用compilation.addModule(/* ... */)

compilation 是「单次编译的执行者」,每次构建(包括 watch 模式下的重新构建)都会创建一个新的,通过compilation也能注册一些钩子的回调函数。

构建阶段 :将项目资源转换为module,并通过AST分析module的依赖关系,构建依赖图谱moduleGraph

  1. EntryPlugin 监听compiler.hook.make 钩子并开始调用 compilation.addEntry 添加入口文件,将入口文件转换为dependency依赖。
  2. 之后会调用handleModuleCreation,根据文件类型构建 module 子类,此时的module是一个"空壳",仅包含模块路径、类型等元信息
  3. 使用loader-runner根据不同类型的module(.css, .svg)按顺序执行 loader 链,对模块的原始内容 (磁盘文件内容)进行转换为JavaScript代码。然后会回填到module实例中。
  4. 使用acorn将JavaScript代码转换为AST,遍历AST,遇到importexport则会创建相应的dependency(一个及以上),然后加入到module的依赖数组中。
  5. 对于module新增的dependency依赖,会重新回到第二步继续处理。
  6. 所有的依赖解析完毕,构建阶段结束。

上述流程提到了dependencydependency代表的就是modulemodule之间的依赖关系。

上述过程中模块源码经历了 module => ast => dependences => module 的流转。

生成阶段 :根据用户配置或者默认的分包规则将module组合成chunk并构建chunkchunk关系图谱chunkGraph,然后以chunk粒度将源码转译为目标环境可以运行的产物并进行输出。
生成阶段 又可以分为三个阶段 ,分别是构建chunkGraph , 优化chunkGraph转换,注入运行时代码并输出文件

chunk是什么?

在 Webpack 构建流程中,三者的流转关系是:
module(分散的模块)→ chunk(模块的分组)→ asset(最终输出的文件)
chunk是一组相关模块module的组合,是对模块module进行逻辑分组,chunk的出现给对module的组合优化留下了空间。正因为模块被分组为 chunk,Webpack 才能在 "组" 的维度上实施优化(如提取公共 chunk 避免重复、对异步 chunk 实现按需加载等)

构建/优化chunkGraph
chunkGraph是chunk的依赖图谱,这个阶段就是创建chunk,然后建立chunkchunk之间的关系,chunk的默认分包规则是:

  • Entry Chunk:一个entry入口文件能够触达到的所有module会组合成一个chunk
  • Async Chunk: 异步加载的module及能够触达到的module会组合成一个chunk。(优化阶段)
  • Runtime Chunk:配置了entry.runtime,就会为入口文件单独打包一个chunk,如有两个入口文件,并且配置了不同的runtime值,那么就会生成两个Runtime Chunk,如果配置相同则会生成一个Runtime Chunk。(优化阶段)

Async Chunk的例子:import('./async-a.js')

chunkchunk之间也存在依赖关系,如Entry ChunkRuntime Chunk之间就是父子关系,将这些依赖关系记录下来并存到ChunkGraph中。
该流程大致可以总结为 :初始阶段 Webpack 会基于入口配置创建「初始 entry chunk」(包含入口模块及同步依赖),而异步 chunk 和 runtime chunk 是在优化阶段通过「代码分割」从初始 chunk 或依赖图中拆分 / 提取出来的,本质上是对模块资源的重新分配。

优化阶段会触发optimizeModules/optimizeChunks钩子,由插件(SplitChunksPlugin)进一步修剪,优化chunk结构,如果用户没有注册相关的插件,则上一步构建的chunkGraph就是最终的结构了。

转换,注入运行时代码并输出文件

该阶段大概可以分为三个步骤。

  1. 遍历module对象,对每个module源码进行转换。

    • 在构建阶段,module对象的依赖列表dependencies已经收集了很多的依赖dependency,每个dependency都对应着一个template.apply方法。调用每个dependency对象的template.apply方法,可能会产生三种副作用。
      • 直接修改modulesource,相当于直接修改源码。
      • 会将修改结果记录到initFragments数组中,initFragments数组储存的都是需要插入到源码中的代码片段,遍历完dependency数组后会将initFragments数组中的代码片段插入到源码中。
      • 将运行时依赖记录到runtimeRequirements数组中。
  2. chunk维度收集运行时依赖

    • 在构建阶段分析AST的时候,解析到模块化语句,会创建了对应的运行时dependency依赖。
    • 在上一个步骤对module的处理中,会将Runtime Dependency转换为__webpack_require__ 等枚举值,runtimeRequirements数组中存储的就是枚举值。
    • 会遍历chunk中的所有moduleruntimeRequirements,并把运行时依赖集中起来,并把该数据记录到chunkGraph中。
  3. 合并最终产物

    • 合并 CMD 代码;
    • 合并 runtime 模块代码;
    • 遍历 chunkModules 变量,合并除 entry 外其它模块代码;
    • 合并 entry 模块代码。 后续调用 compilation.emitAsset,最终将产物内容输出为指定路径的文件中。

流程总结

从流程来看Webpack可以分为三个步骤:初始化 -> 编译 -> 生成

从资源流转来看可以分为:源码 -> dependency -> module -> chunk -> asset -> 文件

相关推荐
用户47949283569155 小时前
面试官:你知道deepseek的ai生成代码预览是用什么做的吗?
前端·javascript·面试
六月的可乐5 小时前
AI助理前端UI组件-悬浮球组件
前端·人工智能
鹏多多5 小时前
vue3监听属性watch和watchEffect的详解
前端·javascript·vue.js
ruanCat5 小时前
使用 cloudflare worker 实现域名重定向
前端
华仔啊5 小时前
关于移动端100vh的坑和终极解决方案,看这一篇就够了!
前端·css
Hashan5 小时前
Webpack 核心双引擎:Loader 与 Plugin 详解
前端·webpack
前端端5 小时前
claude code 学习记录
前端
一位搞嵌入式的 genius5 小时前
ES6 核心特性详解:从变量声明到函数参数优化
前端·笔记·学习
JarvanMo5 小时前
Flutter:纯函数与不可变模型
前端