跳到主要内容

认识

2024年04月07日
柏拉文
越努力,越幸运

一、认识


HMR 的全称叫做 Hot Module Replacement,即模块热替换或者模块热更新。它可以在开发阶段实现局部实时更新、快速重新加载模块和状态保存, 不用刷新整个页面。相比于传统的 Live Reload 所解决的问题:模块局部更新状态保存

Webpack 接受更新的策略: 接受自身更新接受依赖模块的更新接受多个子模块的更新

二、使用


Webpack 生态下,只需要经过简单的配置,即可启动 HMR 功能,大致分两步:

  1. 设置 devServer.hot 属性为 true

        // webpack.config.js
    module.exports = {
    // ...
    devServer: {
    // 必须设置 devServer.hot = true,启动 HMR 功能
    hot: true
    }
    };
  2. 之后,还需要在代码调用 module.hot.accept 接口,声明如何将模块安全地替换为最新代码,如

    import component from "./component";
    let demoComponent = component();

    document.body.appendChild(demoComponent);
    // HMR interface
    if (module.hot) {
    // Capture hot update
    module.hot.accept("./component", () => {
    const nextComponent = component();

    // Replace old content with the hot loaded one
    document.body.replaceChild(nextComponent, demoComponent);

    demoComponent = nextComponent;
    });
    }

三、工作


  1. 使用 Express 启动本地服务, 构造静态服务器: 通过 webpack.config.js 创建 webpack 实例, 将 compiler.outputFileSystem 修改为 memory-fs 的实例: 创建 compiler 对象, 并修改 compiler.outputFileSystem = memory-fs 实例, 将 Webpack 的产物打包到内存中, 因此读写都比较快。memory-fsNodeJS 原生 fs 模块内存版(in-memory)的完整功能实现; 使用 Express 启动本地服务, 处理响应来自客户端的资源请求: 从内存中读取 index.htmlbundle.jschunkId.hash.hot-update.jsonchunkId.hash.hot-update.js 文件内容返回给客户端。

  2. 浏览器加载页面后, 服务端和客户端建立 WebSocket 长连接: 启动一个 WebSocket 服务器, Webpack 重新编译时, 向客户端推送 hashok 两个事件

  3. Webpack 监听文件的变化, 每当文件发生变更时, Webpack 将会监控到此时文件变更事件,并找到其对应的 module, 增量构建发生变更的模块: 每次编译都会生成 hash 值。通过 HotModuleReplacementPlugin 插件, 对比编译后的chunkmodule,将更新后的moduleruntime形成新的HotUpdateChunk, 生成记录着已改动模块的 chunkId.hash.hot-update.json 文件和记录着已改动模块代码的 chunkId.hash.hot-update.js 文件

  4. 编译完成后, 触发 compiler.hooks.done 钩子, 在回调函数中通过 Websocket 发送 hash 事件和 ok 事件, 把变更模块通知到客户端

  5. 客户端通过 hash 事件接受本次编译之后的 hash 值, 如果前后 hash 值不一致, 则 ajax 请求 manifest 资源文件 chunkId.hash.hot-update.json 确认增量变更 modulechunk, 然后再通过 JSONP 请求 chunkId.hash.hot-update.js 获取变更 modulechunk 的代码: 之所以使用 JSONP 获取变更的代码而不用 ajax 或者 websocket 是因为 JSONP 获取的代码可以直接执行。

  6. 获取变更代码之后, 使用新的模块对旧模块进行热替换,并删除其缓存: 在 webpack 的运行时中, 通过 __webpack__modules__ 来维护所有的模块, 通过 chunk 的方式加载最新的 modules,找到 __webpack__modules__ 中对应的模块逐一替换,并删除其上下缓存。

  7. Webpack 运行时触发变更模块的 module.hot.accept 回调, 执行代码变更逻辑: 一旦某个模块没有注册对应的 module.hot.accept 函数后, HMR 运行时会执行兜底策略, 通常是刷新页面, 确保页面上运行的始终是最新的代码。

