想象你开了一家餐厅。传统做法是中央厨房提前把几百道菜做好,装进一个巨大的保温盒,整盒配送到食客面前——你想换其中一道菜,整盒都得重新做、重新送。这就是 Webpack:把所有模块打包成一整份 bundle,任何改动都要重新打包。而 Vite 截然不同:它把餐厅改成了开放式点餐台,每道菜都是独立的一份,食客(浏览器)想吃什么,直接向厨房(开发服务器)单点,现做现上。改一道菜,厨师只需单独重做那一碟,再用对讲机通知食客“您的菜好了”,端上去即可。这“单独重做的一碟菜”,就是 ESM 带来的模块级热更新能力——HMR(Hot Module Replacement)。本文将带你走进这家“点菜餐厅”的后厨,看看它如何用原生 ESM、WebSocket 与 import.meta.hot,把热更新从秒级压缩到毫秒级。

岗位关联

吃透 Vite 的 HMR 原理,是驾驭现代前端工程化、并让 AI 编程工具(如 Cursor、Copilot)做到“即改即视”的关键能力。

目录
  1. 可视化演示
  2. ESM 原生加载
  3. 传统打包困境
  4. Vite 冷启动
  5. HMR 通信管道
  6. accept 热替换
  7. 边界失效处理
  8. 性能对比实践
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示Vite利用浏览器原生ESM按需加载模块、WebSocket驱动HMR热更新与esbuild依赖预构建的完整流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

模式1:ESM原生模块加载 浏览器直接通过import加载ES模块 Vite Dev Server按需编译单文件 browser → import /src/main.js → on-demand compile Vite免打包,启动速度与项目规模无关。

ESM 原生加载

开放式点餐台的“菜牌”上,每道菜都有独立编号,客人指哪道,厨房就单独做哪道。ES Modules(ESM)就是浏览器世界的“菜牌协议”:每个 .js 文件是一道独立的小菜,通过 import 标明它搭配了哪些辅料(依赖),浏览器看到 import,就直接向服务器下单选这个文件,不需要有人提前把几百道菜拼成一大盘再端过来。

传统打包工具之所以存在,是因为浏览器历史上不认 import,只能靠 <script> 逐个加载脚本、用全局变量通信。如今主流浏览器原生支持 ESM:<script type="module" src="/src/main.js"> 一进场,浏览器就会自动解析 main.js 里的 import 语句,像按图索骥一样逐一请求每个依赖文件。这就意味着,开发模式下完全可以不打包。

ESM 有两个关键特性让“免打包”成为可能:一是静态结构——import/export 必须写在顶层、路径在编译期确定,工具能提前看清整张依赖图;二是按需加载——浏览器只在执行到 import 时才发起请求。Vite 正是把这两点用到了极致。

菜牌就摆在眼前,菜的做法和搭配一目了然——这就是“静态依赖图”。Vite 不打包,却比谁都清楚项目里有哪些模块、谁依赖谁,因为 ESM 的声明本身就画好了这张图。

import{widget}  from  ’./widget.js’\displaystyle \text{import}\{ \text{widget} \} \; \text{from} \; \text{'./widget.js'}
这是一句标准的 ESM 导入语句:从相对路径指向的文件中,引入名为 widget 的导出。
  1. import 关键字声明要引入的模块导出名字,相当于点菜时告诉服务员“我要这道菜”。
    import  {widget}\text{import}\; \{ \text{widget} \}
  2. from 路径指定模块文件的相对地址,浏览器据此拼接 URL 发起 HTTP 请求。
    from  ’./widget.js’\text{from}\; \text{'./widget.js'}
  3. 整体效果执行到这里时,浏览器向开发服务器发出一次针对单个文件的请求,拿到文件内容即完成加载。
    浏览器→HTTP GETdev server 的单文件模块\text{浏览器} \xrightarrow{\text{HTTP GET}} \text{dev server 的单文件模块}

传统打包困境

老式“中央厨房”的做法,是把所有菜提前做好、装进一个巨大的保温盒整盒配送。客人想换其中一道菜,厨师必须回厨房把所有菜重新做一遍、重新装盒、重新配送。这就是 Webpack 的宿命——开发时每改一行代码,它都要重新编译、优化、生成新的 chunk 和 sourcemap,再通过 HMR runtime 推送补丁。

Webpack 会把整个项目的数千个模块打包成一个或几个 bundle。项目越大,每次热更新“重做整桌菜”的时间就越长:从几百毫秒到数秒不等。更麻烦的是,Webpack 需要维护自己的模块运行时(module runtime),bundle 里的模块通过 __webpack_require__ 连接,改动一个模块要重建影响到的 chunk,HMR 的“增量”始终建立在“全量打包”的模型之上。

中央厨房的困境在于“一改全改”。只要模型是“整桌套餐”,换菜的速度就永远被整桌菜的规模绑架——这就是 Webpack HMR 在大型项目上越来越力不从心的原因。

THMRwebpack=T重新打包⏟O(N)+T生成补丁+T客户端合并\displaystyle T_{\text{HMR}}^{\text{webpack}} = \underbrace{T_{\text{重新打包}}}_{O(N)} + T_{\text{生成补丁}} + T_{\text{客户端合并}}
热更新总耗时等于重新打包(与模块总数 N 成正比)、生成补丁、客户端合并三部分之和,前两项都躲不开。
  1. 重新打包 O(N)构建性能与模块总数 N 相关,改动越深、影响面越大,重建越慢。
    T重新打包=O(N)T_{\text{重新打包}} = O(N)
  2. 生成补丁打包完成后还需 diff 出新增或修改的 chunk 片段,再压缩传输。
    T生成补丁=O(变更 chunk 大小)T_{\text{生成补丁}} = O(\text{变更 chunk 大小})
  3. 客户端合并浏览器 runtime 接收补丁后,在模块缓存中替换旧模块并触发回调。
    T客户端合并=O(依赖该模块的组件数)T_{\text{客户端合并}} = O(\text{依赖该模块的组件数})

Vite 冷启动

开放式厨房不需要提前做好整桌菜,锅灶一直热着。Vite 启动时业务代码一行都不打包,只做两件事:启动一个静态资源服务器,以及用 esbuild 把第三方依赖预构建一遍。浏览器访问页面时,<script type="module"> 指向 main.js,浏览器顺着 import 链逐个请求源码文件。

Vite 只在请求到某个文件时才对该文件做即时转换(如 TS 转 JS、JSX 转 JS),转换结果按需返回,这就是“按需编译”。源码文件被修改后,浏览器下次请求时拿到的就是新内容——因为根本没有“打包产物”需要重新生成。

第三方依赖(node_modules)不能直接交给浏览器:它们大多是 CommonJS 格式,内部可能有成百上千个文件,浏览器逐个请求会串行卡死。Vite 启动时用 esbuild 将它们快速转成 ESM 并合并依赖,结果缓存在 node_modules/.vite 目录,二次启动几乎瞬时完成。

锅是热的,菜是现点的——客人刚坐下,厨房已备好酱料(预构建依赖),主菜按单现炒(按需转换),所以第一口菜上桌的时间快得惊人。

T启动Vite≈T预构建依赖+T开启静态服务≈O(依赖数)+O(1)\displaystyle T_{\text{启动}}^{\text{Vite}} \approx T_{\text{预构建依赖}} + T_{\text{开启静态服务}} \approx O(\text{依赖数}) + O(1)
Vite 启动耗时只与第三方依赖规模相关,与业务源码总量无关,所以项目再大也能秒开。
  1. 预构建依赖esbuild 使用 Go 编写,比传统 JS 打包器快几十倍,几百个依赖通常一两秒完成。
    T预构建=O(依赖包总数)T_{\text{预构建}} = O(\text{依赖包总数})
  2. 开启静态服务 O(1)服务器就绪不随业务代码规模增长,几百个模块的项目也能秒开。
    T服务=O(1)T_{\text{服务}} = O(1)
  3. 与源码量无关业务代码不参与启动流程,源码越多,冷启动时间基本不变。
    T启动∝̸N业务模块T_{\text{启动}} \not\propto N_{\text{业务模块}}

