Daily-Interview-Question icon indicating copy to clipboard operation
Daily-Interview-Question copied to clipboard

第 70 题: 介绍下 webpack 热更新原理,是如何做到在不刷新浏览器的前提下更新页面

Open zeroone001 opened this issue 5 years ago • 20 comments

参考链接

zeroone001 avatar May 09 '19 00:05 zeroone001

1.当修改了一个或多个文件; 2.文件系统接收更改并通知webpack; 3.webpack重新编译构建一个或多个模块,并通知HMR服务器进行更新; 4.HMR Server 使用webSocket通知HMR runtime 需要更新,HMR运行时通过HTTP请求更新jsonp; 5.HMR运行时替换更新中的模块,如果确定这些模块无法更新,则触发整个页面刷新。

baobaoblue avatar May 09 '19 00:05 baobaoblue

给大家讲个背景,sean larkin(webpack core developer)提过一个事: 2014年时,redux作者Dan Abramov 在 stackoverflow中提过一个问题: What exactly the webpack HMR? 然后webpack founder Tobias K. 在问题下面详细回答了HMR的原理。 https://stackoverflow.com/questions/24581873/what-exactly-is-hot-module-replacement-in-webpack

BruceYuj avatar May 09 '19 08:05 BruceYuj

你们这些人我服了,这原连接地址,我真的不知道说什么好

KuoenI avatar May 11 '19 04:05 KuoenI

简单来说就是:hot-module-replacement-plugin 包给 webpack-dev-server 提供了热更新的能力,它们两者是结合使用的,单独写两个包也是出于功能的解耦来考虑的。 1)webpack-dev-server(WDS)的功能提供 bundle server的能力,就是生成的 bundle.js 文件可以通过 localhost://xxx 的方式去访问,另外 WDS 也提供 livereload(浏览器的自动刷新)。 2)hot-module-replacement-plugin 的作用是提供 HMR 的 runtime,并且将 runtime 注入到 bundle.js 代码里面去。一旦磁盘里面的文件修改,那么 HMR server 会将有修改的 js module 信息发送给 HMR runtime,然后 HMR runtime 去局部更新页面的代码。因此这种方式可以不用刷新浏览器。

james9527 avatar Jun 25 '19 02:06 james9527

当时略微看了一下HMR的代码Webpack4和实现,主要要点在于:

  1. 在页面使用socket和http-server建立链接
  2. module.hot.accpet来添加HMR的js模块
  3. 当对应模块发生变化,发送socket请求和client的chunk头进行比较
  4. 如果有变化,头部追加script脚本chunk

可以参见一个不太成熟的源码解析: https://juejin.im/post/5c86ec276fb9a04a10301f5b

PatrickLh avatar Jul 20 '19 12:07 PatrickLh

1.当修改了一个或多个文件; 2.文件系统接收更改并通知webpack; 3.webpack重新编译构建一个或多个模块,并通知HMR服务器进行更新; 4.HMR Server 使用webSocket通知HMR runtime 需要更新,HMR运行时通过HTTP请求更新jsonp; 5.HMR运行时替换更新中的模块,如果确定这些模块无法更新,则触发整个页面刷新。

为什么HMR用webSocket通知需要更新时不直接把更新后的模块信息发过去呢?还要多一步HMR runtime主动去请求

w3cmark avatar Jul 20 '19 16:07 w3cmark

参考链接

太长了 能用10句话内总结下吗

SoftwareEngineerPalace avatar Aug 27 '19 10:08 SoftwareEngineerPalace

看得有点蒙

Shiryan avatar Sep 20 '19 07:09 Shiryan

1.当修改了一个或多个文件; 2.文件系统接收更改并通知webpack; 3.webpack重新编译构建一个或多个模块,并通知HMR服务器进行更新; 4.HMR Server 使用webSocket通知HMR runtime 需要更新,HMR运行时通过HTTP请求更新jsonp; 5.HMR运行时替换更新中的模块,如果确定这些模块无法更新,则触发整个页面刷新。

为什么HMR用webSocket通知需要更新时不直接把更新后的模块信息发过去呢?还要多一步HMR runtime主动去请求

还记得 HMR 的工作原理图解 中的问题 3 吗?为什么更新模块的代码不直接在第三步通过 websocket 发送到浏览器端,而是通过 jsonp 来获取呢?我的理解是,功能块的解耦,各个模块各司其职,dev-server/client 只负责消息的传递而不负责新模块的获取,而这些工作应该有 HMR runtime 来完成,HMR runtime 才应该是获取新代码的地方。再就是因为不使用 webpack-dev-server 的前提,使用 webpack-hot-middleware 和 webpack 配合也可以完成模块热更新流程,在使用 webpack-hot-middleware 中有件有意思的事,它没有使用 websocket,而是使用的 EventSource。综上所述,HMR 的工作流中,不应该把新模块代码放在 websocket 消息中。

