深入理解 Webpack5:从打包到热更新原理

为什么需要构建工具?

一、解决 "高级语法与浏览器兼容性" 的矛盾

前端技术迭代快(如 ES6+ 的 async/await、箭头函数,CSS 的 grid 布局),但旧浏览器(如 IE11、Safari 10)不支持这些新特性。构建工具通过转译(Transpile)和前缀添加(Prefixing)解决兼容问题:

  1. JavaScript 转译

发者会使用 ES6+ 及以上语法(如类 class)、TypeScript(类型增强的 JS)、JSX(React 组件语法)等提高开发效率,但这些语法在低版本浏览器或部分现代浏览器中无法直接运行。构建工具(配合 Babel、tsc 等)会将这些高级语法转译为浏览器可识别的 ES5 语法。例如:

css 复制代码
// 开发时写的 ES6 语法
const sum = (a, b) => a + b;
// 构建工具转译后(兼容旧浏览器)
var sum = function(a, b) { return a + b; };
  1. CSS 属性添加浏览器前缀

开发者常用 Sass/Less/Stylus 等预处理器(支持变量、嵌套、混入等功能)编写 CSS,或使用 CSS Modules 解决样式冲突,但浏览器只认识原生 CSS。构建工具(配合 sass-loaderpostcss 等)会将预处理器语法编译为原生 CSS,同时通过 autoprefixer自动添加浏览器前缀(如 -webkit--moz-),适配不同浏览器的 CSS 特性支持。例如:

css 复制代码
// 开发时写的 Sass
$color: red;
.box {
  color: $color;
  &:hover { color: blue; }
}
// 构建后生成的 CSS(带前缀)
.box { color: red; }
.box:hover { color: blue; }
@supports (-webkit-appearance: none) { .box { /* 适配webkit内核 */ } }

二、处理 "模块化开发与浏览器加载限制" 的矛盾

前端项目早已从 "单文件" 发展为 "多模块组合",但浏览器对模块化的原生支持有限,需要构建工具进行依赖管理与打包:

  1. 模块化语法统一与解析前端模块化规范众多(ES Modules import/export、CommonJS require/module.exports、AMD 等),不同库可能使用不同规范,浏览器原生无法统一处理。构建工具会解析所有模块的依赖关系,并将不同规范的模块转换为统一格式(通常是 ES Modules 或打包后的单文件),确保代码能正确运行。

  2. 依赖合并与路径处理一个复杂项目可能依赖数百个模块(如业务组件、工具函数、第三方库),若直接让浏览器加载这些模块,会导致:

  • 大量网络请求(浏览器对同一域名的并发请求数有限,通常 6-8 个),严重拖慢加载速度;
  • 模块路径混乱(如相对路径、别名路径 @/components 等,浏览器无法直接识别)。

构建工具会将多个关联模块合并为少数几个 "chunk"(打包产物),并自动处理路径解析(如将 @/components 映射到实际目录),减少请求数并解决路径问题。

三、优化 "开发效率与生产性能" 的矛盾

开发时,我们追求的是高效的调试和快速的代码变更反馈(开发体验)。而生产环境追求的是极致的加载性能和用户体验。构建工具通过区分开发模式(development) 和生产模式(production),用一套配置完美解决两种截然不同的需求。

  1. 开发模式(Development) - 追求速度与可调试性
  • Source Map:将打包压缩后的代码映射回原始源代码。当你在浏览器调试时,看到的仍然是清晰可读的原始文件结构,而不是一团混乱的打包后代码,使得调试变得非常简单。
  • 热更新(HMR):只替换修改了的模块,保持应用当前状态(如表单输入、滚动位置),无需整页刷新,实现秒级更新。
  • 更快的构建速度:会跳过代码压缩、Tree Shaking 等耗时的优化步骤,保证开发时重新打包的速度。
  1. 生产模式(Production) - 追求体积与性能
  • 代码压缩(Minification):使用 Terser 压缩 JavaScript,cssnano 压缩 CSS,移除所有注释、空格、缩短变量名,能显著减小文件体积(通常减少 60%-70%)。
  • 代码分割(Code Splitting):将代码拆分成多个按需加载的块(chunk)。结合动态导入(import()),可以实现懒加载,让用户只加载当前页面或路由所需的代码,极大加速首屏加载时间。
  • Tree Shaking:像摇树一样,通过静态分析抖落未被使用的代码(dead code),并将其从最终打包文件中移除。例如,你只引用了 Lodash 中的 debounce 函数,打包后就只会包含 debounce 及其依赖,而不是整个 Lodash 库。
  • 资源优化与压缩:自动压缩图片、将小图片转为 Base64 内联、为现代浏览器提供更高效的图片格式(如 WebP/AVIF)。

有哪些构建工具?

构建工具 开发语言 Github Star 发布时间 作者
Vite TypeScript 75k 2020年4月 ViteJS
Webpack JavaScript 65.5k 2012年3月 Facebook
Parcel JavaScript 43.9k 2017年12月 Filament Group
esbuild Go 39.3k 2020年 Evan Wallace
Gulp JavaScript 33.1k 2013年 Eric Schoffstall
swc Rust 32.7k 2017年 Vercel
Turbopack Rust 28.5k 2022年10月 Vercel
Nx TypeScript 26.8k - Nrwl
Rollup JavaScript 26k 2015年 Rich Harris
Snowpack JavaScript 19.4k 2020年5月 Pika
Rolldown Rust 11.9k 2024年 VueJS
Rspack Rust 11.9k 2023年3月 Bytedance
WMR JavaScript 4.9k - Preact

Webpack 工作原理

在众多前端构建工具中,Webpack 由于其高度的灵活性和强大的生态系统,依然是最广泛使用的打包工具之一。与 Vite、Parcel 等工具相比,Webpack 提供了更为细致的定制能力,能够满足复杂项目的需求,如代码分割、资源优化和热更新等功能。掌握 Webpack 不仅有助于提升开发效率,更能优化应用性能,因此本文将深入介绍 Webpack 的工作原理。

一、打包过程

Webpack 的打包流程可以分为以下关键步骤:

  1. 读取配置文件,加载构建参数

Webpack CLI 启动后,会首先加载开发者提供的配置文件webpack.config.js,包括:

  • mode、entry、output 等基本配置
  • module.rules 中的 loader 配置
  • plugins 列表
  • optimization 配置

Webpack 采用 webpack-cli 来解析命令行参数,并最终调用:

ini 复制代码
const webpack = require("webpack");
webpack(options)

此时 Webpack 会完成配置合并(Webpack 内部使用 webpack-merge 的策略),将默认配置与用户配置拼接成最终 options 对象。

这一步的作用是确定 Webpack 后续构建过程所依赖的全部元信息。

  1. 创建 Compiler 对象,初始化编译总体调度器

Webpack 的主控制器是 Compiler 类,Compiler 是 Webpack 从开始到结束贯穿始终的对象,它控制着构建生命周期的每一个阶段,并将生命周期开放给插件系统(基于 Tapable)。

Compiler 的职责包括:

  • 管理整个构建生命周期
  • 负责调用插件、触发钩子
  • 统一调度 Compilation、模块工厂、Chunk 构建等核心内容

简化后的核心结构如下:

scala 复制代码
class Compiler extends Tapable {
  constructor(options) {
    super();
    this.options = options;
    this.hooks = {
      initialize: new SyncHook(),
      run: new AsyncSeriesHook(["compiler"]),
      compile: new SyncHook(["params"]),
      make: new AsyncParallelHook(["compilation"]),
      emit: new AsyncSeriesHook(["compilation"]),
      done: new SyncHook(["stats"]),
      // ...
    };
  }

  run(callback) {
    this.hooks.run.callAsync(this, err => {
      this.compile(onCompiled);
    });
  }

  compile(callback) {
    const compilation = this.newCompilation();
    this.hooks.make.callAsync(compilation, err => {
      compilation.finish(() => {
        compilation.seal(callback);
      });
    });
  }
}

compiler.run() 是 Webpack 单次构建流程的启动函数。它做三件事:

  • 调度构建生命周期钩子,如 beforeRun / run
  • 调用 compiler.compile() 执行真正的编译逻辑
  • 调用 emit / afterEmit / done 等钩子产出结果

Compiler 控制整体流程,而 Compilation 则控制:

  • 模块构建(构建每一个 JS/CSS/图片等模块)
  • 模块依赖分析
  • Chunk 构建
  • 代码优化(Tree Shaking、SplitChunks)
  • 生成最终资源(assets)

Compilation 是构建的"工作单位",负责所有实际的打包工作。

  1. 从入口文件开始,递归查找和编译模块

EntryPlugin 会根据 entry 配置向 Compilation 注册入口模块。Webpack 随后会从入口开始递归解析依赖,构建整个依赖图。当遇到非 JS 模块时,通过 Loader 进行处理(如将 TS/LESS/图片等转为可用模块)。

Webpack 在构建模块时,会根据 module.rules 匹配对应的 loader,从右向左依次对模块内容进行转换。

Loader 的作用包括但不限于:

  • Babel:将 ES6+ 转译为 ES5
  • css-loader:处理 CSS 文件内容
  • url-loader / file-loader:处理图片资源
  • vue-loader / ts-loader:处理 SFC 或 TS

最终,所有文件都会被转换成 JavaScript 字符串供后续 AST 解析。

  1. 解析每个模块的依赖关系,构建模块图 ModuleGraph

webpack 把 loader 产出的 JS 代码解析成 AST,识别出所有依赖节点(import / require / import() / loader 插入的依赖等),并把这些依赖投影到 ModuleGraph 上,形成可供后续优化和分块的有向依赖图。