HMR 通信管道

点菜餐厅里需要一套对讲系统:谁来监听需求变化?如何精准通知到某一桌?Vite 的答案是“文件监听器 + WebSocket”。当你保存文件,chokidar(文件监听库)立刻感知变更,交给 Vite 服务端处理。

服务端读取文件、判断它属于哪个模块,回溯“哪些模块 import 了它”,计算出本次更新的最小边界,然后通过 WebSocket 向所有连接的浏览器客户端推送一条轻量消息:/src/widget.js 更新了,时间戳是多少。这条消息只包含路径与版本号,毫秒级送达。

浏览器端的 HMR client(注入页面的一个小脚本)收到消息后,使用动态 import 按 URL 重新拉取该模块的最新文件。因为 ESM 的请求天然按文件粒度,“拉新菜”只需一个 HTTP 请求,不需要任何服务端重新编译。

对讲机只负责喊“某桌某道菜要重做”,绝不会把整本菜谱重新背一遍。通信管道虽小,却是整个热替换链路的第一推动力。

server→WebSocketupdate: ./src/widget.jsbrowser→HTTP GET新版本文件\displaystyle \text{server} \xrightarrow[\text{WebSocket}]{\text{update: ./src/widget.js}} \text{browser} \xrightarrow{\text{HTTP GET}} \text{新版本文件}
服务端通过 WebSocket 推送更新通知,浏览器再通过 HTTP 拉取单个文件的新版本,两步完成一次更新传播。
  1. server → browser推送仅包含模块路径与版本号的轻量消息,延迟在毫秒级。
    server→WebSocketbrowser\text{server} \xrightarrow{\text{WebSocket}} \text{browser}
  2. browser → server浏览器发起的拉取只针对变更文件,其他模块完全不受影响。
    browser→HTTP GET./src/widget.js\text{browser} \xrightarrow{\text{HTTP GET}} \text{./src/widget.js}
  3. 关键点通知快、拉取轻,因为编译工作在源文件层面已经按需完成,不再有“整包重建”环节。

accept 热替换

服务员把新菜端到桌前,客人得点头“这碟可以换”,厨师才会动手——模块必须自己声明“我可以被热替换”。ESM 提供了 import.meta 元信息对象,在 Vite 开发环境下,import.meta.hot 就是模块与 HMR runtime 对话的窗口。

import.meta.hot.accept(path, callback) 表示:当指定模块更新时,请执行我的回调,而不要刷新页面。回调里通常拿到新模块,替换旧的渲染逻辑。比如一个纯函数工具模块,更新后只需重新导出即可;一个组件模块,则重新渲染组件子树。

更重要的是“状态保留”能力:热替换只替换模块本身,不销毁整个应用。组件里的输入框内容、滚动位置、全局 store 都原样保留——这正是 HMR 与整页刷新最本质的区别。

“这碟菜可以换”是一句承诺:客人承诺不掀桌,厨房承诺只动这一碟。accept 就是这句承诺的代码化表达。

import.meta.hot.accept(newMod⇒render(newMod))\displaystyle \text{import.meta.hot.accept}(\text{newMod} \Rightarrow \text{render}(\text{newMod}))
accept 接收一个回调:当模块更新时,用新模块重新渲染,而不刷新整个页面。
  1. import.meta.hot由 Vite 注入的模块热更新接口,仅在开发环境存在,生产构建中会被移除。
    import.meta.hot∈{dev server 环境}\text{import.meta.hot} \in \{ \text{dev server 环境} \}
  2. accept 回调收到更新后执行自定义逻辑,通常是重新渲染或替换数据。
    newMod⇒render(newMod)\text{newMod} \Rightarrow \text{render}(\text{newMod})
  3. 默认处理不写回调时框架插件也会自动应用更新,但复杂场景需要手动声明边界。

边界失效处理

有些菜不是独立的一道——比如许多菜共用的酱料。单独重做酱料,别的菜也得跟着变。模块没声明 accept,或其变更影响无法被安全隔离时,就要启用兜底策略。