来源自: https://zhuanlan.zhihu.com/p/30669007

APPLLEJN avatar Oct 13 '19 04:10 APPLLEJN

关于webpack的热更新原理,面试官比较想听到的是工作流程和关键点,非“流水账”式的源码分析。我认为可以这样的介绍:

首先,介绍webpack-dev-server: webpack-dev-server 主要包含了三个部分: 1.webpack: 负责编译代码 2.webpack-dev-middleware: 主要负责构建内存文件系统,把webpack的 OutputFileSystem 替换成 InMemoryFileSystem。同时作为Express的中间件拦截请求,从内存文件系统中把结果拿出来。 3.express:负责搭建请求路由服务。

其次,介绍工作流程: 1.启动dev-server,webpack开始构建,在编译期间会向 entry 文件注入热更新代码; 2.Client 首次打开后,Server 和 Client 基于Socket建立通讯渠道; 3.修改文件,Server 端监听文件发送变动,webpack开始编译,直到编译完成会触发"Done"事件; 4.Server通过socket 发送消息告知 Client; 5.Client根据Server的消息(hash值和state状态),通过ajax请求获取 Server 的manifest描述文件; 6.Client对比当前 modules tree ,再次发请求到 Server 端获取新的JS模块; 7.Client获取到新的JS模块后,会更新 modules tree并替换掉现有的模块; 8.最后调用 module.hot.accept() 完成热更新;

欢迎拍砖!

tonyyls avatar Nov 15 '19 17:11 tonyyls

在 webpack 中,都是模块且有唯一标识。在 webpack 编译完成后,将修改的模块 hash 对应的模块重新执行。就达到了局部刷新的效果。

过程

  • webpack-dev-middleware 是用来处理文件打包到哪里,到内存读取速度更快。
  • devServer 在监听 compiler done 后,利用 socket 告诉 devServer/client 修改模块的 hash
  • HMR.runtime 利用 HTTP 请求 hash.hot-update.json 获取更新模块列表 hotDownloadManifest
    {"h":"11ba55af05df7c2d3d13","c":{"index-wrap":true}}
    
  • 再通过 HTTP (jsonp) 获取更新模块的 js
    index-wrap.7466b9e256c084c8463f.hot-update.js
    
    返回执行
    webpackHotUpdate("index-wrap", {
      // ....
    })
    
  • webpackHotUpdate 做了三件事
    • 找到过期的模块和依赖并从缓存中删除
      delete installedModules[moduleId];
      delete outdatedDependencies[moduleId];
      
    • 遍历所有的 module.children,重新 installedModules 所有的子模块
    • 最后将自身模块的内容做替换修改
      modules[moduleId] = appliedUpdate[moduleId]
      
  • 最后代码替换之后并没有重新执行,需要手动注册需要重新执行的模块方法
      if (module.hot) {
         module.hot.accept('./print.js', function() {
           console.log('Accepting the updated printMe module!');
           printMe();
         })
       }
    

Jmingzi avatar Nov 22 '19 12:11 Jmingzi

上面说的都太复杂了,一句话:

每个模块都有个名称,当文件内容改变时,通过建立好的socket通知浏览器;
然后页面端的webpack脚手架代码会重载这个模块文件。

有个前提是模块的名称不会变化,所以在开发期间的webpack配置的chunkId不能是hash,因为模块名称如果变化,webpack脚手架就找不到新的文件在浏览器中模块的位置,自然无法局部更新这个模块。 这个问题也许可以通过新旧文件名的map映射解决,但是同样的依赖这个模块的模块内容也变更了,因为引用链接变了,由此蔓延,就是说牵一发动全身,所有的模块内容都可能要更新,HMR也就没意义了。

但是在部署生产环境的的时候,为了防止浏览器缓存模块,一般是要做hash处理的。如果使用现成的什么create-react/vue之类的脚手架就不用关心这个,因为别人已经帮你处理好了,如果自己手动搭建要注意。

whosesmile avatar Mar 10 '20 14:03 whosesmile

官方解答

cwyan605 avatar Apr 09 '20 12:04 cwyan605

所以知道了之后,能做什么呢?装逼吗?记住几个步骤,然后背出来?为了知道而知道?还是说你遇到了问题才去研究,自己去看?

EvalGitHub avatar Jul 05 '20 01:07 EvalGitHub

上面有几个评论只是在介绍步骤,能再深入点吗?感觉就像是在介绍代码调用了哪些函数,都没有深入解析这些函数内部的实现原理。

我觉得这问题可以换一种问法:有 A、B 两个 js 模块,现要将当前使用的 A 模块替换成 B 模块,如何用最简单的代码实现热更新?

fakeguru avatar Jul 18 '20 02:07 fakeguru

  1. 修改代码,触发webpack打包
  2. webpack将新模块通过websocket推送到浏览器
  3. 浏览器使用新模块替换旧模块

