前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
  • Vue

    • 从源码解读Vue生命周期
    • 组件的本质
    • 有状态组件的设计
    • 设计 VNode
    • 辅助创建 VNode 的 h 函数
    • 自定义渲染器和异步渲染
    • 渲染器之挂载
    • 渲染器的核心 Diff 算法
    • 渲染器之patch
    • 图解 Vue 响应式原理
    • 图解 Vue 异步更新
    • 剖析 Vue 内部运行机制
    • vue响应式原理模拟
    • vue状态管理之vuex
    • 理解Vue的设计思想及实现Vue
    • diff算法深入
    • vue router vuex原理分析
    • Vue3初探响应式原理.
    • vue2源码分析
    • vue组件化实践
  • React

  • Webpack

  • Node

  • Javascript

  • 综合

完整面试题地址:
作者:程序员poetry
扫码关注作者公众号:「前端进阶之旅」 每天分享技术干货
前端进阶之旅公众号二维码

Webpack中的HMR热更新原理剖析|原理篇

文章学习导航
面试总览

30 秒速记

  • 核心判断:HMR 通过文件监听、增量编译、更新通知和运行时模块替换保留页面状态
  • 原理主线:围绕 「核心机制」、「执行链路」、「边界与实践」 建立输入、状态变化与输出之间的因果关系
  • 文章范围:解析了Webpack的HMR(热模块替换)机制,包括HMR的工作原理、配置方法、与webpack-dev-server的协作流程,以及在React等框架中的热更新实践,帮助前端开发者理解如何实现无需刷新页面的高效开发体验。
  • 边界与代价:依赖边界拒绝更新或副作用无法安全重放时必须回退刷新;开发期热替换不属于生产运行逻辑
  • 工程落地:排障应沿文件是否被监听、哈希是否变化、客户端是否收到更新、accept 边界是否成立逐段检查

HMR 通过监听文件变化、增量编译并把更新模块交给浏览器运行时替换,从而尽量避免整页刷新和状态丢失。 devServer 编译完成后会把新 hash 通知客户端,客户端再获取更新描述和模块补丁。运行时沿依赖关系判断模块是否被接受,随后清理旧缓存、写入新模块并重新执行。若更新未被接收或应用失败,通常会退化为刷新页面;旧版 JSONP 链路也不能直接套用到所有新版本。

这篇文章不要按 API 清单来背。先用上面的 Mind Map 建立全局结构,再通过交互 DEMO 观察正常路径和边界路径如何改变状态;阅读正文时重点核对每一步的输入、负责执行的参与者、产生的中间状态以及最终可观察结果。遇到版本敏感结论,要把“历史实现”“当前行为”和“工程兼容策略”分开说明;遇到性能或架构取舍,则用实际指标、失败现象和验证手段支撑判断。

版本校准: 旧文中的 webpack 4、JSONP 更新清单或 react-hot-loader 代码用于解释历史链路,不应直接复制到新项目。当前 webpack-dev-server 4+ 默认启用 HMR,严格 ESM 可使用 import.meta.webpackHot;生产环境不得携带 HMR runtime。以 webpack HMR 官方指南 和项目锁定版本为准。

Hot Module Replacement(以下简称 HMR)是 webpack 发展至今引入的最令人兴奋的特性之一 ,当你对代码进行修改并保存后,webpack 将对代码重新打包,并将新的模块发送到浏览器端,浏览器通过新的模块替换老的模块,这样在不刷新浏览器的前提下就能够对应用进行更新。

基本实现原理大致这样的,构建 bundle 的时候,加入一段 HMR runtime 的 js 和一段和服务沟通的 js 。文件修改会触发 webpack 重新构建,服务器通过向浏览器发送更新消息,浏览器通过 jsonp 拉取更新的模块文件,jsonp 回调触发模块热替换逻辑

# 热更新配置

使用webpack-dev-server,设置 hot 属性为 true.写模块时,按照以下写法:

if (module.hot) { //判断是否有热加载
    module.hot.accept('./hmrTest.js', function() { //热加载的模块路径
        console.log('Accepting the updated printMe module!'); //热加载的回调,即发生了模块更新时,执行什么 callback
        printMe();
    })
}
@前端进阶之旅: 代码已经复制到剪贴板
  • 缺点:更新逻辑得自己写。比如要使页面显示的内容生效,需要在回调中写入document.append(xxx)

react 的热加载,使用 react-hot-loader

import { hot } from'react-hot-loader';
    const Record = ()=>{
        ...
    }
    exportdefault hot(module)(Record);
或

    if (module.hot) {
        module.hot.accept('./App', function () {
            var NextApp = require('./App')
            ReactDOM.render(<NextApp />, rootEl)
        })
    }
@前端进阶之旅: 代码已经复制到剪贴板

# 实现过程

  • watch 编译过程、devServer 推送更新消息到浏览器
  • 浏览器接收到服务端消息做出响应
  • 对模块进行热更新或刷新页面

# watch 编译过程、devServer 推送更新消息到浏览器

  1. webpack-dev-server 里引用了 webpack-dev-middleware,相关的 watch 逻辑就是在里面实现的
//webpack-dev-server/lib/Server.js
setupDevMiddleware() {
    this.middleware = webpackDevMiddleware(
        this.compiler,
        Object.assign({}, this.options, { logLevel: this.log.options.level })
    );
}
// webpack-dev-middleware/index.js
if (!options.lazy) {
    context.watching = compiler.watch(options.watchOptions, (err) => {
    ...
    });
}
@前端进阶之旅: 代码已经复制到剪贴板

