Files
vite-plugin-demo/docs/vite.md
2026-06-26 17:56:46 +08:00

25 KiB
Raw Blame History

Vite 面试题(含详细解答)

基于本项目的 Vue 3 + Vite + 自定义插件实战经验整理。


1. Vite 和 Webpack 在项目开发中有哪些区别?

这是一道高频面试题,关键不是背概念,而是从开发体验、构建机制、配置复杂度、生态兼容性四个维度说清楚。

1.1 核心差异:开发阶段的模块处理方式

Webpack 是一个 Bundle-based 的构建工具。开发时它会从入口文件出发递归解析所有依赖把所有模块打包成一个或多个bundle然后启动 dev server。每次代码变更它需要重新打包受影响的模块并刷新 bundle这就是为什么大项目中 Webpack 的 HMR 有时会明显变慢。

Vite 在开发阶段不走打包流程,而是利用浏览器原生支持的 ES Module (ESM)。它直接把 .vue.ts.jsx 等源码文件按需编译后通过 ESM 发给浏览器。浏览器通过 <script type="module"> 发起一个个 import 请求Vite 的 dev server 拦截这些请求并实时编译返回。这种「按需编译」的方式使得冷启动和热更新都非常快,几乎与项目规模无关。

Webpack 开发流程:
  入口 → 递归打包所有依赖 → bundle → dev server → 浏览器

Vite 开发流程:
  浏览器 import → dev server 拦截 → 按需编译单个文件 → 返回浏览器

1.2 冷启动速度

Webpack Vite
冷启动 预先打包整个应用,大型项目可能需要几十秒甚至几分钟 只编译浏览器当前请求的文件,通常 < 1s
原理 Bundle-first ESM on-demand

1.3 热更新HMR

  • Webpack:文件变更后,需要重新构建受影响的模块链并替换 bundle项目越大越慢。
  • Vite:只失效变更文件及其直接依赖链上的模块,通过 WebSocket 推送更新,浏览器重新 import 即可。因为本身就是按需加载的HMR 粒度天然更细。

1.4 底层依赖

阶段 Webpack Vite
开发构建 Webpack 自己JS 实现) esbuildGo 实现,快 10-100 倍)做预构建
生产打包 Webpack / Terser Rollup(成熟的 ES module 打包器)

为什么 Vite 不用 esbuild 打包生产环境? esbuild 虽然极快但在旧版本js代码支持代码分割、Tree Shaking 的精细控制方面不如 Rollup 成熟。Vite 选择 Rollup 作为生产构建引擎是务实的选择。

1.4.1 依赖预构建的两大作用(关键技术)

Vite 的依赖预构建解决两个核心问题:

① 格式兼容 — CJS/UMD → ESM

很多第三方依赖(特别是老牌 npm 包)发布的是 CommonJS 或 UMD 格式。浏览器原生只支持 ESM无法直接加载 CJS 模块。esbuild 在预构建阶段将这些包统一转换为浏览器可识别的 ESM 格式。

// node_modules 中的 CJS 包
module.exports = function sum(a, b) { return a + b }

// 预构建后node_modules/.vite/deps/sum.js
export default function sum(a, b) { return a + b }

② 性能优化 — 合并细碎模块

有些包内部由成百上千个独立文件组成(典型如 lodash-es,每个工具函数一个文件)。如果不做合并,浏览器将发起数百个级联的 HTTP 请求,形成严重的请求瀑布:

浏览器 import lodash-es
  → 请求 /node_modules/lodash-es/add.js
    → 请求 /node_modules/lodash-es/_baseAdd.js
      → 请求 /node_modules/lodash-es/_createMathOperation.js
        → ...(数百个请求层层依赖)

Vite 将这些细碎模块预构建为单个或少数几个文件,把成百上千次请求合并成数次,大幅减少浏览器请求带来的性能开销。预构建结果缓存在 node_modules/.vite/deps/,只在依赖变更时重新执行。

1.5 配置复杂度