当被修改的模块自身没有 accept,Vite 会沿着 import 关系向上“冒泡”:检查直接父模块是否 accept,父模块的父模块是否 accept……一旦找到声明 accept 的边界,就只替换边界以内的模块;如果一路冒泡到入口还没有人接住,就整页刷新(相当于重新上桌)。

另外几种情况也会触发降级:修改 vite.config.js 等配置文件需要重启 dev server;新增或删除文件导致依赖图结构性变化;模块破坏了 accept 语义。框架插件(如 @vitejs/plugin-vue)会为组件自动注入 accept,把边界管理从开发者手里接管过去,出错时还能优雅降级并提示错误位置。

再灵活的点菜餐厅也有“这桌菜没法单独换”的时候,这时最稳妥的做法是整桌重上——总比给客人上一半新一半旧、吃着不舒服要强。

更新范围={冒泡到最近的 accept 边界,边界存在整页 reload,边界不存在\displaystyle \text{更新范围} = \begin{cases} \text{冒泡到最近的 accept 边界}, & \text{边界存在} \\ \text{整页 reload}, & \text{边界不存在} \end{cases}
更新范围由是否存在 accept 边界决定:有边界则局部替换,无边界则降级为整页刷新。
  1. 冒泡沿 importer 依赖链向上搜索,类似服务员逐桌询问“谁接受换这道菜”。
    module→parent→⋯→entry\text{module} \to \text{parent} \to \cdots \to \text{entry}
  2. 边界存在替换边界内所有受影响模块,页面不重载,状态保留。
    replace(边界内模块集)\text{replace}(\text{边界内模块集})
  3. 边界不存在执行 window.location.reload(),全量刷新兜底。
    ∄  accept 边界⇒reload()\nexists \; \text{accept 边界} \Rightarrow \text{reload()}

性能对比实践

上菜速度对照表:Webpack 的热更新耗时随项目规模线性增长,中小项目几百毫秒,大型项目动辄数秒;Vite 的热更新则稳定在几十毫秒级别,因为它的耗时只与“变更的那个模块”有关,与项目总量几乎无关。

复杂度上,Webpack 是 O(N)(N 为模块总数),Vite 是 O(Δ)(Δ 为变更模块及其边界)。当 N 从几百涨到几万,Vite 的 HMR 依旧保持“蚊子腿”级别的开销,这也是大型 monorepo、微前端项目纷纷转向 Vite 的原因。

实践要点:Vite 的极速 HMR 只服务开发环境,生产构建依然交给 Rollup(保证兼容性与产物优化);依赖预构建的缓存会在依赖版本变化时自动失效。在 AI 编程工具链(如 Cursor、Copilot)里,AI 生成的代码逐行注入文件,全靠 HMR 秒级生效,开发者的“改-看-反馈”循环被压缩到几乎无感。

点菜餐厅的招牌不是“所有菜一起上”,而是“每道菜都新鲜、随时能换”。Vite 用 ESM 的天然粒度,把热更新从“重做整桌”变成了“单碟现炒”——这正是现代前端工程化与 AI 编程协作时代最需要的厨房。

TVite HMRTWebpack HMR≈O(Δ)O(N)⟶0(N→∞)\displaystyle \frac{T_{\text{Vite HMR}}}{T_{\text{Webpack HMR}}} \approx \frac{O(\Delta)}{O(N)} \longrightarrow 0 \quad (N \to \infty)
项目规模越大,Vite 在热更新上的相对优势越明显,两者耗时之比趋近于零。
  1. O(Δ)只处理变更模块及其直接依赖链,与总规模无关。
    O(Δ)=O(1)∼O(小常数)O(\Delta) = O(1) \sim O(\text{小常数})
  2. O(N)每次都要重建受影响 chunk 及依赖图,规模越大越慢。
    O(N)=O(模块总数)O(N) = O(\text{模块总数})
  3. 极限意义当模块数量趋近无穷,Vite 的热更新耗时趋近于 Webpack 的零头。

AI 应用场景

AI 编程助手的实时预览

阶段:AI 生成代码 → 自动写入文件 → Vite HMR 即时生效

解决的问题:传统模式下 AI 改一行代码,开发者要手动触发构建才能看到效果,反馈回路长达数秒。Vite 让 AI 的每次修改毫秒级呈现在浏览器,反馈闭环极短,开发者可以像“对话式调菜”一样指挥 AI 微调界面。

具体案例:用 Cursor 生成一个 React 组件,AI 连续修改组件的样式与逻辑,浏览器几乎同步更新,而页面上的输入状态不丢失——这正是 HMR 状态保留能力的价值。

数据大屏的高频迭代

阶段:可视化组件开发调试 → 图表配置修改 → 模块级热替换

解决的问题:数据大屏动辄几十个图表组件,传统打包每次调整配色、布局都要等整个仪表盘重建;Vite 只重载被改动的图表模块,迭代节奏大幅加快。

具体案例:某大数据团队的实时监控大屏,在 Vite 下把“改配置 → 看效果”从 8 秒压缩到 1 秒内,配合 AI 生成图表配置效率倍增。

设计稿转代码工具的内嵌预览

阶段:AI 从设计稿生成代码 → 写入临时模块 → HMR 热替换到预览画布

解决的问题:每次 AI 重新生成代码,预览环境都要快速响应;工具内置 Vite dev server,把生成的组件热替换到画布中,设计师可即时校对还原度。

具体案例:某 low-code 平台的“AI 生成组件”功能,借助 Vite HMR 实现了“生成即预览”,用户几乎感觉不到等待。

Python 演示

python52 行
# -*- coding: utf-8 -*-"""模拟 Vite HMR 核心流程:文件变更 -> WS 通知 -> accept 热替换/降级刷新"""import hashlib  class ModuleStore:    def __init__(self):        self.modules = {            "store.js": "export const count = 1",            "widget.js": "export const text = 'hello'",            "app.js": "export const root = 'app'",        }        self.accepts = {}        # 是否声明 accept        self.versions = {}       # 模块内容哈希        self.changed = []        # 本次更新被替换的模块        self.reload = False      # 是否降级整页刷新        for name, code in self.modules.items():            self.versions[name] = hashlib.md5(code.encode()).hexdigest()     def accept(self, name):        """对应 import.meta.hot.accept 声明"""        self.accepts[name] = True     def watch_change(self, name, new_code):        """文件监听器发现变更:哈希不同才处理"""        new_hash = hashlib.md5(new_code.encode()).hexdigest()        if new_hash == self.versions[name]:            return        self.modules[name] = new_code        self.versions[name] = new_hash        self.changed = []        self.reload = False        self.push_update(name)     def push_update(self, name):        """WebSocket 推送:检查模块是否可热替换"""        if self.accepts.get(name):            self.changed.append(name)            print(f"[HMR] 模块 {name} 已 accept,热替换新版本")            print(f"      新代码: {self.modules[name]}")        else:            self.reload = True            print(f"[reload] 模块 {name} 未声明 accept,整页刷新兜底")     def run(self):        self.accept("widget.js")        self.watch_change("widget.js", "export const text = 'hello vite'")        self.watch_change("store.js", "export const count = 2")  if __name__ == "__main__":    ModuleStore().run()
目的:用 Python 简化仿真 Vite HMR 的判断逻辑——哪些模块能热替换、哪些会降级为整页刷新。
  1. self.accepts = {}记录哪些模块声明了 accept,对应 import.meta.hot.accept,是热替换边界的核心依据。
  2. hashlib.md5(code.encode()).hexdigest()用内容哈希判断文件是否真的变了,避免无效通知。
  3. def accept(self, name)对应模块里的 accept 声明,意思是“这碟菜可以换”。
  4. def watch_change(self, name, new_code)对应 chokidar 监听回调,发现变更即进入更新流程。
  5. self.push_update(name)对应 WebSocket 推送环节,把更新意图送达“客户端”。
  6. if self.accepts.get(name)边界判断:声明过 accept 则局部替换,否则整页刷新兜底。

常见误区

误区既然 Vite 开发时不打包,生产环境是不是也直接上 ESM,让浏览器一个个请求文件就行?那构建速度肯定也飞快。

正解生产环境 Vite 默认交给 Rollup 打包。原因有二:一是现代浏览器对大量小文件的 HTTP 请求开销远高于一个压缩过的 bundle;二是 ESM 对老浏览器兼容性不足。开发快与生产快是两件事,就像点菜餐厅只负责堂食体验,外卖仍然需要统一打包配送。

为何常见:Vite 的“快”宣传深入人心,用户容易把 dev server 的免打包特性下意识延伸到生产构建。

误区我改完代码页面自动变了,这不就是浏览器自动刷新吗?两者没啥区别。

正解自动刷新(live reload)是整页 reload,会丢失组件状态、重新加载所有资源;HMR 是模块级替换:只重新拉取变更的模块并执行其 accept 回调,其它模块与页面状态原样保留。比如表单填了一半,HMR 后输入内容还在;自动刷新则全部清空。

为何常见:从用户视角两者都表现为“改动即时生效”,肉眼很难区分内部机制。

误区Vite HMR 快是因为 WebSocket 比 HTTP 快很多,推送消息几乎没有延迟,所以性能碾压 Webpack。

正解WebSocket 只是“传呼机”,本身不负责编译。Vite 快的根本原因是 ESM 免打包:变更一个模块,服务器只需对比模块图、推送一条更新路径,浏览器按 URL 拉取单个文件。Webpack 也有 WebSocket,但它要先完成“重打包整个依赖图”的编译工作,通知才能发出。

为何常见:两件事先后发生,容易被误判为因果关系;WebSocket 的实时性确实给人“快”的直觉。

误区想在 Vite 里获得 HMR,每个组件都得自己写 accept 逻辑,太麻烦。

正解使用 Vue/React 等框架时,官方插件(@vitejs/plugin-vue 等)会自动注入 HMR 包装代码,组件文件默认支持热替换并保留状态;只需要在少数自定义边界(如纯 JS 模块、全局状态)手动声明 accept。手写是“逃生舱”,不是日常必修。

为何常见:看到框架插件的编译产物里有大量 HMR 代码,误以为那都是开发者自己该写的。

误区Vite 不是不打包吗?为什么启动时还会跑 esbuild 预构建?是不是业务代码也没逃过打包。

正解预构建只针对 node_modules 里的第三方依赖:把 CommonJS 转成浏览器可识别的 ESM,并把分散的小模块合并扁平化,否则依赖内部的几百个 import 会让浏览器串行请求崩溃。业务源码仍然零打包、按需请求,两者互不干扰。

为何常见:启动日志里出现 "optimizing dependencies" 字样,很容易让人误以为是业务代码在做优化打包。

面试题

Q1. 在 Vite 开发环境下,浏览器如何加载项目源码模块?

单选题 · easy

  • A.依赖被打包成一个大文件
  • B.浏览器原生 ESM 逐个请求← 正确答案
  • C.服务端渲染后一次性输出
  • D.通过 CDN 缓存整站资源

答案:B。Vite 开发模式不打包,浏览器通过 <script type="module"> 入口,按 import 链逐个请求源码文件,就像客人按菜牌单点。A 是 Webpack 的做法;C、D 与 Vite 的按需加载模型无关。

Q2. 与 Webpack 相比,Vite 冷启动快的根本原因是什么?

单选题 · easy

  • A.省去了全量打包这个步骤← 正确答案
  • B.浏览器按需请求源文件
  • C.服务端提前压缩好资源
  • D.浏览器缓存命中率非常高

答案:A。根本原因是“免打包”:Vite 启动时业务代码零构建,只启动静态服务并预构建第三方依赖。B 是免打包带来的表现而非原因;C、D 并非 Vite 特有的关键设计。

Q3. 依赖预构建(esbuild)的主要目的是什么?

单选题 · mid

  • A.将 CJS 转为 ESM 并合并依赖← 正确答案
  • B.提前把业务代码编译打包好
  • C.减少 HTML 中的 script 标签
  • D.替代生产环境的 Rollup 打包

答案:A。第三方依赖多为 CommonJS 且内部文件众多,需预构建转成 ESM 并扁平化合并,否则浏览器会因大量 import 串行请求而卡死。B 混淆了预构建对象;C 与预构建无关;D 中预构建只服务开发环境,不替代生产打包。

Q4. HMR 与“整页自动刷新”最本质的区别是什么?

单选题 · mid

  • A.HMR 不丢失页面的运行状态← 正确答案
  • B.自动刷新的速度会更快一些
  • C.HMR 会重新执行全部代码
  • D.自动刷新会增加打包体积

答案:A。HMR 只替换变更模块并执行 accept 回调,应用的其他部分与状态保留——这是它区别于 reload 的本质。B 不成立;C 描述的是刷新行为;D 与体积无关。

Q5. 一次完整的 Vite HMR 更新包含哪些环节?

多选题 · mid

  • A.文件监听器捕获代码变更← 正确答案
  • B.全量重新构建整个项目
  • C.WebSocket 推送更新通知← 正确答案
  • D.浏览器拉取并替换新模块← 正确答案

答案:ACD。流程是:监听变更 → WebSocket 通知 → 浏览器拉取新模块 → accept 替换。B 是全量重打包,正是 Vite 刻意避免的环节,选它就把 Vite 理解成了 Webpack。

Q6. 某模块 B 被修改且自身未声明 accept,Vite 会如何优先处理?

单选题 · hard

  • A.仅热替换被修改的模块自身
  • B.沿依赖链向上寻找 accept 边界← 正确答案
  • C.将整个模块树全部重新请求
  • D.提示用户手动刷新浏览器

答案:B。Vite 会先检查模块自身是否 accept,没有则沿 importer 依赖链向上冒泡,寻找最近的 accept 边界;若直到入口都无人接受,才降级为整页刷新。A 仅当自身声明 accept 时才成立;C、D 都不是 Vite 的更新策略。

Q7. import.meta.hot.accept() 的作用是什么?

单选题 · mid

  • A.声明模块可以被安全热替换← 正确答案
  • B.关闭当前模块的缓存功能
  • C.强制刷新当前的整个页面
  • D.把模块打入生产环境产物

答案:A。accept 是模块对 HMR runtime 的承诺:“我接受热替换”,随后可传入回调执行自定义更新逻辑。B、C、D 都与 accept 的语义无关,它只存在于开发环境。

Q8. 以下哪些情况会导致 Vite HMR 降级为整页刷新?

多选题 · hard

  • A.被修改的模块没有接受者← 正确答案
  • B.修改了构建配置文件← 正确答案
  • C.新增一个未被引用的文件
  • D.修改了项目核心入口文件← 正确答案

答案:ABD。A 冒泡到入口无 accept 边界会整页刷新;B 配置文件变更需要重启 dev server,等价全量重载;D 入口文件无人 import 且通常无 accept,只能刷新。C 的新文件未进入依赖图,不影响任何现有模块,不会触发刷新。

Q9. Vite 生产环境构建默认使用什么完成打包?

单选题 · easy

  • A.浏览器原生加载器
  • B.Rollup 打包器← 正确答案
  • C.esbuild 编译器
  • D.Webpack 内核

答案:B。Vite 开发用 esbuild 预构建依赖、按需转换源码,但生产构建默认交给 Rollup,以获取更好的产物优化与兼容性。C 只参与开发期预构建;A、D 均非 Vite 的构建工具。

Q10. 关于依赖预构建,下列说法正确的是?

多选题 · hard

  • A.借助 esbuild 高效完成← 正确答案
  • B.只转换第三方依赖包← 正确答案
  • C.每次启动都会重新构建
  • D.结果缓存在缓存目录中← 正确答案

答案:ABD。预构建用 esbuild 完成,对象是 node_modules 中的第三方依赖,业务源码不参与;结果缓存在 node_modules/.vite,仅在依赖版本变化时失效重建,因此 C“每次重新构建”错误。