ini 复制代码
// NormalModule.build() 内部流程(精简)
runLoaders(resource, loaders, (err, result) => {
  const source = result.result[0];             // loader 输出 -> JS 字符串 / Source
  const parser = compilation.getParser("javascript");
  const ast = parser.parse(source);            // acorn -> AST
  const dependencies = parser.collectDependencies(ast); // 解析产生 Dependency 实例
  for (const dep of dependencies) {
    const resolved = resolver.resolve(dep.request);
    const depModule = normalModuleFactory.create(resolved);
    moduleGraph.connect(module, dep, depModule); // 建图:模块 -> 依赖 -> 目标模块
  }
});
  • 解析器(JavascriptParser)是通过 Tapable hook 逐节点触发、生成 实例(以便插件/loader 能介入解析过程)。
  • NormalModuleFactory / Resolver 负责把语法层面的请求(request)解析到具体路径并创建 Module 实例,Compilation 将这些新模块加入构建队列直至递归完成。
  1. 优化模块

    在模块级别上减少最终运行时代码体积并提升运行时性能的策略,包括剔除未使用代码(Tree Shaking)、合并模块以缩短调用链与减少函数包装开销(Scope Hoisting / Module Concatenation)、以及其他小粒度优化(常量折叠、dead code elimination 交给 Terser 等压缩器完成)。

  2. 根据模块图生成 Chunk,对生成的代码块进行进一步优化

Webpack 将完成构建的模块根据入口点和不同类型的依赖关系构建多个 Chunk,Chunk 是 Webpack 打包产物的基础单位,每个 Chunk 对应一个最终输出文件(或多个文件组合),同时承载 runtime、依赖关系、模块顺序和异步加载信息。ChunkGraph 会在这一阶段被创建并建立,它记录:

  • 当前 Chunk 包含哪些模块
  • 模块属于哪些 Chunk
  • Chunk 与 Chunk 之间的关系

生成的 Chunk 进行最终优化,包括渲染 Chunk 内部代码、处理异步加载和动态 import、应用代码分割策略提取公共模块、注入 runtime 逻辑、生成按需加载和预加载提示,以及通过 [contenthash] 和独立 runtime Chunk 实现长期缓存,从而使输出文件体积最小化、加载高效并支持浏览器缓存优化。

  1. 生成最终资源并写入输出目录

Webpack 构建完成后会将所有 Chunk 转换为可执行文件(assets),例如:

  • main.js
  • vendors.js
  • style.css
  • 图片 / 字体等静态资源

输出阶段触发 Compiler 的 emit 钩子,随后写入文件系统。

javascript 复制代码
emitAssets(compilation, callback) {
  const { entries, modules, chunks, assets } = compilation;
  const output = this.options.output;
    
  // 调用 Plugin emit 钩子
  this.hooks.emit.call();
  
  // 若 output.path 不存在,进行创建
  if (!fs.existsSync(output.path)) {
    fs.mkdirSync(output.path);
  }
    
  // 将 assets 中的内容写入文件系统中
  Object.keys(assets).forEach((fileName) => {
    const filePath = path.join(output.path, fileName);
    fs.writeFileSync(filePath, assets[fileName]);
  });
  
  // 结束之后触发钩子
  this.hooks.done.call();
  
  callback(null, {
    toJSON: () => {
      return {
        entries,
        modules,
        chunks,
        assets,
      };
    },
  });
}

二、热更新过程

热模块替换(HMR,Hot Module Replacement)是指当我们对代码修改并保存后,Webpack 将会对代码进行重新打包,并将新的模块发送到浏览器端,浏览器用新的模块替换掉旧的模块 ,以实现在不刷新浏览器的前提下更新页面。

整体流程:

  • 首先是 Webpack 监听到文件修改后,进行编译,打包出新的文件,得到新的 hash。
  • 其次是 webpack-dev-server 通过 websocket 通知浏览器,告诉浏览器新的 hash。
  • 浏览器收到通知后,请求新的文件,拿到新的文件后,更新页面。
文件监听

Webpack 的文件监听机制用于在开发模式下自动检测文件变化并触发增量编译。其核心由三个组件构成:

  • Watching(调度层)
  • Watchpack(抽象管理层)
  • DirectoryWatcher(底层监听实现层)

整体基于 发布--订阅模型(EventEmitter) 实现,整个过程是典型的 事件驱动的监听 → 通知 → 重新编译。Webpack 通过以下流程实现监听:

  1. compiler.watch() 创建 Watching 实例,负责整个监听调度。
  2. Watching 初始化 Watchpack,并让其负责监听所有文件和目录。
  3. Watchpack 内部使用 DirectoryWatcher 监听目录变动,同时监听单文件变化。
  4. 文件变动发生时,DirectoryWatcher 发出 change 事件。
  5. Watchpack 将事件上报给 Watching。
  6. Watching 调用 Webpack 的增量编译流程,生成新的构建文件。