以上代码可以看出,webpack-dev-middleware 是通过调用 webpack 的 api 对文件系统 watch 的。watchOptions 如果没有配置的话,会取默认值。值的含义见: https://webpack.js.org/configuration/watch/

  1. 当文件发生变化时,重新编译输出 bundle.js。devServer 下,是没有文件会输出到 output.path 目录下的,这时 webpack 是把文件输出到了内存中。webpack 中使用的操作内存的库是 memory-fs,它是 NodeJS 原生 fs 模块内存版(in-memory)的完整功能实现,会将你请求的url映射到对应的内存区域当中,因此读写都比较快
// webpack-dev-middleware/lib/fs.js
fileSystem = fs;
} elseif (isMemoryFs) {
    fileSystem = compiler.outputFileSystem;
} else {
    fileSystem = new MemoryFileSystem();
    compiler.outputFileSystem = fileSystem;
}
@前端进阶之旅: 代码已经复制到剪贴板
  1. devServer 通知浏览器端文件发生改变,在启动 devServer 的时候,sockjs 在服务端和浏览器端建立了一个 webSocket 长连接,以便将 webpack 编译和打包的各个阶段状态告知浏览器,最关键的步骤还是 webpack-dev-server 调用 webpack api 监听 compile的 done 事件,当compile 完成后,webpack-dev-server通过 _sendStatus 方法将编译打包后的新模块 hash 值发送到浏览器端
// webpack-dev-server/lib/Server.js
const addHooks = (compiler) => {
    ...
    done.tap('webpack-dev-server', (stats) => {
        this._sendStats(this.sockets, this.getStats(stats));
        this._stats = stats;
    });
};
...
_sendStats(sockets, stats, force) {
    ...
    this.sockWrite(sockets, 'hash', stats.hash);
    if (stats.errors.length > 0) {
        this.sockWrite(sockets, 'errors', stats.errors);
    } elseif (stats.warnings.length > 0) {
        this.sockWrite(sockets, 'warnings', stats.warnings);
    } else {
        this.sockWrite(sockets, 'ok');
    }
}
@前端进阶之旅: 代码已经复制到剪贴板

# 浏览器接收到服务端消息做出响应

  1. 这里的主要逻辑位于 webpack-dev-server/client-src 中,webpack-dev-server 修改了webpack 配置中的 entry 属性,在里面添加了 webpack-dev-client 的代码,这样在最后的 bundle.js 文件中就会有接收 websocket 消息的代码了
//webpack-dev-server/lib/utils/addEntries.js
let hotEntry;
if (options.hotOnly) {
    hotEntry = require.resolve('webpack/hot/only-dev-server');
} elseif (options.hot) {
    hotEntry = require.resolve('webpack/hot/dev-server');
}
...
if (hotEntry && checkInject(options.injectHot, config, true)) {
    additionalEntries.push(hotEntry);
}
config.entry = prependEntry(config.entry || './src', additionalEntries);
@前端进阶之旅: 代码已经复制到剪贴板
  1. 以上代码可以看出,如果选择了热加载,输出的 bundle.js 会包含接收 websocket 消息的代码。而且 plugin 也会注入一个 HotModuleReplacementPlugin,构建过程中热加载相关的逻辑都在这个插件中。这个插件主要处理两部分逻辑:
  • 注入 HMR runtime 逻辑
  • 找到修改的模块,生成一个补丁 js 文件和更新描述 json 文件

先看一张图,看看 websocket 中的消息长什么样子:

可以看到,接收的消息只有 type 和 hash 两个内容。在 client 里面的逻辑,他们分别对应不同的处理逻辑:

// webpack-dev-server/client-src/default/index.js
hash(hash) {
    status.currentHash = hash;
},
...
ok() {
        sendMessage('Ok');
    if (options.useWarningOverlay || options.useErrorOverlay) {
        overlay.clear();
    }
    if (options.initial) {
        return (options.initial = false);
    } // eslint-disable-line no-return-assign
    reloadApp(options, status);
}
@前端进阶之旅: 代码已经复制到剪贴板
  1. 可以看出,当接收到 type 为 hash 消息后会将 hash 值暂存起来,当接收到 type 为 ok 的消息后对应用执行 reload 操作,而 hash 消息是在 ok 消息之前的。再看看 reload 里面的处理逻辑:

← Webpack4打包机制原理解析从源码窥探Webpack4.x原理 →

fe
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
  • Vue

    • 从源码解读Vue生命周期
    • 组件的本质
    • 有状态组件的设计
    • 设计 VNode
    • 辅助创建 VNode 的 h 函数
    • 自定义渲染器和异步渲染
    • 渲染器之挂载
    • 渲染器的核心 Diff 算法
    • 渲染器之patch
    • 图解 Vue 响应式原理
    • 图解 Vue 异步更新
    • 剖析 Vue 内部运行机制
    • vue响应式原理模拟
    • vue状态管理之vuex
    • 理解Vue的设计思想及实现Vue
    • diff算法深入
    • vue router vuex原理分析
    • Vue3初探响应式原理.
    • vue2源码分析
    • vue组件化实践
  • React

  • Webpack

  • Node

  • Javascript

  • 综合