Webpack:你需要配置 entryoutputloader(如 css-loaderstyle-loaderbabel-loader)、plugin(如 HtmlWebpackPluginMiniCssExtractPlugin)。一个中等规模的项目 webpack.config.js 动辄几百行。

Vite:开箱即用。.vue.ts.jsx、CSS、静态资源都不需要额外配置。一个典型的 Vite 配置:

// vite.config.js — 本项目实际配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()]   // 一行搞定 Vue SFC 支持
})

1.6 生态与兼容性

  • Webpack生态极其庞大loader 和 plugin 覆盖几乎所有场景。如果你需要兼容 IE11 或做一些非常规的构建操作Webpack 是更安全的选择。
  • Vite:插件生态快速增长中,但一些老旧/小众的 Webpack loader 可能没有直接对应。不过 Vite 的 Rollup 插件兼容层可以复用大量 Rollup 插件。

1.7 总结

维度 Webpack Vite
开发模式 Bundle-first ESM on-demand
冷启动 慢(全量打包) 快(按需编译)
HMR 随项目规模递减 几乎恒定
预构建 esbuildGo
生产打包 Webpack + Terser Rollup
配置复杂度
生态成熟度 极高 快速增长
浏览器要求 无限制 需支持 ESM现代浏览器

一句话总结Vite 用「开发时不打包 + esbuild 预构建 + Rollup 生产打包」的组合拳,在开发体验上对 Webpack 形成了代际优势Webpack 则在兼容性和极端定制场景上仍有不可替代的地位。


2. Vite 的核心工作原理是什么?

2.1 开发阶段

  1. 启动 dev serverVite 启动一个基于 connect 的 HTTP 服务器。
  2. 预构建依赖:用 esbuild 把 node_modules 中的 CommonJS/UMD 依赖转换为 ESM并合并细碎模块如 lodash-es 的几百个文件)以减少浏览器请求数。
  3. 源码按需编译:浏览器请求 src/main.js → Vite 拦截 → 用 esbuild 编译 JS/TS → 返回 ESM 格式。
  4. .vue 文件处理:由 @vitejs/plugin-vue 拦截 .vue 请求,将其拆分为 template、script、style 三部分分别处理。
  5. HMR文件变更时Vite 通过 WebSocket 通知浏览器,浏览器重新请求变更的模块。

2.2 生产构建

直接使用 Rollup 打包,因为 Rollup 在 tree-shaking、代码分割、输出稳定性方面是最好的 ESM 打包器。


3. Vite 的预构建Pre-bundling做了什么为什么需要它

3.1 做了什么

Vite 使用 esbuildnode_modules 中的依赖进行预构建:

  1. CJS → ESM:将 CommonJS/UMD 格式的包转为 ESM。
  2. 合并碎片模块:有些包(如 lodash-es由数百个独立文件组成Vite 将它们合并为单个模块,大幅减少 HTTP 请求。

3.2 为什么需要

  • CJS 兼容:浏览器只认识 ESM而 npm 上有大量 CJS 包。
  • 请求瀑布:如果不合并,lodash-es 的一个 import 可能触发几百个级联请求,拖慢页面加载。

3.3 缓存策略

预构建结果缓存在 node_modules/.vite/deps/,只有 package.json 中的依赖列表变更时才会重新执行,开发阶段几乎无感。


4. 为什么 Vite 开发时用 esbuild生产打包用 Rollup

esbuild Rollup
优势 极快Go 语言,原生并发) 成熟的 tree-shaking、精细的代码分割控制
劣势 代码分割能力弱,对 ESM 输出的精细控制不够 相对较慢
适用阶段 开发——需要快速编译和预构建 生产——需要最优的输出质量

Vite 不是绑死在一个工具上,而是「谁适合干什么就让它干什么」。


5. Vite 插件钩子完整有哪些?执行顺序是怎样的?

Vite 插件兼容 Rollup 全部钩子,并扩展了 Vite 特有钩子。钩子分为 Build 阶段(解析模块依赖)和 Output 阶段(生成产物)。

5.1 钩子分类总览