Watching

Watching 是 Webpack 启动 watch 时最上层的控制者,通过:

  • 创建并启动 Watchpack;
  • 订阅 Watchpack 的 changeremove 事件;
  • 触发增量编译;
  • 将新的 hash 传给 dev-server,使浏览器执行 HMR 刷新。
javascript 复制代码
class Watching {
    constructor(){
        this.startTime = Date.now()
    }
    watch(files){
        let watcher = new Watchpack();
        let callback =  (changes, removals)=>{
             // 执行各种hooks
        }
        watcher.once("aggregated",callback);
        watcher.watch(files, directories, this.startTime);
        return watcher;
    }
}

Watchpack

Watchpack 是 Webpack 文件监听功能的核心抽象层,负责:

  • 统一管理所有被监听的文件和目录;

  • 为每个目录创建 DirectoryWatcher;

  • 决定具体监听方式(fs.watch、fs.watchFile、轮询);

  • 将底层监听事件汇总后统一向外发射:

    • change(文件变化)
    • remove(文件被删除)

所以它不直接监听,而是管理多个监听者。

javascript 复制代码
class Watchpack extends EventEmitter{
    constructor(){
        this.fileWatchers = new Map();
        this.directoryWatchers = new Map();
    }
    watch(files, directories, startTime){
        // 为每个文件添加一个或多个 DirectoryWatcher
        for(let file of files){
            let watcher = new DirectoryWatcher();
            this.fileWatchers.set(file, watcher);
            watcher.on("change", this._onTimeout);
            watcher.watch(file, startTime);
        }
        // for of directories
        // bala bala
        
        // 源码这里的方法调用,没太明白是个啥意思?
        // 后面还有一个watchEventSource.watch()
        watchEventSource.batch()
    }
    _onTimeout() {
        const changes = this.aggregatedChanges;
        const removals = this.aggregatedRemovals;
        this.emit("aggregated", changes, removals);
    }
}

DirectoryWatcher

DirectoryWatcher 是最底层、最核心的监听器,负责检测:

  • 文件内容修改(mtime 改变)
  • 新文件创建
  • 文件删除(readdir 不存在)
javascript 复制代码
class DirectoryWatcher extends EventEmitter {
    constructor(){
       this.watchers = new Map();
       this.files = new Map();
       
       this.watcher = watchEventSource.watch(file);
       this.watcher.on("change", this.onWatchEvent);
    }
    watch(file, startTime){
        let watchers = new Set();
        let watcher = new Watcher(startTime);
        watchers.add(watcher);
        this.watchers.set(file, watchers);     
    }
    onWatchEvent(eventType, filename){
        fs.lstat(filePath, (err, stats)=>{
            this.setFileTime(filePath, stats.mtime);
            // this.setDirectory(filePath, eventType);
        })
    }
    setFileTime(filePath, mtime){
        // 记录文件信息
        let safeTime = Date.now();
        this.files.set(filePath, {
            safeTime,
            timestamp: mtime
        })
        let watchers = this.watchers.get(filePath)
        for (const w of watchers) {
           if (w.checkStartTime(safeTime)) {
                // 文件更新后触发的事件源
                w.emit("change", mtime);
            }
        }
       
    }
}
  • 优先使用操作系统事件(fs.watch)

  • 系统不支持时退回轮询(Polling)

    • 通过周期性的 fs.stat + mtime 对比检测变化。
模块编译处理

当文件监听到变化后,Webpack 会触发一次增量编译,除了与打包类似的编译外,webpack 还会往 Compiler 注入 HotModuleReplacementPlugin 插件,插件内部做了三方面处理:

  • 语法转译:模块代码允许使用 module.hot.xxx 进行个性化处理,需要将这些语法处理为符合运行时能力的代码;
  • HMR Runtime Module:HMR模式下会将 HMR Runtime 模块进行注入操作。
  • 产物生成:HMR 模式下每次变更都会产生 manifest 和 update chunks 文件,编译器需要额外处理这些产生生成逻辑。

语法转译

在开发模式下,模块里可能会用到 HMR API,例如:

javascript 复制代码
if (module.hot) {
    module.hot.accept('./foo.js', () => {
        console.log('foo.js 更新了');
    });
}

HotModuleReplacementPlugin 提供了语法转义能力:

  • 对模块代码进行解析,找到 module.hot.xxx 相关调用;

    • 主要替换语法有 module.hot、module.hot.accept、module.hot.decline 语法
  • 将这些调用转换成运行时可以识别的代码(符合 HMR Runtime 约定);

  • 注入必要的回调钩子,保证模块在被替换时能够正确触发 accept、dispose 回调。

让开发者书写的 HMR API 能够在浏览器端的 HMR Runtime 中生效,实现模块级别的热替换。

HMR Runtime Module 注入

HMR 在运行时需要相应基础能力才能够运行,在 HotModuleReplacementPlugin 内部会往编译器注入 HotModuleReplacementRuntimeModule 保证应用能够正常提供运行时能力。

该 Runtime Module 实现了:

  • WebSocket 通信(接收 hash / ok 消息);
  • 补丁文件的加载(hot-update.js / hot-update.json);
  • 模块替换逻辑(dispose → apply → accept);
  • 状态管理(已替换的模块、更新队列等)。

作用:保证浏览器端可以正确识别 HMR 消息,拉取补丁并执行模块替换,而不刷新整个页面。

manifest 及 Chunk 生成

Compiler 每次编译之后都会得到每个模块、Chunk 的哈希值,Compiler 通过判断本次编译的产物的哈希和上次编译记录的Hash得出模块变更信息,HotModuleReplacementPlugin 根据模块变更信息生成两份数据:

  1. 更新描述manifest.json:以JSON形式数据
  • updateChunkIds:更新的 Chunk Id
  • removedChunkIds:移除的 Chunk Id
  • removedModuleIds:移除的 Module Id
  1. 模块 Chunk 产物[key].[newHash].hot-update.js:只包含更新模块代码
javascript 复制代码
// [key].[newHash].hot-update.js
self["webpackHotUpdate_myApp"]("main", {
  0: function(module, __webpack_exports__, __webpack_require__) {
    // 新模块代码
  },
  2: function(module, __webpack_exports__, __webpack_require__) {
    // 新模块代码
  }});

当 Webpack 完成增量编译后,会将新的构建产物与新的 hash 生 成在内存中,此时 Webpack-dev-server 会通过 WebSocket 将更新消息推送给浏览器。

Webpack-dev-server 的核心功能是作为 浏览器与编译器之间的中间层,让前端项目在开发阶段拥有实时更新能力。

在传统的 LiveReload 模式下,文件变更后浏览器会被强制刷新整个页面;而 HMR 在此基础上更进一步,只更新变化的模块而不刷新整页。这两种机制最终都依赖 Webpack-dev-server 提供的能力。

从整体设计来看,Webpack-dev-server 可以拆分为两个主要模块:

  • 应用通信模块(推送更新消息)

  • 静态资源服务模块(返回构建产物)

    • 使用 express 框架提供静态资源服务器功能
更新通知

服务器与浏览器之间通过 WebSocket 建立长连接,并会使用长轮询作为兜底方法。

  1. 监听 Webpack 编译完成后,主动触发 sendStats 方法
  2. 本地服务端的 WebSocket 主动发送 hash 命令和 ok 命令到本地浏览器 client 端
kotlin 复制代码
class Server {
 setupHooks() {
     // 初始化时注册done的监听事件,编译完成后,调用sendStats方法进行webSocket的命令发送
     this.compiler.hooks.done.tap(
         "webpack-dev-server",
         (stats) => {
             if (this.webSocketServer) {
                 this.sendStats(this.webSocketServer.clients, this.getStats(stats));
             }
             this.stats = stats;
         }
     );
 }

 sendStats(clients, stats, force) {
     // 更新当前的hash
     this.currentHash = stats.hash;
   
     // 发送给客户端当前的hash值
     this.sendMessage(clients, "hash", stats.hash);

     // 发送给客户端ok的指令
     this.sendMessage(clients, "ok");
 }
}
模块替换

当浏览器端收到 ok 消息后,Webpack 的 HMR Runtime 便会开始执行模块热替换流程。浏览器端调用 emitter将最新 hash 值发送给 HMR runtime。

ini 复制代码
import hotEmitter from "webpack/hot/emitter.js";

function reloadApp(_ref, status) {
  var hot = _ref.hot, liveReload = _ref.liveReload;
  ...
  var currentHash = status.currentHash, previousHash = status.previousHash;
  ...
  var allowToHot = search.indexOf("webpack-dev-server-hot=false") === -1;
  if (hot && allowToHot) {
    log.info("App hot update...");
    hotEmitter.emit("webpackHotUpdate", status.currentHash);
    ...
  }
  ...
}

HMR runtime 会更新 lastHash,并在必要时调用 check(),发起更新检查

javascript 复制代码
hotEmitter.on("webpackHotUpdate", function (currentHash) {
  lastHash = currentHash;
  if (!upToDate() && module.hot.status() === "idle") {
    log("info", "[HMR] Checking for updates on the server...");
    check();
  }
});
var check = function check() {
  module.hot
    .check(true)
    .then(function (updatedModules) {
      ...
    })
    .catch(function (err) {
      ...
    });
};

module.hot.check最终会触发hotCheck()方法

javascript 复制代码
function hotCheck(applyOnUpdate) {
  ...
  return setStatus("check")
  .then(__webpack_require__.hmrM)
  .then(function(update) {
      ...
      return setStatus("prepare").then(function() {
            return Promise.all(
              Object.keys(__webpack_require__.hmrC).reduce(function (
              			promises,
              			key
              		) {
                        // key = jsonp
              			__webpack_require__.hmrC[key](
              					update.c,
              					update.r,
              					update.m,
              					promises,
              					currentUpdateApplyHandlers,
              					updatedModules
              				);
              			return promises;
              		},
              	[])
          ).then(function() {
              return waitForBlockingPromises(function() {
                  ...
                  return internalApply(applyOnUpdate);
              });
          });
      });
  });
}
__webpack_require__.hmrC.jsonp = function(
  chunkIds,
  removedChunks,
  removedModules,
  promises,
  applyHandlers,
  updatedModulesList,
) {
    applyHandlers.push(applyHandler);
    ...
    chunkIds.forEach(function(chunkId) {
        if (
        __webpack_require__.o(installedChunks, chunkId) &&
        installedChunks[chunkId] !== undefined
        ) {
            promises.push(loadUpdateChunk(chunkId, updatedModulesList));
        }
    });
};

webpack_require .hmrM 中 fetch 到新的 manifest.json 文件,完成后,触发 webpack_require.hmrC.jsonp 方法执行

执行 webpack_require .hmrC.jsonp,传入 manifest.json 文件中返回的key,通过 jsonp 方式请求得到 [key].[newHash].hot-update.js,执行对应的 moudle 代码的缓存并且触发对应 promise 的resolve请求,从而顺利回调internalApply()方法

internalApply()方法是 HMR 的"内部调度器",连接了 检查更新 和 实际替换模块 两个阶段。根据更新的模块和依赖信息,决定哪些模块可以热替换、哪些模块需要 dispose,并最终调用 apply() 来应用新模块

ini 复制代码
function internalApply(options) {
 // 这里的currentUpdateApplyHandlers存储的是上面jsonp请求js文件所创建的callback
 var results = currentUpdateApplyHandlers.map(function (handler) {
     return handler(options);
 });
 currentUpdateApplyHandlers = undefined;

 results.forEach(function (result) {
   if (result.dispose) result.dispose();
 });

 var outdatedModules = [];
 results.forEach(function (result) {
     if (result.apply) {
         // 这里的result的是上面jsonp请求js文件所创建的callback所返回Object的apply方法
         var modules = result.apply(reportError);
         if (modules) {
             for (var i = 0; i < modules.length; i++) {
                 outdatedModules.push(modules[i]);
             }
         }
     }
 });

 return Promise.all([disposePromise, applyPromise]).then(function () {

     return setStatus("idle").then(function () {
         return outdatedModules;
     });
 });
}

替换的总体执行逻辑:

  1. 根据webpack配置拼接出当前 moduleId 的热更新策略,比如允许热更新,比如不允许热更新等等
  2. 根据热更新策略,拼接多个数据结构,为applay()方法代码服务
  1. internalApply()可以知道,最终会先执行result.dispose(),然后再执行result.apply()方法

拼接数据结构

  1. 根据getAffectedModuleEffects(moduleId)整理出该moduleId的热更新策略,是否需要热更新
  2. 根据多个对象拼凑出disposeapply方法所需要的数据结构
