如何对 Vue 3 组件实现局部热更新

解读

面试官抛出“局部热更新”这一关键词,核心想验证两点:

  1. 你是否理解 HMR(Hot Module Replacement) 在前端工程中的定位——不刷新浏览器、只替换变更模块,同时保留组件状态与 DOM 位置
  2. Grunt 主导的构建体系 里,如何低成本、可落地地接入 Vue3 官方 HMR 能力,而不是“切到 Vite/Webpack 就完事”。
    国内很多老项目仍用 Grunt,直接说“换构建工具”会被判为逃避问题;必须给出在 Grunt 生命周期内可运行的方案,并讲清与 Vue3 模板编译、ESM 运行时、WebSocket 推送三者的配合关系

知识点

  1. Vue3 的 HMR 客户端:@vue/runtime-core 暴露的 __VUE_HMR_RUNTIME__ API,只要运行时环境注入 import.meta.hotmodule.hot,就能触发 rerender / reload / dispose 三种行为。
  2. Grunt 生态没有原生 HMR,但可以通过 grunt-contrib-watch + grunt-contrib-connect + WebSocket 手写桥接层 模拟“文件变更 → 浏览器接受 diff → Vue3 执行 HMR”。
  3. 浏览器端需要一段 <5KB 的 inline 脚本,用来:
    • 建立 WebSocket 连接;
    • 收到变更信号后,对 *.vue 文件执行 fetch + compile + eval
    • 调用 __VUE_HMR_RUNTIME__.createRecord()reload() 完成局部替换。
  4. 编译环节:用 @vue/compiler-sfc 把单文件组件拆成 renderFn + style + hmrId在内存中完成,不落盘,避免 Grunt 的 IO 瓶颈。
  5. 状态保持:借助 Vue3 的 keep-alivescript setup 顶层 const state = reactive({...}) 写法,只要组件类型签名不变,状态即可复用。
  6. 国内落地注意:
    • 内网常禁用 8000+ 随机端口,WebSocket 端口必须写死 35729 并在 nginx 侧代理;
    • 安全审计要求不允许 eval,可把编译结果生成 Blob URL,通过 import(/* @vite-ignore */ blobUrl) 动态加载,绕过 CSP。

答案

“在 Grunt 体系里给 Vue3 做局部热更新,我分四步落地:
第一步,在 Gruntfile 中新增一个 watch 子任务,只监听 src/**/*.vue,一旦变更,触发自定义任务 vue-hmr
第二步,实现 vue-hmr 任务:用 @vue/compiler-sfc 把改动文件编译成 { render, styles, hmrId } 三元组,通过 WebSocket 推送到浏览器,不落地磁盘,保证毫秒级响应。
第三步,浏览器端注入一段 3KB 的 hmr-client.js

  • 连接 ws://localhost:35729/grunt-hmr
  • 收到推送后,用 new Function() 执行 render 代码,拿到最新 renderFn
  • 调用 window.__VUE_HMR_RUNTIME__.reload('${hmrId}', newRender),Vue3 内部会做 diff + patchDOM 节点原地替换,状态不丢
    第四步,在入口 HTML 里加一段条件注释
    <!--[if development]><script src="/@grunt/hmr-client"></script><![endif]-->
    保证生产构建不会多 1byte
    整个流程零外部依赖、不碰 Webpack/Vite,在国有银行、运营商内网项目里已稳定跑两年,单次热更新 <300ms,完全满足开发效率要求。”

拓展思考

  1. 如果团队后续要迁移到 Vite,可以把这段 WebSocket 桥接逻辑抽象成独立 npm 包 grunt-plugin-vue3-hmr暴露同 vite 的 import.meta.hot 接口,将来切构建工具时业务代码零改动,实现“先享受 HMR,再渐进式替换构建”的国内主流演进路线。
  2. 当项目组件超过 1000 个,全量编译会拖慢 watch;可以在 vue-hmr 任务里加依赖图缓存,利用 Vue3 的 <script setup> 编译后的 hmrId 做哈希索引只重新编译受影响的单文件,把热更新时间从 300ms 压到 80ms 以内。
  3. 若需支持 Vue3 + TSX,把 @vue/compiler-sfc 换成 esbuild-service在内存里把 tsx 转译成 esm 再喂给 hmr-runtime,整体思路不变,但要把 sourcemap 也一起打过去,否则控制台行号对不上,会被测试同事投诉