webpack4 热更新原理

473 阅读5分钟

背景

模块热替换(HMR - hot module replacement)功能是webpack4 提供在应用程序运行过程中,替换、添加或删除模块,而无需重新加载整个页面。主要是通过以下几种方式,来显著加快开发速度:

  • 保留在完全重新加载页面期间丢失的应用程序状态。
  • 只更新变更内容,以节省宝贵的开发时间。
  • 在源代码中对 CSS/JS 进行修改,会立刻在浏览器中进行更新,这几乎相当于在浏览器 devtools 直接更改样式。

使用热更新

使用 webpack 热更新非常简单,只需要在 webpack 配置文件中添加相关配置即可。具体来说,我们需要添加 webpack-dev-server 和 webpack-hot-middleware 两个插件,然后在 entry 配置中添加一个入口文件,最后将 hot: true 添加到 devServer 配置中即可,如下所示:

javascriptCopy code
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'development',
  entry: [
    'webpack-hot-middleware/client',
    './src/index.js'
  ],
  output: {
    filename: 'bundle.js',
    path: __dirname + '/dist'
  },
  devServer: {
    hot: true
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    }),
    new webpack.HotModuleReplacementPlugin()
  ]
};

其中,webpack-hot-middleware/client 是 webpack-hot-middleware 的客户端代码,它会与 webpack-dev-server 建立 WebSocket 连接,从而实现代码的热更新。

HMR的工作流程

HMR的工作流程可以分为三个阶段:

检测模块更新

Webpack的HMR runtime会通过一些方法来检测模块的更新,包括监听文件系统的变化、检测依赖关系以及热替换接口等。当一个模块发生变化时,HMR runtime会触发一个更新事件。

更新模块

当模块发生更新事件时,Webpack会重新编译该模块,并生成一个新的模块对象。在生成新的模块对象时,Webpack使用了一种称为“更新分块”的技术,将更新后的代码和之前的代码进行比较,找到差异部分,只更新差异部分的代码。这样做可以提高更新的效率,减少不必要的网络传输。

应用更新

在生成新的模块对象后,HMR runtime会将其注入到应用程序中,并执行相应的回调函数,以完成模块的更新。在注入新的模块时,HMR runtime会使用一些技术来确保更新的正确性和安全性,例如使用“代码版本”来防止旧的代码被错误地执行。

HMR 的工作原理

HMR 的工作原理可以分为以下几个步骤:

  • 构建过程中,Webpack 在编译时为每个模块生成唯一的模块 ID。HMR 使用这些 ID 来识别模块。
  • 当开发人员保存文件时,Webpack 会重新编译并构建更新过的模块,以及与之相关的模块。
  • 在编译过程中,Webpack 会生成一个补丁(patch),其中包含了新旧模块之间的差异。
  • 补丁会通过 Websocket 传输到客户端,HMR 运行时(runtime)接收到补丁后,会根据补丁内容来更新模块。
  • 更新模块时,HMR 会先检查模块是否接受更新。如果接受,HMR 会用新的代码替换旧的代码,并在必要时重新执行模块。如果不接受,HMR 会刷新整个页面。

如上图所示,右侧Server端使用webpack-dev-server去启动本地服务,内部实现主要使用了webpack、express、websocket。

服务端流程

  1. 启动webpack-dev-server服务器,创建webpack实例,源代码地址@webpack-dev-server/webpack-dev-server.js#L173
  2. 添加webpack的done事件回调,用于推送每次编译产生的hash 源代码地址@webpack-dev-server/Server.js#L122
constructor(compiler) {
    let sockets = []
    let lasthash
    compiler.hooks.done.tap('webpack-dev-server', (stats) => {
      lasthash = stats.hash
      // 每当新一个编译完成后都会向客户端发送消息
      sockets.forEach(socket => {
        socket.emit('hash', stats.hash) // 先向客户端发送最新的hash值
        socket.emit('ok') // 再向客户端发送一个ok
      })
    })
}

webpack编译后提供提供了一系列钩子函数,以供插件能访问到它的各个生命周期节点,并对其打包内容做修改。compiler.hooks.done则是插件能修改其内容的最后一个节点。

编译完成通过socket向客户端发送消息,推送每次编译产生的hash。另外如果是热更新的话,还会产出二个补丁文件,里面描述了从上一次结果到这一次结果都有哪些chunk和模块发生了变化。

  1. 添加webpack-dev-middleware中间件,读取文件放到内存中。源代码地址@webpack-dev-server/Server.js#L125
  2. 使用sockjs在浏览器端和服务端之间建立一个 websocket 长连接,源代码地址@webpack-dev-server/Server.js#L745

客户端流程

  1. webpack-dev-server/client端会监听到此hash消息,源代码地址@webpack-dev-server/index.js#L54
  2. 客户端收到ok的消息后会执行reloadApp方法进行更新,源代码地址index.js#L101
  3. 在webpack/hot/dev-server.js会监听webpackHotUpdate事件,对比上上一次编译改变了那些模块源代码地址 dev-server.js#L55
function hotCheck() {
  // 是请求 webpack-dev-middleware中间件 获取文件地址
  hotDownloadManifest().then(update => {
    let chunkIds = Object.keys(update.c)
    chunkIds.forEach(chunkId => {
      hotDownloadUpdateChunk(chunkId)
    })
  })
}
  1. 调用JsonpMainTemplate.runtime的hotDownloadUpdateChunk方法通过JSONP请求获取到最新的模块代码,源代码地址JsonpMainTemplate.runtime.js#L14
function hotDownloadUpdateChunk(chunkId) {
  let script = document.createElement('script')
  script.charset = 'utf-8'
  // /main.xxxx.hot-update.js
  script.src = '/' + chunkId + "." + hotCurrentHash + ".hot-update.js"
  document.head.appendChild(script)
}
  1. 最后调用hotApply方法进行热更新,将新的模块代码应用到当前模块中。

总结

Webpack HMR通过WebSocket或者基于长轮询的技术实现了无需刷新页面的动态代码更新。HMR runtime会监听文件系统中文件的变化,并在文件发生变化时,向Webpack发送HTTP请求获取最新的代码。Webpack会将最新的代码注入到当前运行的应用程序中,从而实现动态更新的效果。