ini 复制代码
function applyHandler(options) {
   currentUpdateChunks = undefined;

   // at begin all updates modules are outdated
   // the "outdated" status can propagate to parents if they don't accept the children
   var outdatedDependencies = {}; // 使用module.hot.accept部署了依赖发生更新后的回调函数
   var outdatedModules = []; // 当前过期需要更新的modules
   var appliedUpdate = {}; // 准备更新的modules


   for (var moduleId in currentUpdate) {
       var newModuleFactory = currentUpdate[moduleId];

       // 获取之前的配置:该moduleId是否允许热更新
       var result = getAffectedModuleEffects(moduleId);

       var doApply = false;
       var doDispose = false;

       switch (result.type) {
           // ...
           case "accepted":
               if (options.onAccepted) options.onAccepted(result);
               doApply = true;
               break;
           //...
       }
       if (doApply) {
           appliedUpdate[moduleId] = newModuleFactory;
           //...代码省略... 拼凑出outdatedDependencies过期的依赖,为下面的module.hot.accept(moduleId, function() {})做准备
       }
       if (doDispose) {
           //...代码省略... 处理配置为dispose的情况
       }

   }
   currentUpdate = undefined;

   // 根据outdatedModules拼凑出需要_selfAccepted=true,即热更新是重新加载一次自己的module的数据到outdatedSelfAcceptedModules中
   var outdatedSelfAcceptedModules = [];
   for (var j = 0; j < outdatedModules.length; j++) {
       var outdatedModuleId = outdatedModules[j];
       // __webpack_require__.c = __webpack_module_cache__
       var module = __webpack_require__.c[outdatedModuleId];
       if (module && (module.hot._selfAccepted || module.hot._main) &&
           appliedUpdate[outdatedModuleId] !== warnUnexpectedRequire &&
           !module.hot._selfInvalidated
       ) {
           // _requireSelf: function () {
           //      currentParents = me.parents.slice();
           //      currentChildModule = _main ? undefined : moduleId;
           //         __webpack_require__(moduleId);
           // },
           outdatedSelfAcceptedModules.push({
               module: outdatedModuleId,
               require: module.hot._requireSelf, // 重新加载自己
               errorHandler: module.hot._selfAccepted
           });
       }
   }

   var moduleOutdatedDependencies;

   return {
       dispose: function() {...}
       apply: function(reportError) {...}
   };
}
ini 复制代码
function getAffectedModuleEffects(updateModuleId) {
   var outdatedModules = [updateModuleId];
   var outdatedDependencies = {};

   var queue = outdatedModules.map(function (id) {
       return {
           chain: [id],
           id: id
       };
   });
   while (queue.length > 0) {
       var queueItem = queue.pop();
       var moduleId = queueItem.id;
       var chain = queueItem.chain;
       var module = __webpack_require__.c[moduleId];
       if (!module || (module.hot._selfAccepted && !module.hot._selfInvalidated)) { continue; }

       // ************ 处理不热更新的情况 ************
       if (module.hot._selfDeclined) {
           return {
               type: "self-declined",
               chain: chain,
               moduleId: moduleId
           };
       }
       if (module.hot._main) {
           return {
               type: "unaccepted",
               chain: chain,
               moduleId: moduleId
           };
       }
       // ************ 处理不热更新的情况 ************

       for (var i = 0; i < module.parents.length; i++) {
           // module.parents=依赖这个模块的modules
           // 遍历所有依赖这个模块的 modules
           var parentId = module.parents[i];
           var parent = __webpack_require__.c[parentId];
           if (!parent) continue;
           if (parent.hot._declinedDependencies[moduleId]) {
               // 如果依赖这个模块的parentModule设置了不理会当前moduleId热更新的策略,则不处理该parentModule
               return {
                   type: "declined",
                   chain: chain.concat([parentId]),
                   moduleId: moduleId,
                   parentId: parentId
               };
           }
           // 如果已经包含在准备更新的队列中,则不重复添加
           if (outdatedModules.indexOf(parentId) !== -1) continue;
           if (parent.hot._acceptedDependencies[moduleId]) {
               if (!outdatedDependencies[parentId])
                   outdatedDependencies[parentId] = [];
               // TODO 这个parentModule设置了监听其依赖module的热更新
               addAllToSet(outdatedDependencies[parentId], [moduleId]);
               continue;
           }
           delete outdatedDependencies[parentId];
           outdatedModules.push(parentId); // 添加该parentModuleId到队列中,准备更新

           // 加入该parentModuleId到队列中,进行下一轮循环,把parentModule的相关parent也加入到更新中
           queue.push({
               chain: chain.concat([parentId]),
               id: parentId
           });
       }
   }

   return {
       type: "accepted",
       moduleId: updateModuleId,
       outdatedModules: outdatedModules,
       outdatedDependencies: outdatedDependencies
   };
}

function addAllToSet(a, b) {
   for (var i = 0; i < b.length; i++) {
       var item = b[i];
       if (a.indexOf(item) === -1) a.push(item);
   }
}

dispose 方法

dispose 方法是 HMR 模块替换流程中的"销毁旧模块"阶段,主要负责:

  • 清理缓存,将旧模块从模块系统中移除
  • 移除之前注册的回调函数
  • 移除目前 module 与其它 module 的绑定关系( parent 和 children),避免旧模块残留影响新模块
  • 为后续热更新的模块替换(apply 阶段)做准备
ini 复制代码
dispose: function () {
   currentUpdateRemovedChunks.forEach(function (chunkId) {
       delete installedChunks[chunkId];
   });
   currentUpdateRemovedChunks = undefined;

   var idx;
   var queue = outdatedModules.slice();
   while (queue.length > 0) {
       var moduleId = queue.pop();
       var module = __webpack_require__.c[moduleId];
       if (!module) continue;

       var data = {};

       // Call dispose handlers: 回调注册的disposeHandlers
       var disposeHandlers = module.hot._disposeHandlers;
       for (j = 0; j < disposeHandlers.length; j++) {
           disposeHandlers[j].call(null, data);
       }
       // __webpack_require__.hmrD = currentModuleData置为空
       __webpack_require__.hmrD[moduleId] = data;

       // disable module (this disables requires from this module)
       module.hot.active = false;

       // remove module from cache: 删除module的缓存数据
       delete __webpack_require__.c[moduleId];

       // when disposing there is no need to call dispose handler: 删除其它模块对该moduleId的accept回调
       delete outdatedDependencies[moduleId];

       // remove "parents" references from all children: 
       // 解除moduleId引用的其它模块跟moduleId的绑定关系,跟下面的解除关系是互相补充的
       // 一个是children,一个是parent
       for (j = 0; j < module.children.length; j++) {
           var child = __webpack_require__.c[module.children[j]];
           if (!child) continue;
           idx = child.parents.indexOf(moduleId);
           if (idx >= 0) {
               child.parents.splice(idx, 1);
           }
       }
   }

   // remove outdated dependency from module children: 
   // 解除引用该moduleId的模块跟moduleId的绑定关系,可以理解为moduleId.parent删除children,跟上面的解除关系是互相补充的
   // 一个是children,一个是parent
   var dependency;
   for (var outdatedModuleId in outdatedDependencies) {
       module = __webpack_require__.c[outdatedModuleId];
       if (module) {
           moduleOutdatedDependencies =
               outdatedDependencies[outdatedModuleId];
           for (j = 0; j < moduleOutdatedDependencies.length; j++) {
               dependency = moduleOutdatedDependencies[j];
               idx = module.children.indexOf(dependency);
               if (idx >= 0) module.children.splice(idx, 1);
           }
       }

   }
}