shuch avatar Jul 28 '20 08:07 shuch

我现在在自己写一个比较简陋的demo。发现文件变动,建立websocket,浏览器更新对应模块都好说。但是最大的问题是更新了模块之后呢?如果只是重新__require__该模块没办法刷新页面,引入之后只是返回了一个对象,里面的函数不知道运行哪一个。不知道是不是我流程搞错了,希望有大佬能解答一下

lujiajian666 avatar Feb 15 '21 11:02 lujiajian666

掘金政采云有篇文章写的很好,整体流程大概如下:

WDS 启动本地服务 (new webpack --> 启动 server --> 启动 websocket{将 websocket 的代码注入到浏览器代码中}
--> webpack 开始监听文件变动{变动了就重新编译构建} --> HMR-Plugin 将热更新代码注入到 浏览器运行代码中,也就是 HRM runtime)
--> HRM runtime 删除过期的模块,替换为新的模块,然后开始执行相关代码

Rabbitzzc avatar Mar 06 '21 15:03 Rabbitzzc

网上的这些关于HMR的文章都是为了博眼球吸流量的,放下浮躁的心好好看看源码
webpack.HotModuleReplacementPlugin负责比对module的hash生成
  核心代码
  hotUpdateMainContentByFilename.set(filename, {							      
      removedChunkIds,
      removedModules,
      updatedChunkIds,
      assetInfo
    });

webpack-hot-middleware负责获取HotModuleReplacementPlugin对比出来的chunk信息丢给客户端
  服务端核心代码
    function onDone(statsResult) {
      if (closed) return;
      // Keep hold of latest stats so they can be propagated to new clients
      latestStats = statsResult; // 取到最新的chunk的stats信息
      publishStats('built', latestStats, eventStream, opts.log);
    }
    compiler.hooks.done.tap('webpack-hot-middleware', onDone); // webpack的插件钩子,一次更新编译后完成调用
    eventStream.publish({ // 通过EventSource长链接推送给前端,(这里还有很多逗比说HMR是websocket,sock.js实现的)
      name: name,
      action: action,
      time: stats.time, // stats 就是 onDone 取到的chunk信息
      hash: stats.hash,
      warnings: stats.warnings || [],
      errors: stats.errors || [],
      modules: buildModuleMap(stats.modules),
    });
  客户端核心代码
    var applyResult = module.hot.apply(applyOptions, applyCallback); // 接收到变更的chunk信息

在webpack包下的hrm目录下有个LazyCompilationPlugin.js文件
  核心代码
  Template.indent([
    "module.hot.accept();",
    `module.hot.accept(${JSON.stringify(
      chunkGraph.getModuleId(module)
    )}, function() { module.hot.invalidate(); });`,
    "module.hot.dispose(function(data) { delete data.resolveSelf; dispose(data); });",
    "if (module.hot.data && module.hot.data.resolveSelf) module.hot.data.resolveSelf(module.exports);" // 得到最新的chunk代码
  ]),

总结起来就是:webpack.HotModuleReplacementPlugin负责生成变更的chunk信息 -> webpack-hot-middleware负责接受到这些信息然后通过EventSource丢给客户端 -> 同时webpack-hot-middleware也负责接受chunk信息传递给module.hot链 -> webpack通过hot.accept传入文件名获取对应的moduleId然后在hot.data中取代码

以上是花费30分钟看webpack源码的结果,以后有时间有机会写文章详述

qzruncode avatar Apr 22 '21 15:04 qzruncode

来着chatGPT的回答: Webpack 热更新(Hot Module Replacement,简称 HMR)是一种替换更新模块而无需刷新整个页面的技术。它可以在不刷新浏览器的情况下更新页面,从而提高开发效率和用户体验。

实现 HMR 的原理主要有以下几个步骤:

  1. 监听文件变化:Webpack-dev-server 监听文件变化,当文件发生变化时,会触发重新编译和打包。

  2. 更新模块:Webpack-dev-server 会将编译后的模块发送给浏览器端,浏览器端通过 WebSocket 接收到新的模块代码。

  3. 替换更新模块:浏览器端通过 Webpack-dev-server 提供的 HMR Runtime API,替换掉原有的模块。

  4. 更新页面:当模块被替换后,浏览器会重新渲染页面,但只会更新被替换的模块部分,从而实现页面无刷新更新。

需要注意的是,HMR 需要在代码中进行特殊的标记,以便在编译时生成对应的 HMR Runtime 代码,使得浏览器端能够正确地接收和替换更新的模块。同时,由于 HMR 只替换更新模块,因此对于一些全局状态的修改,仍然需要手动刷新页面才能生效。

总之,Webpack 热更新通过实现模块的替换更新,从而避免了页面的刷新,提高了开发效率和用户体验。

Frank-MJ avatar Apr 12 '23 07:04 Frank-MJ