四、问题


4.1 HMR 与 Live Reload 有什么区别?

HMR: 无需刷新在内存环境中即可替换掉过旧模块, 相对于 Live Reload, 整体刷新页面方案, HMR 的优点在于可以做到模块局部更新, 可以保存应用的状态, 提高开发效率

Live Reload: 代码进行更新后, 在浏览器自动刷新以获取最新前端代码

4.2 Webpack HMR 与 Vite HMR 有什么区别?

webpack的热更新对比起来,两者都是建立socket联系,但是两者不同的是,前者是通过bundle.jshash来请求变更的模块,进行热替换。后者是根据自身维护HmrModule,通过文件类型以及服务端对文件的监听给客户端发送不同的message,让浏览器做出对应的行为操作。

4.3 vue-loader 如何实现 HMR ?

HMR 模式下,vue-loader 还会为每一个 Vue 文件注入一段处理模块替换的逻辑

"./src/a.vue":
/*!*******************!*\
!*** ./src/a.vue ***!
\*******************/
/***/
((module, __webpack_exports__, __webpack_require__) => {
// 模块代码
// ...
/* hot reload */
if (true) {
var api = __webpack_require__( /*! ../node_modules/vue-hot-reload-api/dist/index.js */ "../node_modules/vue-hot-reload-api/dist/index.js")
api.install(__webpack_require__( /*! vue */ "../node_modules/vue/dist/vue.runtime.esm.js"))
if (api.compatible) {
module.hot.accept()
if (!api.isRecorded('45c6ab58')) {
api.createRecord('45c6ab58', component.options)
} else {
api.reload('45c6ab58', component.options)
}
module.hot.accept( /*! ./a.vue?vue&type=template&id=45c6ab58& */ "./src/a.vue?vue&type=template&id=45c6ab58&", __WEBPACK_OUTDATED_DEPENDENCIES__ => {
/* harmony import */
_a_vue_vue_type_template_id_45c6ab58___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__( /*! ./a.vue?vue&type=template&id=45c6ab58& */ "./src/a.vue?vue&type=template&id=45c6ab58&");
(function () {
api.rerender('45c6ab58', {
render: _a_vue_vue_type_template_id_45c6ab58___WEBPACK_IMPORTED_MODULE_0__.render,
staticRenderFns: _a_vue_vue_type_template_id_45c6ab58___WEBPACK_IMPORTED_MODULE_0__.staticRenderFns
})
})(__WEBPACK_OUTDATED_DEPENDENCIES__);
})
}
}
// ...

/***/
})

这段被注入用于处理模块热替换的代码,主要步骤有:

  1. 首次执行时,调用 api.createRecord 记录组件配置,apivue-hot-reload-api 库暴露的接口

  2. 执行 module.hot.accept() 语句,监听当前模块变更事件,当模块发生变化时调用 api.reload

  3. 执行 module.hot.accept("xxx.vue?vue&type=template&xxxx", fn) ,监听 Vue 文件 template 代码的变更事件,当 template 模块发生变更时调用 api.rerender

可以看到,vue-loaderHMR 的支持,基本上围绕 vue-hot-reload-api 展开,当代码文件发生变化触发 module.hot.accept 回调时,会根据情况执行 vue-hot-reload-api 暴露的 reloadrerender 函数,两者最终都会触发组件实例的 $forceUpdate 函数强制执行重新渲染。

另外,为什么这里需要调用两次 module.hot.accept?这是因为 vue-loader 在做转译时,会将 SFC 不同板块拆解成多个 module,例如 template 对应生成 xxx.vue?vue&type=template; script 对应生成 xxx.vue?vue&type=script。因此,vue-loader 必须为这些不同的 module 分别调用 accept 接口,才能处理好不同代码块的变更事件

参考资料


彻底搞懂并实现webpack热更新原理