apply方法

apply()方法是HMR 模块替换流程中的"加载新模块"阶段,主要执行的逻辑是:

  • 更新全局的 window.webpack_require 对象,存储了所有路径+内容的对象
  • 执行 runtime 代码,比如 _webpack_require__.h = ()=> {"xxxxxhash值"}
  • 触发之前 hot.accept 部署了依赖变化时的回调 callBack
  • 重新加载标识 _selfAccepted 的 module,这种模块会重新 require 一次
ini 复制代码
apply: function (reportError) {
   // insert new code
   for (var updateModuleId in appliedUpdate) {
       // __webpack_require__.m = __webpack_modules__
       // 更新全局的window.__webpack_require__对象,存储了所有路径+内容的对象
       __webpack_require__.m[updateModuleId] = appliedUpdate[updateModuleId];
   }

   // run new runtime modules
   // 执行runtime代码,比如_webpack_require__.h = ()=> {"xxxxxhash值"}
   for (var i = 0; i < currentUpdateRuntime.length; i++) {
       currentUpdateRuntime[i](__webpack_require__);
   }

   // call accept handlers:触发之前hot.accept部署了依赖变化时的回调callBack
   for (var outdatedModuleId in outdatedDependencies) {
       var module = __webpack_require__.c[outdatedModuleId];
       if (module) {
           moduleOutdatedDependencies =
               outdatedDependencies[outdatedModuleId];
           var callbacks = [];
           var errorHandlers = [];
           var dependenciesForCallbacks = [];
           for (var j = 0; j < moduleOutdatedDependencies.length; j++) {
               var dependency = moduleOutdatedDependencies[j];
               var acceptCallback = module.hot._acceptedDependencies[dependency];
               var errorHandler = module.hot._acceptedErrorHandlers[dependency];
               if (acceptCallback) {
                   if (callbacks.indexOf(acceptCallback) !== -1) continue;
                   callbacks.push(acceptCallback);
                   errorHandlers.push(errorHandler);
                   dependenciesForCallbacks.push(dependency);
               }
           }
           for (var k = 0; k < callbacks.length; k++) {
               callbacks[k].call(null, moduleOutdatedDependencies);
           }
       }
   }

   // Load self accepted modules:重新加载标识_selfAccepted的module,这种模块会重新require一次
   for (var o = 0; o < outdatedSelfAcceptedModules.length; o++) {
       var item = outdatedSelfAcceptedModules[o];
       var moduleId = item.module;

       item.require(moduleId);
   }

   return outdatedModules;
  }

总结

前端构建工具让模块管理、资源打包和优化变得高效,提升了开发体验和应用性能。Webpack 的工作原理尤其值得学习:从模块解析、依赖图构建,到 Chunk 生成和热更新,它展示了现代前端构建的完整机制。理解这些原理不仅能帮助我们更好地使用工具,也能为优化前端项目打下坚实基础。

相关推荐
星月心城2 小时前
八股文-JavaScript(第一天)
开发语言·前端·javascript
T___T2 小时前
从入门到实践:React Hooks 之 useState 与 useEffect 核心解析
前端·react.js·面试
山有木兮木有枝_2 小时前
当你的leader问你0.1+0.2=?
前端
前端程序猿之路2 小时前
模型应用开发的基础工具与原理之Web 框架
前端·python·语言模型·学习方法·web·ai编程·改行学it
名字被你们想完了2 小时前
Flutter 实现一个容器内部元素可平移、缩放和旋转等功能(八)
前端·flutter
听风说图2 小时前
Figma画布协议揭秘:组件系统的设计哲学
前端
sure2822 小时前
在react native中实现短视频平台滑动视频播放组件
前端·react native
weibkreuz2 小时前
React开发者工具的下载及安装@4
前端·javascript·react
代码猎人2 小时前
link和@import有什么区别
前端