ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Vite涉及到了哪些底层原理

Vite涉及到了哪些底层原理 一、面试题Vite 开发环境为什么不需要像 Webpack 一样先整体打包核心思路一句话Vite 开发环境把“整体打包”改成“浏览器原生 ECMAScript Modules Dev Server 按请求编译模块”从而避免启动阶段构建完整 Bundle。解决方案流程图传统 Webpack Dev 源码 ↓ 解析依赖 ↓ 构建完整 Module Graph ↓ Loader / Plugin ↓ 生成 Bundle ↓ 浏览器请求 BundleVite Dev 源码 ↓ 启动 Dev Server ↓ 浏览器请求 /src/main.js ↓ Vite 按请求处理 main.js ↓ 浏览器发现 import ↓ 再次请求依赖模块 ↓ Vite 按需转换依赖 ↓ 浏览器原生 ECMAScript Modules 执行底层实现原理浏览器本身支持import{createApp}from./app.js;因此 Vite 开发环境不需要先把整个应用变成一个 Bundle。例如scripttypemodulesrc/src/main.js/script浏览器请求GET /src/main.jsVite Dev Server 返回经过转换后的 ECMAScript Moduleimport{createApp}from/src/app.js;createApp();浏览器继续根据import自动请求/src/app.js然后继续解析其中的import。所以本质上浏览器负责模块加载Vite Dev Server 负责模块转换。Vite 开发环境仍然维护 Module Graph只是不需要像传统 Bundle 打包器一样在启动阶段把整个应用递归构建成最终 Bundle。Module Graph 主要用于模块依赖关系 ↓ 文件变化定位 ↓ HMR 更新传播 ↓ 缓存 ↓ 失效模块管理因此应该理解成Webpack Dev 启动时 源码 → 大规模模块图分析 → Bundle Vite Dev 启动时 启动 Server ↓ 请求来了才处理对应模块 ↓ 同时逐步建立 Module Graph二、面试题浏览器不认识裸模块导入Vite 是怎么解决的核心思路一句话浏览器只认识 URL 形式的模块路径Vite Dev Server 会把裸模块导入转换成浏览器可以请求的路径。例如源码import{createApp}fromvue;这里vue叫做裸模块导入Bare Module Specifier。浏览器不能直接把import{createApp}fromvue;理解成某个 URL。Vite 会将其处理成类似import{createApp}from/node_modules/.vite/deps/vue.js;浏览器于是可以GET /node_modules/.vite/deps/vue.js流程图源码 ↓ import { createApp } from vue ↓ Vite Module Transform ↓ 发现裸模块 vue ↓ 映射到依赖预构建产物 ↓ /node_modules/.vite/deps/vue.js ↓ 浏览器请求 ↓ 执行 ECMAScript Module三、面试题Vite 为什么还需要依赖预构建既然开发环境不打包为什么还要用 esbuild核心思路一句话Vite 不打包业务源码但会对第三方依赖进行预构建主要解决 CommonJS/Universal Module Definition 兼容、请求数量和依赖缓存问题。这是面试中非常容易被追问的点“Vite 不打包” ≠ “Vite 什么都不打包”。更准确地说业务源码 → 开发阶段按需转换不做整体 Bundle 第三方依赖 → 启动阶段进行依赖预构建 → 生成浏览器友好的 ECMAScript Modules流程图node_modules ↓ 发现依赖 ↓ esbuild Dependency Pre-Bundling ↓ CommonJS / Universal Module Definition ↓ 转换成 ECMAScript Modules ↓ 减少大量内部模块请求 ↓ 缓存到 node_modules/.vite ↓ 浏览器直接使用为什么不能直接让浏览器加载 CommonJS例如constvuerequire(vue);这是 CommonJS 语法。浏览器原生 ECMAScript Modules 并不直接支持require()module.exports exports.xxx所以需要转换。为什么还需要“合并”假设一个依赖内部有vue ├── module-a.js ├── module-b.js ├── module-c.js ├── module-d.js ├── ... └── module-500.js如果完全按照模块拆分加载浏览器 ├── 请求 module-a ├── 请求 module-b ├── 请求 module-c ├── ... └── 请求 module-500会产生大量模块请求。依赖预构建会把依赖处理成更加适合浏览器加载的产物node_modules ↓ esbuild ↓ 依赖预构建 ↓ node_modules/.vite/deps/ ↓ vue.js所以预构建主要解决三个问题① CommonJS / Universal Module Definition → ECMAScript Modules ② 减少第三方依赖产生的大量模块请求 ③ 预构建结果缓存提高后续启动速度但要注意“一个依赖最终只有一个请求”并不是绝对规则。预构建的目标是减少第三方依赖造成的请求数量和兼容问题具体产物结构取决于依赖本身以及 Vite 的预构建策略。四、面试题Vite HMR 为什么只修改一个文件就能快速更新它怎么知道应该更新哪些模块核心思路一句话Vite 通过文件系统监听发现变化再利用 Module Graph 找到受影响模块通过 HMR 边界沿依赖关系向上传播最终只更新能够被安全接受的模块。完整流程图修改 Button.vue ↓ 文件系统监听器发现变化 ↓ Vite Dev Server ↓ 定位 Module Graph 中对应模块 ↓ 失效旧模块缓存 ↓ 通过 WebSocket 通知浏览器 ↓ 浏览器 HMR Runtime ↓ 重新请求变化模块 ↓ 执行新模块 ↓ 寻找 HMR Accept Boundary ↓ 能接受 ┌────┴────┐ 是 否 ↓ ↓ 局部更新 沿 import 链向上冒泡 ↓ 找到边界 ┌───┴───┐ 是 否 ↓ ↓ 局部更新 页面刷新五、Vite Module Graph 到底解决什么问题这是这个问题的核心追问。Module Graph 可以理解成A.js ├── import B.js │ └── import C.js │ └── import D.js形成A ├── B │ └── C └── DVite 会维护模块之间的关系。因此修改C.jsVite 可以知道C ↑ B ↑ A然后寻找谁能够接受 C 的 HMR 更新六、面试题HMR 为什么有时候只更新组件有时候却整页刷新核心思路一句话HMR 能否局部更新取决于更新链上是否存在 HMR Accept Boundary没有可接受边界就只能回退到页面刷新。例如 Vue 单文件组件App.vue ↓ Button.vue修改Button.vueVue 插件会为组件注入相应的 HMR 运行时代码使其成为一个 HMR 边界。因此Button.vue 修改 ↓ HMR Runtime ↓ Button.vue 接受更新 ↓ 只更新 Button如果修改的是普通工具函数例如// utils.jsexportfunctionformatPrice(price){return¥${price};}App.vue ↓ usePrice.js ↓ utils.js如果utils.js本身没有接受 HMRutils.js ↓ 寻找父模块 ↓ usePrice.js ↓ 继续寻找父模块 ↓ App.vue ↓ 没有模块接受 ↓ 页面重新加载所以不要简单理解成“Vite 修改什么就只更新什么。”更准确的是Vite 只重新处理发生变化的模块但最终能否局部热更新取决于 HMR 边界和依赖关系。七、面试题为什么 Vite HMR 比传统整体重新打包更快核心思路一句话核心不是“Vite 不编译”而是“变化范围小”只处理变化模块并避免重新构建整个应用 Bundle。修改 Button.vue 传统整体 Bundle Button.vue ↓ 重新分析依赖 ↓ 重新执行大量构建流程 ↓ 重新生成 Bundle ↓ 浏览器重新加载 Vite Button.vue ↓ 定位 Module Graph ↓ 只重新转换 Button.vue ↓ WebSocket 通知 ↓ 浏览器重新请求 Button.vue ↓ HMR 更新主要矛盾变化范围次要矛盾esbuild 编译速度 缓存 文件监听效率 WebSocket 通信因此不要把“Vite 快 esbuild 快”作为完整答案。esbuild 很快是因素之一但 Vite 开发体验快的根本原因是开发模式架构改变了。八、面试题Vite 的插件为什么能够同时支持开发和生产核心思路一句话Vite 在插件系统上兼容 Rollup 的核心插件接口开发阶段由 Vite Dev Server 驱动这些钩子生产阶段则交给 Rollup 执行。架构图Vite Plugin │ ┌──────────┴──────────┐ ↓ ↓ Vite Dev Vite Build ↓ ↓ Dev Server Rollup ↓ ↓ 按请求处理模块 构建整个应用 ↓ ↓ resolveId/load/transform resolveId/load/transform所以一个插件可以exportdefaultfunctionmyPlugin(){return{name:my-plugin,resolveId(source){// 模块解析},load(id){// 加载模块},transform(code,id){// 转换模块}};}开发阶段浏览器请求 /src/a.js ↓ Vite ↓ 调用 transform ↓ 返回转换结果生产阶段Rollup 构建 ↓ 遍历模块 ↓ 调用 transform ↓ 生成最终 Bundle九、Vite 插件开发环境和生产环境最大的区别是什么核心思路插件接口可以复用但执行上下文不同开发环境是“请求驱动”生产环境是“构建驱动”。维度Vite DevVite Build驱动者Vite Dev ServerRollup工作方式按请求处理模块构建整个依赖图resolveId请求到模块时调用构建时调用load请求模块时调用构建模块时调用transform当前请求模块构建过程中涉及的模块configureServer有效不用于普通生产构建HMR 相关能力有无开发服务器 HMR最终结果单模块响应Bundle重要边界不要认为“所有 Rollup 插件在 Vite 中一定完全兼容。”插件如果依赖Rollup 特定构建阶段 Bundle 输出结构 文件系统写入 完整 Bundle 信息那么开发环境可能与生产环境行为不同。十、Vite 插件中configureServer是干什么的核心思路一句话configureServer是 Vite 开发服务器专属扩展点用于访问和扩展 Dev Server 能力。例如exportdefaultfunctionapiPlugin(){return{name:api-plugin,configureServer(server){server.middlewares.use(/api/hello,(req,res){res.setHeader(Content-Type,application/json);res.end(JSON.stringify({message:Hello Vite}));});}};}请求GET /api/hello就可以直接由 Vite Dev Server 返回数据。使用场景开发环境 Mock API 开发代理 自定义 Middleware WebSocket 开发调试能力 HMR 辅助功能边界configureServer是Dev Server 专属不能把它当成生产 Bundle 生命周期十一、完整示例实现一个同时支持 Vite Dev 和 Build 的 Markdown 插件这个例子可以把插件系统 resolveId load transform 开发环境 生产环境一次串起来。项目结构project/ ├── index.html ├── package.json ├── vite.config.js ├── src/ │ ├── main.js │ └── README.md └── plugins/ └── markdownPlugin.jsplugins/markdownPlugin.jsimportfsfromnode:fs;importpathfromnode:path;/** * 一个简单的 Vite Markdown 插件。 * * 功能 * 1. 浏览器 import README.md 时将 Markdown 转换成 JavaScript 模块 * 2. 开发环境和生产环境都可以使用 * 3. 通过 transform 钩子完成 Markdown → JavaScript 的转换 */exportdefaultfunctionmarkdownPlugin(){return{// 插件名称必须唯一方便 Vite / Rollup 识别插件。name:markdown-to-js,/** * resolveId * * 负责“模块 ID 解析”。 * * 这里不需要特殊解析 Markdown * 因为 README.md 本身就是一个真实文件 * Vite 默认已经能够解析它的路径。 * * 所以这里故意不实现 resolveId。 *//** * load * * 可以自己接管某个模块的加载过程。 * * 这里也不需要实现 * 因为 Markdown 本身可以由 transform 读取。 *//** * transform * * 这是本插件的核心。 * * Vite 开发环境 * 浏览器请求 README.md * ↓ * Vite 找到本插件 * ↓ * transform 被调用 * * Vite 生产环境 * Rollup 构建模块 * ↓ * transform 被调用 */transform(code,id){// 只处理 .md 文件其他文件直接交给后续插件。if(!id.endsWith(.md)){returnnull;}/** * 这里为了演示底层原理 * 不引入完整 Markdown 解析器。 * * 实际项目应该使用 * marked、markdown-it 等成熟解析器。 */consthtmlcode// 转义 HTML 字符避免直接生成非法 JavaScript 字符串。.replace(//g,amp;).replace(//g,lt;).replace(//g,gt;)// # 标题转换成 h1。.replace(/^# (.)$/gm,h1$1/h1)// ## 标题转换成 h2。.replace(/^## (.)$/gm,h2$1/h2)// 普通行转换成 p。.replace(/^(?!h[12])(.)$/gm,p$1/p);/** * 最终返回的不是 HTML * 而是一段 JavaScript 模块代码。 * * 这就是 Loader / Plugin 最容易混淆的地方 * * Markdown * ↓ * HTML * ↓ * JavaScript Module * * 浏览器最终执行的是 JavaScript Module。 */return{code:const html ${JSON.stringify(html)}; export default html;,/** * source map。 * * 这个简单示例没有生成真正的 Source Map * 所以返回 null。 * * 正式插件应根据转换结果生成对应 Source Map。 */map:null};}};}vite.config.jsimport{defineConfig}fromvite;importmarkdownPluginfrom./plugins/markdownPlugin.js;exportdefaultdefineConfig({plugins:[markdownPlugin()]});src/main.jsimportmarkdownfrom./README.md;/** * README.md 最终已经被插件转换成 * * export default h1.../h1... * * 因此这里拿到的是 HTML 字符串。 */document.querySelector(#app).innerHTMLmarkdown;src/README.md# Hello Vite Vite Plugin Demo ## HMR Markdown 可以被转换成 JavaScript Module。整体执行链main.js ↓ import README.md ↓ Vite Dev Server ↓ Markdown Plugin ↓ transform(code, README.md) ↓ Markdown → HTML ↓ HTML → JavaScript Module ↓ export default html ↓ 浏览器执行十二、Vite 底层原理整体架构图把整个知识点串起来┌──────────────────────┐ │ Browser │ │ Native ECMAScript │ │ Modules │ └──────────┬───────────┘ │ HTTP / WebSocket │ ↓ ┌──────────────────────┐ │ Vite Dev Server │ └──────────┬───────────┘ │ ┌────────────────────┼───────────────────┐ │ │ │ ↓ ↓ ↓ Module Graph Plugin Container HMR Runtime │ │ │ │ ↓ │ │ resolveId/load/ │ │ transform │ │ │ ↓ ↓ 文件依赖关系 WebSocket │ │ └────────────────────┬───────────────────┘ ↓ 局部模块更新 第三方依赖 ↓ esbuild ↓ Dependency Pre-Bundling ↓ CommonJS / Universal Module Definition ↓ ECMAScript Modules ↓ node_modules/.vite/deps生产环境则发生变化源码 ↓ Vite ↓ Rollup ↓ Plugin Container ↓ Module Graph ↓ Chunk Graph ↓ Code Splitting ↓ Tree Shaking ↓ Asset Processing ↓ 最终 Bundle十三、这道题真正考察的底层原理面试官表面问“Vite 涉及哪些底层原理”实际上是在考察下面这条链① 浏览器原生 ECMAScript Modules ↓ ② Dev Server 按请求提供模块 ↓ ③ Module Graph 管理模块关系 ↓ ④ Dependency Pre-Bundling ↓ ⑤ esbuild 处理第三方依赖 ↓ ⑥ Plugin Container ↓ ⑦ Module Transform ↓ ⑧ HMR ↓ ⑨ WebSocket 通知 ↓ ⑩ HMR Boundary Module Graph 冒泡不是简单回答“Vite 用 esbuild所以很快。”这只能回答其中一个点。十四、面试官连续追问时应该这样回答追问 1Vite 为什么开发时不需要整体打包因为浏览器原生支持 ECMAScript Modules。 Vite 开发环境启动 Dev Server 浏览器请求哪个模块Vite 就按请求转换哪个模块 因此不需要像传统 Bundle 工具一样启动时先生成完整 Bundle。追问 2那第三方依赖怎么办第三方依赖可能使用 CommonJS 同时可能存在大量内部模块。 所以 Vite 会用 esbuild 做 Dependency Pre-Bundling 把依赖转换成浏览器可以加载的 ECMAScript Modules 同时减少请求数量并缓存结果。追问 3Vite 怎么知道哪个文件发生变化Dev Server 监听文件系统变化 然后根据 Module Graph 找到对应模块 失效相关缓存并通过 WebSocket 通知浏览器。追问 4为什么有时候只更新一个组件浏览器收到 HMR 更新后 会根据 Module Graph 沿 import 关系寻找 HMR Accept Boundary。 如果当前模块或者上层模块能够接受更新 就局部更新。 如果一直冒泡到入口都没有模块接受 就只能回退到页面刷新。追问 5Vite 为什么插件开发和生产都能用因为 Vite 兼容 Rollup 的核心插件接口。 开发阶段由 Vite Dev Server 按请求调用插件钩子 生产阶段由 Rollup 在完整构建过程中调用同一套核心钩子。 所以插件可以复用 但执行时机、调用范围和上下文并不完全一样。十五、最终满分答案面试题Vite 涉及哪些底层原理为什么开发这么快核心思路一句话Vite 开发环境利用浏览器原生 ECMAScript Modules Dev Server 按需转换模块第三方依赖通过 esbuild 预构建HMR 基于 Module Graph 精确定位变化模块生产环境再交给 Rollup 完成整体构建。一张图讲清楚Vite │ ┌────────────┴────────────┐ ↓ ↓ 开发环境 生产环境 ↓ ↓ 浏览器原生 ECMAScript Modules Rollup ↓ ↓ Dev Server 整体构建 ↓ ↓ 按请求转换模块 Tree Shaking ↓ Code Splitting Module Graph Asset Processing ↓ ↓ HMR WebSocket 最终 Bundle 第三方依赖 node_modules ↓ esbuild Dependency Pre-Bundling ↓ CommonJS / Universal Module Definition ↓ ECMAScript Modules ↓ 缓存到 node_modules/.vite1. 为什么开发时不需要整体打包因为浏览器原生支持 ECMAScript Modules。浏览器请求 main.js ↓ Vite 返回 ECMAScript Module ↓ 浏览器发现 import ↓ 继续请求依赖 ↓ Vite 按需转换所以 Vite 的核心不是“完全不编译”而是不在开发启动阶段把整个应用先 Bundle。2. 为什么还需要依赖预构建因为第三方依赖可能使用 CommonJS而且内部可能包含大量模块。第三方依赖 ↓ esbuild ↓ CommonJS / Universal Module Definition 转换为 ECMAScript Modules ↓ 减少模块请求 ↓ 缓存所以Vite 不打包业务源码 ≠ Vite 完全不进行构建。3. HMR 为什么快修改文件 ↓ 文件监听器发现变化 ↓ Module Graph 定位模块 ↓ 失效缓存 ↓ WebSocket 通知浏览器 ↓ 重新请求变化模块 ↓ 沿 import 链寻找 HMR Accept Boundary ↓ 找到 → 局部更新 没找到 → 页面刷新真正的核心是只处理变化模块而不是重新构建整个应用 Bundle。4. Module Graph 有什么作用它记录模块 → import 谁 模块 ← 被谁 import因此修改C.js可以找到C ↑ B ↑ A然后沿依赖关系寻找能够接受 HMR 的模块。所以 Module Graph 是Vite 精确更新和 HMR 冒泡的基础。5. 为什么插件可以同时用于开发和生产Vite 兼容 Rollup 核心插件接口。Plugin │ ┌────────┴────────┐ ↓ ↓ Vite Dev Vite Build ↓ ↓ Dev Server Rollup ↓ ↓ resolveId/load/transform开发环境按请求处理一个模块。生产环境Rollup 遍历整个模块图进行完整构建。因此插件接口可以复用但执行范围和上下文不同。最后一句话Vite 快的根本不是单纯因为 esbuild 快而是改变了开发阶段的构建架构浏览器负责原生模块加载Vite 负责按需转换Module Graph 负责依赖关系和 HMResbuild 负责第三方依赖预构建生产环境再由 Rollup 完成最终 Bundle。这套答案基本可以作为前端工程化面试的主答案先讲“开发架构变化”再展开原生 ECMAScript Modules → 依赖预构建 → Module Graph → HMR → Plugin Container → Rollup Production Build面试官继续追问时就沿这条链往下展开。
返回列表