Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
zeroone001 opened this issue May 9, 2019 · 20 comments

Comments

@zeroone001
Copy link

zeroone001 commented May 9, 2019

参考链接

@baobaoblue
Copy link

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

@BruceYuj
Copy link

BruceYuj commented May 9, 2019

给大家讲个背景,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

@yygmind yygmind changed the title 第 70 题: 介绍下 webpack 热更新原理,是如何做到在不刷新浏览器的前提下更新页面的 第 70 题: 介绍下 webpack 热更新原理,是如何做到在不刷新浏览器的前提下更新页面 May 10, 2019
@KuoenI
Copy link

KuoenI commented May 11, 2019

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

@james9527
Copy link

james9527 commented Jun 25, 2019

简单来说就是: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 去局部更新页面的代码。因此这种方式可以不用刷新浏览器。

@PatrickLh
Copy link

PatrickLh commented Jul 20, 2019

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

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

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

@w3cmark
Copy link

w3cmark commented Jul 20, 2019

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

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

@SoftwareEngineerPalace
Copy link

参考链接

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

@Shiryan
Copy link

Shiryan commented Sep 20, 2019

看得有点蒙

@APPLLEJN
Copy link

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

@tonyyls
Copy link

tonyyls commented Nov 15, 2019

关于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() 完成热更新;

欢迎拍砖!

@Jmingzi
Copy link

Jmingzi commented Nov 22, 2019

在 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();
         })
       }

@whosesmile
Copy link

whosesmile commented Mar 10, 2020

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

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

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

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

@cwyan605
Copy link

cwyan605 commented Apr 9, 2020

官方解答

@EvalGitHub
Copy link

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

@fakeguru
Copy link

fakeguru commented Jul 18, 2020

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

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

@shuch
Copy link

shuch commented Jul 28, 2020

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

@lujiajian666
Copy link

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

@Rabbitzzc
Copy link

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

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

@qzruncode
Copy link

网上的这些关于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源码的结果,以后有时间有机会写文章详述

@Frank-MJ
Copy link

来着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 热更新通过实现模块的替换更新,从而避免了页面的刷新,提高了开发效率和用户体验。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests