Vite Dev Server 的运行时模型:浏览器如何成为构建的起点
注:
本专栏基于作者当前阶段的理解进行整理与表达。
如有疏漏或不准确之处,欢迎反馈与交流:Issues
在此表示感谢。
引言: 从“构建工具”到“运行时系统”
在上一篇文章《为什么Vite能这么快》里面,我们已经对Vite的定位已经有了一个十分清晰的认识,其核心能力在于在合适的阶段中选择合适的“执行模型”:
开发阶段:由浏览器主导模块系统,工具只做最小必要的代码转换 生产阶段:由构建工具接管全局视角,完成针对于对应环境的静态优化
这意味着在开发阶段,Vite 不再存在一个集中式、预先完成的构建阶段,取而代之的是一个持续运行的由浏览器驱动、以模块为单位运作的运行时供给系统。
本篇文章开始,我们将深入探究Vite在开发阶段的运行时模型(Vite Dev Server),明白Vite是如何在“不打包,不构建”的情况下,让整套运行时系统成为浏览器的起点的。
一、重新理解 Dev Server: 不再作为“托管者”
在传统构建工具体系中,构建行为与开发服务器(Dev Server)是明确分层的:
- 构建工具先完成模块解析、打包与产物生成
- Dev Server 只是负责把这些已经构建完成的结果提供给浏览器
因此在 Webpack 时代,Dev Server 的核心角色更接近于:
构建产物的托管者 + 热更新通信桥梁
在传统构建体系中,Dev Server 并不参与模块转换或构建决策,真正的“构建行为”发生在 bundler 内部。 而在 Vite 中,这种 “构建先完成,服务器只分发结果” 的关系发生了彻底改变。
Vite Dev Server 的真实定位
在开发阶段,Vite Dev Server 不再是构建结果的托管者,而是直接参与到模块的“生成过程”中。它更接近于一个:
面向浏览器 ESM 的按需模块翻译与供给系统
几个关键特征可以直接划清它与传统 Dev Server 的边界:
- ❌ 不生成 bundle
- ❌ 不提前构建用于打包的静态完整依赖图
- ❌ 不预先确定整个应用的执行顺序
- ✅ 只响应浏览器发起的模块请求
- ✅ 仅维护运行时所需的模块关系图(用于缓存与 HMR)
- ✅ 只在请求到达时,临时将源码转换为浏览器可执行的形态
也就是说,代码编译的行为并没有消失,但它不再以“构建阶段”的形式集中发生,而是被拆散为一次次由浏览器请求触发的即时模块转换。
换句话说:
Vite Dev Server 是一个持续参与模块生成的运行时系统,而不是一个在启动阶段完成工作的编译产线。
二、以 HTML 为起点的模块加载模型
与 Webpack 等传统构建工具不同,Vite 的 Dev Server 并不是从一个 JavaScript 文件开始组织应用,而是从 HTML 出发。这源自于两种完全不同的模块启动模型。
在浏览器的原生执行机制中,应用的启动流程永远始于 HTML:
- 浏览器首先请求并解析 HTML
- 在解析过程中遇到
<script>标签 - 如果是
<script type="module">,浏览器才会进入 ESM 模块加载流程
也就是说,浏览器从来不会“主动执行一个 JS 文件”。JS 模块的加载权,始终来自 HTML 的声明。
Webpack:以 JS 作为构建入口
在 Webpack Dev Server 模式下,我们习惯把 main.js 称为“入口文件”,但这个“入口”指的是:
构建系统中的依赖图起点
Webpack 在 Node 环境中从这个 JS 文件出发,递归解析依赖,构建完整模块图,并最终打包成 bundle。随后 Dev Server 返回一个 HTML,浏览器加载的其实是:
1
<script src="/bundle.js"></script>
此时浏览器面对的已经是一个构建完成的程序整体,而不是一组离散的模块。
Vite:HTML 是“运行时模块系统的起点”
Vite 不同于 Webpack,在开发阶段不存在“打包”行为,而是直接以 HTML 为起点,由浏览器驱动模块加载与转换。因此在浏览器请求到 HTML 后,看到的是:
1
<script type="module" src="/main.js"></script>
此时:
/main.js不是一个打包后的产物,它只是一个等待被请求的源码模块- Dev Server 会在请求到达的那一刻,临时对其进行转换后返回
接下来浏览器继续根据 import 语句递归请求依赖模块,而 Dev Server 则在每一次请求到达时即时完成对应的源码转换。
因此在 Vite 的开发模型中:
- JS 不再是“构建入口”
- JS 只是被浏览器逐步请求到的普通模块
- HTML 才是整个运行时模块图展开的真正起点
Vite Dev Server 在开发模式下返回的第一份关键资源,始终是 HTML。
值得注意的是,在 Vite 中,HTML 本身也会进入 transform 管线,因此它不仅是浏览器的起点,也是 Vite 运行时处理流程的第一站。
三、浏览器主导的模块执行流程
从浏览器的视角完整追踪一次模块的加载过程,我们可以得这样一条路径:
- 浏览器请求 HTML
- 解析
<script type="module"> - 发起对应模块的请求
- Dev Server 拦截请求
- 对模块源码进行即时转换
- 修正依赖路径,并完成必要的语法转换
- 返回浏览器可直接执行的 ESM 模块
- 浏览器继续递归请求依赖模块
在这条路径中有一个十分重要的特征:
模块的请求顺序与执行节奏,始终由浏览器的原生 ESM 机制主导。 Dev Server 只在请求到达时进行必要的路径修正与代码转换。
Vite Dev Server 并不主导这一流程的执行节奏,它只是被动响应浏览器发起的模块请求,确保其在“实际工程环境”中依然能够走得通。
从这个角度来看,Vite Dev Server 更像是一个运行时的“翻译官”或者“桥梁”:
当浏览器的原生模块系统遇到无法直接跨越的工程化问题时,Dev Server 在请求发生的那一刻临时补上一段可行的路径。
这些“问题”主要来自于浏览器与工程源码之间的能力断层,如:
- 裸模块路径(如
import { ref } from 'vue')在浏览器中无法直接解析 - TS、JSX、Vue SFC 等源码格式浏览器无法直接执行
- 通过 CommonJS 实现的依赖不能被 ESM 原生加载
因此,在每一次模块请求到达时,Vite Dev Server 需要根据模块类型执行一系列按需发生的转换工作,包括:
- 依赖路径修正(将裸模块映射为可请求地址)
- 语法层面的源码转换(如 TS / JSX → JS)
- 框架文件的编译拆分(如 Vue 单文件组件)
- 模块格式转换(如 CommonJS → ESM)
这些转换并不会提前整体完成,而是分散在浏览器驱动的模块加载过程中逐步发生。
也正因为如此,Vite 的开发阶段几乎不存在一个独立的“构建阶段”,只有一个伴随浏览器模块请求不断运行的即时转换系统。
唯一例外的是针对第三方依赖的“预构建”步骤。该步骤通常在首次启动或依赖发生变化时触发,用于提升后续请求效率,并不改变浏览器驱动模块图展开这一核心运行机制。
结语:从“构建驱动”到“浏览器驱动”的范式转变
到这一步,我们对 Vite Dev Server 的定位与作用边界就已经有十分清晰的认识了。当我们从整体视角重新审视 Vite Dev Server 的工作方式时,会发现开发阶段的前端工程结构已经发生了一次底层范式的转移:
浏览器负责 发现并驱动模块
Dev Server 负责 修正并翻译模块
构建工具不再是执行中心,而是退居为运行时体系的支持系统
1
2
3
4
5
Webpack Dev:
Node → 构建 → Bundle → Browser
Vite Dev:
Browser → Request → DevServer → Transform → Browser
在传统构建工具体系中,模块图的建立、依赖关系的确定以及代码的最终形态,都在浏览器启动之前就已经由构建器决定完成;浏览器只是被动加载一个已经准备好的程序整体。
而在 Vite 的开发模型下,这一顺序被彻底倒置:
- 模块从何处开始展开 —— 由 HTML 决定
- 依赖在什么时候被发现 —— 由浏览器解析
import决定 - 模块何时被转换 —— 由请求到达 Dev Server 的时刻决定
构建不再是一个发生在“运行之前”的阶段,而是被拆解进了运行过程本身,成为一个伴随模块加载持续发生的即时转换行为。
这使得开发期的前端应用更像是一个由浏览器驱动逐步展开的“模块网络”,而 Vite Dev Server 则是这个网络中的实时翻译层与通行保障系统。
也正因为这种运行时主导的模型成立,Vite 才得以摆脱传统构建阶段带来的启动成本,将“等待构建完成”转变为“随着访问逐步准备”。
在这个意义上,Vite Dev Server 不只是一个更快的开发服务器,它代表的是一种新的工程组织方式:
让浏览器回到模块系统的中心位置,而构建工具则退居为按需提供能力的运行时协作者。
🚀 Hello!看一下作者的其他开源项目!
一个面向事件流的 WebSocket 客户端框架,尝试把通信过程组织为一种可组合的“流”结构。
- 将不同通信方式(请求 / 推送)统合到同一模型之中
- 通过事件订阅与规则路由从数据流中获取数据
- 以流式任务的方式处理持续到达的数据
- 构建可拓展的中间件管线灵活调整通信逻辑
如果你正在做实时系统或复杂通信层,可能对你有所帮助。