阶段 钩子 要点
Build options 修改构建配置
Build buildStart 构建开始
Build resolveId 自定义模块解析alias、虚拟模块
Build load 返回模块源码(虚拟模块核心)
Build transform 最常用 — 转换单个模块代码
Build moduleParsed AST 解析完成,读取 import/export
Build resolveDynamicImport 处理动态 import()
Build buildEnd 构建结束(成功/失败都调用)
Output outputOptions 修改输出配置
Output renderStart 产物生成开始
Output banner / footer / intro / outro chunk 首尾注入代码
Output renderChunk 转换整个 chunk 代码
Output augmentChunkHash 影响 chunk 文件名 hash
Output generateBundle 最常用 — 产物生成完、写盘前增删改
Output writeBundle 产物写盘后
Output closeBundle 收尾清理
Vite config 修改/合并 vite 配置(插件入口首选)
Vite configResolved 配置解析完毕,缓存最终配置
Vite configureServer 给 dev server 加中间件
Vite configurePreviewServer 给预览服务器加中间件
Vite transformIndexHtml 向 HTML 注入 <script> / <link> 标签
Vite handleHotUpdate 自定义 HMR 行为

5.2 完整执行顺序

options → buildStart
  → resolveId → load → transform → moduleParsed
  → resolveDynamicImport
→ buildEnd

→ outputOptions → renderStart
  → banner → footer → intro → outro
  → renderChunk → augmentChunkHash
→ generateBundle → writeBundle → closeBundle

5.3 钩子执行类型

类型 含义 典型钩子
async 可返回 Promise buildStarttransform
sync 同步执行 outputOptions
first 多个插件排队,第一个返回非 null 即停止 resolveIdload
sequential 多个插件按序依次执行 transformrenderChunk
parallel 多个插件并行执行 buildEndmoduleParsed

5.4 常见面试追问

transform vs renderChunk transform 针对单个模块AST 解析前renderChunk 针对整个 chunk(多个模块合并后)。

generateBundle 为什么最常用? 产物已生成但未写盘,可 this.emitFile() 追加资源、delete bundle[fileName] 删除、直接改 chunk.code

虚拟模块怎么实现? resolveId 拦截特定 id → load 返回代码字符串id 前加 \0 前缀标记虚拟)。

插件间怎么共享数据? moduleInfo.meta 对象可读写;闭包缓存 configResolved 的配置引用。

废弃钩子: transformBundlegenerateBundletransformChunkrenderChunkongenerate / onwritegenerateBundle / writeBundle

5.5 enforce 执行顺序

enforce: 'pre' 插件
    ↓
核心插件(如 @vitejs/plugin-vue
    ↓
普通插件(默认,如本项目中的 helloPlugin
    ↓
enforce: 'post' 插件
    ↓
Vite 内置构建插件

本项目中的自定义插件示例:

// vite.config.js 中的 helloPlugin
function helloPlugin() {
  return {
    name: 'helloPlugin',
    transform(src, id) {
      if (!id.includes('/src/')) return null         // 只处理业务代码
      if (process.env.NODE_ENV === 'production') {
        const result = src.replace(/console\.log\([^)]*\);?/g, '')
        return { code: result, map: null }
      }
      return null   // 返回 null 表示不处理
    }
  }
}
  • transform 钩子在每个模块被编译时调用,可修改源码。
  • 返回 null 表示不作为Vite 继续默认流程。
  • name 字段必须有,用于调试和错误追踪。

6. Vite 的 HMR热模块替换是如何工作的

  1. 文件监听Vite 使用 chokidar 监听文件变更。
  2. 模块失效文件变更后Vite 以该文件为起点,沿着模块依赖图找到所有受影响的模块。
  3. 边界判定找到最近的「HMR 边界」(通常是接受热更新的组件/模块),只重载边界内的模块。
  4. WebSocket 推送Vite 通过 WebSocket 向浏览器发送更新信息(变更文件路径 + 更新类型)。
  5. 浏览器处理:客户端 runtime 收到消息后,重新 import 变更的模块并执行框架特定的更新逻辑(如 Vue 的 hot.accept)。

对于 Vue SFC@vitejs/plugin-vue 为每个 .vue 文件注入 HMR 边界代码,所以修改一个组件只更新它自己,不影响页面状态。


7. Vite 中如何处理环境变量?

Vite 通过 .env 文件加载环境变量,支持四种优先级:

文件 用途
.env 所有环境共享
.env.local 本地覆盖(应加入 .gitignore
.env.[mode] 特定模式(如 .env.production
.env.[mode].local 特定模式的本地覆盖

在代码中通过 import.meta.env 访问:

// 所有变量VITE_ 前缀的才会暴露给客户端
console.log(import.meta.env.VITE_API_BASE_URL)

// 内置变量
import.meta.env.MODE   // 'development' | 'production'
import.meta.env.DEV    // boolean
import.meta.env.PROD   // boolean
import.meta.env.BASE_URL  // 部署基础路径

安全性:只有 VITE_ 前缀的变量会暴露给客户端,这防止了服务端密钥泄露到浏览器。


8. Vite 如何做代码分割Code Splitting

Vite 使用 Rollup 的生产构建能力,代码分割主要通过以下方式:

8.1 动态 import自动分割

任何动态 import 都会在打包时自动产生独立的 chunk无需任何配置

// 源码
import('./sum.js').then(({ sum }) => {
  console.log(sum(1, 2))
})

// 打包产物
// dist/assets/sum-xxxx.js   ← 自动分离为独立 chunk

打包产物文件名中的 xxxx 是内容哈希,用于浏览器长期缓存。当文件内容不变时,哈希不变,浏览器直接命中缓存。

8.2 手动配置分割策略 — 对象语法

如果你的项目里有一个通用模块需要单独拆包(比如一段多处引用的工具函数),可以通过 manualChunks 手动指定:

// src/test.js
export const sayHello = () => {
  console.log('hello')
}
// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // key: chunk 名称value: 模块路径
          'test-vendor': './src/test.js'
        }
      }
    }
  }
})

8.3 手动配置分割策略 — 函数语法(更灵活)

对象语法只能匹配精确路径,函数语法可以基于模块 ID 做任意逻辑判断:

// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes('test')) {
            return 'test-vendor'   // 所有路径含 'test' 的模块合并为一个 chunk
          }
        }
      }
    }
  }
})

函数语法更适合实际项目:你可以按 node_modules 来源拆分 vendor按业务域拆分 page chunks或者按体积阈值动态决定是否拆分。


9. Vite 的 CSS 处理有什么特点?

  1. 原生 CSS 支持:直接 import './style.css'Vite 自动注入到页面。
  2. CSS Modules:文件名以 .module.css 结尾,自动启用 CSS Modules。
  3. PostCSS:项目根目录放置 postcss.config.js 即可Vite 会自动应用。
  4. CSS 代码分割:生产构建时,每个异步 chunk 的 CSS 会被提取为独立文件,按需加载。
  5. @import 内联与 rebase@importurl() 路径会自动重写,避免开发与生产路径不一致。
  6. 预处理器:安装 sassless 即可直接使用 .scss.less 文件,无需额外配置 loader。

10. Vite 的静态资源处理策略是怎样的?

  • 小资源(< 4KB:自动转为 base64 内联,减少 HTTP 请求。
  • 大资源:复制到 dist/assets/,文件名包含 hash 用于缓存。
  • public 目录:此目录下的资源不会被处理,直接复制到 dist/ 根目录,适合 robots.txtfavicon.ico 等。
  • new URL:支持通过 new URL('./img.png', import.meta.url) 动态引用资源。

11. 如何调试 Vite 的自定义插件?

  1. 添加 name:每个插件必须有唯一的 name,方便定位问题。
  2. 在项目中引入 vite-plugin-inspect
  3. 使用 console.log:在插件的各个钩子中添加日志,观察执行时机和参数。 启用 debug 模式
    DEBUG=vite:* vite
    
  4. 使用 Vite 的 apply 选项:可以限制插件只在开发或生产环境生效:
    {
      name: 'dev-only-plugin',
      apply: 'serve'   // 或 'build',分别限制开发/生产
    }
    

12. Vite 项目从开发到部署的完整流程是怎样的?

# 开发
npm run dev         # vite — 启动 dev serverESM 按需编译 + HMR

# 构建
npm run build       # vite build — Rollup 打包,输出到 dist/

# 本地预览生产构建
npm run preview     # vite preview — 在本地启动静态服务器预览 dist/

典型部署:将 dist/ 目录部署到 CDN 或静态服务器Nginx、Vercel、Netlify 等)。

对于 SPA 应用,需要配置 fallback 到 index.html

location / {
  try_files $uri $uri/ /index.html;
}

13. 有没有了解微内核设计Vite 的插件化设计思想是怎样完成个性化打包构建需求的?

13.1 什么是微内核架构

微内核Microkernel是一种架构模式核心思想是内核只提供最精简的基础能力,所有扩展功能通过插件机制加载。操作系统领域最经典的例子是 Minix 和 macOS 的 XNU 内核。

在前端构建工具领域,这个思想同样适用:

┌──────────────────────────────────────────┐
│              插件层Plugin Layer        │
│  Vue SFC  │  JSX  │  TS  │  CSS  │  ...  │ 
├──────────────────────────────────────────┤
│            微内核Minimal Core          │
│  dev server  │  HMR  │  module graph     │
│  esbuild预构建 │  中间件机制               │
└──────────────────────────────────────────┘

13.2 Vite 的微内核设计

Vite 的核心(内核)只做几件事:

内核能力 说明
Dev Server 基于 connect 的 HTTP 服务器,拦截浏览器请求
模块图谱 追踪所有模块的依赖关系,驱动 HMR
esbuild 预构建 CJS → ESM 转换 + 细碎模块合并
中间件/钩子系统 提供插件接入点,但不做具体编译

Vue SFC 编译、JSX 转换、TypeScript 类型检查、CSS 预处理 —— 这些全部由插件完成,不在内核里。 这就是微内核思想:内核保持精简稳定,功能由插件按需扩展。

13.3 插件机制如何实现个性化构建

Vite 的插件体系基于 Rollup 插件接口 并做了增强:

① 通用钩子Rollup 兼容层)

{
  name: 'my-plugin',
  // 构建开始
  buildStart() {},
  // 模块解析(可拦截 import 路径,实现虚拟模块)
  resolveId(id) {},
  // 模块加载(可返回自定义内容)
  load(id) {},
  // 代码转换(最常用)
  transform(src, id) {}
}

② Vite 独有钩子

{
  name: 'my-vite-plugin',
  // 配置解析阶段(修改用户配置)
  config(config) {},
  // dev server 启动后(注入自定义中间件)
  configureServer(server) {},
  // 处理 HMR 更新
  handleHotUpdate(ctx) {}
}

13.4 一个完整的个性化构建案例

回到本项目的 helloPlugin,它实现了一个个性化需求——「生产环境自动移除 console.log」

// vite.config.js
function helloPlugin() {
  return {
    name: 'helloPlugin',
    // 接入 transform 钩子,拿到每个模块的源码
    transform(src, id) {
      if (!id.includes('/src/')) return null     // 只处理业务代码
      if (process.env.NODE_ENV === 'production') {
        const result = src.replace(/console\.log\([^)]*\);?/g, '')
        return { code: result, map: null }
      }
      return null                                 // 不处理则透传
    }
  }
}

这个插件展示了插件化的核心价值:不需要修改 Vite 源码,不需要 fork 项目,只需写一个函数接入钩子,就能定制打包行为。 这种「内核不动、插件扩展」的模式,正是微内核架构在前端工程化中的最佳实践。

13.5 总结Vite 的插件化设计思想怎样完成个性化打包构建需求?

一个结构化的回答应包含以下几点:

  1. 架构层面Vite 采用了微内核设计。它的核心非常轻量,仅包含一个 ESM 开发服务器和一套插件加载机制。所有高层功能如框架支持、CSS 预处理、特定文件加载等,都由独立的插件实现。
  2. 接口层面Vite 提供了一套兼容 Rollup 的、统一的插件接口。这套接口通过暴露一系列生命周期钩子 (Hooks)允许开发者在构建过程的各个关键节点如配置解析、模块加载、代码转换、HTML 生成等)注入自定义逻辑。
  3. 实践层面:开发者可以编写一个插件,通过选择合适的钩子来完成特定任务。例如,使用 transform 钩子可以支持一种新的语言或文件格式;使用 configureServer 钩子可以在开发时添加自定义的服务器中间件;使用 transformIndexHtml 则可以动态修改主页面的内容。

14. 站在前端架构角度说说 Bundleless 原理

14.1 什么是 Bundleless

传统的 Bundle 模式Webpack 为代表)是「先打包再启动」:

源码 → 打包器Bundle→ 一个/多个 bundle 文件 → 浏览器加载

Bundleless 模式Vite 为代表)跳过了打包这一步:

源码 → 开发服务器按需编译 → 浏览器通过 ESM 直接加载

14.2 为什么 Bundleless 现在才成为主流

Bundleless 不是新概念——ESM 规范早在 2015 年就发布了。之前无法普及的原因:

时间 瓶颈 现状
2015-2018 浏览器 ESM 支持率低IE 占主流) IE 已淘汰,现代浏览器全覆盖
2015-2019 npm 包几乎全是 CJS 格式 新包逐渐提供 ESM 入口,且有 esbuild 做转换
2015-2019 HTTP/1.1 并发连接有限6个/域名) HTTP/2 多路复用,数百个请求不再是瓶颈

三个条件同时满足——浏览器 ESM 支持、npm 生态转换能力、HTTP/2 普及——Bundleless 才真正可行。

14.3 Bundleless 的架构优势

① 按需编译,冷启动 O(1)

传统打包:启动时间 ∝ 项目模块数。Bundleless只编译浏览器当前请求的文件启动几乎恒定。

② HMR 粒度天然匹配 ESM 边界

每个文件就是一个 ESM 模块,热更新不需要重新打包 bundle只需要让浏览器重新 import 变更的那个文件。

③ 开发与生产的明确分工

开发阶段Bundleless          生产阶段Bundled
─────────────────────         ─────────────────────
  不打包,按需编译                Rollup 全量打包
  esbuild 做预构建               Tree-shaking
  ESM 原生加载                  代码分割 + 压缩
  追求开发体验                  追求加载性能

这两个阶段的目标本就不同开发要快生产要小。Bundleless 把这个分工理清了。

14.4 Bundleless 的工程代价

从架构角度,不是所有场景都适合 Bundleless

场景 Bundleless 是否适用 原因
现代浏览器开发 完美 ESM + HTTP/2
需要兼容 IE 不行 IE 不支持 ESM
巨型 node_modules ⚠️ 需预构建 否则请求数爆炸
非 JS 资源CSS/图片) ⚠️ 需额外处理 ESM 只管 JS

这也是为什么 Vite 不是纯 Bundleless——它用预构建和 Rollup 生产打包弥补了 Bundleless 的短板。务实的设计比纯粹的理念更重要。


扩展思考:如果你来设计一个类似的工具,会怎么做?

这通常是大厂面试的进阶追问。可以从以下角度组织回答:

  1. 利用浏览器原生能力:现代浏览器已经支持 ESM、ES2020+ 语法,开发阶段不需要向下兼容。
  2. 语言选择:高频编译用 Go/Rust 实现的工具(如 esbuild、SWC生产打包用成熟的 JS 生态工具。
  3. 按需编译:改变「先打包再启动」的思路,变成「请求时编译」。
  4. 插件兼容:设计插件接口时尽量对齐已有标准(如 Rollup 插件格式),降低迁移成本。