Skip to content

Vite

Vite(法语意为"快")是由 Vue 作者尤雨溪开发的下一代前端构建工具。它的核心理念是:开发阶段利用浏览器原生 ESM 能力按需加载模块,跳过打包步骤,从而实现毫秒级的冷启动和极速的热更新。生产构建则基于 Rollup,产出高度优化的静态资源。

本文将从底层原理 → 配置体系 → 插件机制 → 性能优化 → 横向对比 → 面试高频题六个维度全面拆解 Vite。


一、Vite 核心原理

1.1 为什么 Vite 快

传统构建工具(如 Webpack)采用 Bundle-based 模式:不管你改了哪一行代码,dev server 启动时都要先把所有模块打包成一个或多个 bundle,然后才能在浏览器中运行。项目越大,启动越慢。

Vite 采用 Native ESM-based 模式:利用现代浏览器原生支持 <script type="module"> 的能力,dev server 启动时只需启动一个 HTTP 服务器,浏览器请求哪个模块才编译哪个模块。

Webpack 启动流程(Bundle-based):

┌─────────────────────────────────────────────────────┐
│                   启动 dev server                     │
│                        ↓                              │
│   ┌────────────────────────────────────────────┐     │
│   │         扫描全部入口和依赖关系                  │     │
│   │                    ↓                         │     │
│   │         编译所有模块(Loader 处理)             │     │
│   │                    ↓                         │     │
│   │         构建完整依赖图                         │     │
│   │                    ↓                         │     │
│   │         生成 Bundle(打包输出)                 │     │
│   └────────────────────────────────────────────┘     │
│                        ↓                              │
│              Bundle 准备好,页面可访问                   │
└─────────────────────────────────────────────────────┘

时间线:[===== 全量打包 =====]→ 页面可用

                        可能需要数十秒
Vite 启动流程(Native ESM-based):

┌─────────────────────────────────────────────────────┐
│                   启动 dev server                     │
│                        ↓                              │
│   ┌────────────────────────────────────────────┐     │
│   │   esbuild 预构建第三方依赖(仅首次/依赖变化)    │     │
│   │              (极快,毫秒级)                   │     │
│   └────────────────────────────────────────────┘     │
│                        ↓                              │
│              HTTP Server 就绪,页面可访问               │
│                        ↓                              │
│   ┌────────────────────────────────────────────┐     │
│   │   浏览器发起 ESM 请求 → 按需编译单个模块         │     │
│   │   GET /src/App.tsx → 编译 App.tsx → 返回         │     │
│   │   GET /src/utils.ts → 编译 utils.ts → 返回       │     │
│   │              (只编译请求到的文件)               │     │
│   └────────────────────────────────────────────┘     │
└─────────────────────────────────────────────────────┘

时间线:[==预构建==]→ 页面可用 → [按需编译...]

               通常 < 1 秒

关键差异:Webpack 的启动时间和项目规模成正比,而 Vite 的启动时间几乎与项目规模无关——因为它把编译工作推迟到了浏览器请求时按需执行。

1.2 开发环境:原生 ESM + esbuild 预构建

Vite 开发服务器的核心架构分为两部分:

第一部分:Dependency Pre-Bundling(依赖预构建)

浏览器原生 ESM 有两个问题需要解决:

  1. 裸模块导入import React from 'react' 浏览器无法识别,不知道去哪里找 react
  2. 请求瀑布流lodash-es 有 600+ 个内部模块,如果每个都发起一个 HTTP 请求,浏览器会被压垮

Vite 的解决方案:在 dev server 启动前,使用 esbuildnode_modules 中的第三方依赖进行预构建:

预构建流程:

node_modules/react/           node_modules/lodash-es/
├── index.js (CJS)            ├── add.js
├── cjs/                      ├── chunk.js
│   └── react.development.js  ├── clone.js
└── ...                       ├── ... (600+ 文件)
                              └── lodash.js
        │                              │
        │       esbuild 预构建          │
        ▼                              ▼
.vite/deps/                   .vite/deps/
├── react.js (ESM, 单文件)    └── lodash-es.js (ESM, 单文件)
└── react.js.map

esbuild 做了两件事:

  • 格式转换:将 CJS/UMD 格式转为 ESM
  • 合并模块:将有大量内部模块的包(如 lodash-es)合并为单个文件,减少 HTTP 请求数

为什么选 esbuild?因为 esbuild 使用 Go 编写,利用多线程和高效的内存管理,编译速度比传统 JS 编写的工具(如 tsc、Babel)快 10-100 倍:

各工具编译速度对比(编译大型项目):

esbuild    ████                           0.33s
swc        ████████                       0.8s
Babel      ████████████████████████████   38s
tsc        ████████████████████████████████████  46s

第二部分:源码按需编译

对于项目自身的源码(src/ 下的文件),Vite 不做任何预编译。当浏览器通过 ESM 请求某个模块时,Vite 的 dev server 拦截请求,实时编译并返回:

浏览器请求链路:

Browser                         Vite Dev Server
  │                                    │
  │  GET /src/main.tsx                 │
  │ ──────────────────────────────────→│
  │                                    │  读取 main.tsx
  │                                    │  esbuild 编译 TSX → JS
  │                                    │  重写 import 路径
  │  200 OK (编译后的 ESM JS)           │
  │ ←──────────────────────────────────│
  │                                    │
  │  import './App.tsx' 触发新请求      │
  │  GET /src/App.tsx                  │
  │ ──────────────────────────────────→│
  │                                    │  同样按需编译
  │  200 OK                            │
  │ ←──────────────────────────────────│

源码编译使用 esbuild 处理 TypeScript 和 JSX 转换(仅做语法转换,不做类型检查),CSS 等其他资源由 Vite 内置插件链处理。

1.3 生产环境:为什么用 Rollup 而不是 esbuild

Vite 在生产构建时使用 Rollup 而不是 esbuild。这个设计决策背后有深层考量:

为什么生产构建不用 esbuild?

┌───────────────────────────────────────────────────┐
│                     esbuild 不足                    │
│                                                     │
│  1. 代码分割(Code Splitting)能力有限               │
│     - 不够灵活的 chunk 分割策略                      │
│     - 无法做细粒度的 manualChunks 控制               │
│                                                     │
│  2. CSS 处理能力弱                                   │
│     - 不支持 CSS Code Splitting                     │
│     - 不支持 CSS Modules 的完整语义                  │
│                                                     │
│  3. 插件生态不成熟                                   │
│     - 插件 API 不够丰富                              │
│     - 无法复用 Rollup 庞大的插件生态                  │
│                                                     │
│  4. 产物优化不够精细                                  │
│     - Tree-shaking 不如 Rollup 精确                  │
│     - 缺少高级优化如 scope hoisting                  │
└───────────────────────────────────────────────────┘

┌───────────────────────────────────────────────────┐
│                    Rollup 优势                      │
│                                                     │
│  1. 成熟的代码分割和 Tree-shaking                    │
│  2. 完善的插件 API 和庞大的插件生态                    │
│  3. 输出格式灵活(ESM / CJS / IIFE / UMD)           │
│  4. 对 ESM 输出做了 scope hoisting 优化              │
│  5. 稳定可靠,经过大量生产环境验证                     │
└───────────────────────────────────────────────────┘

值得注意的是,Vite 团队正在开发 Rolldown(用 Rust 重写的 Rollup 兼容构建工具),目标是未来统一开发和生产构建,同时兼顾速度和产物质量。

1.4 HMR 热更新原理

HMR(Hot Module Replacement)是开发体验的核心。Vite 的 HMR 和 Webpack 有本质区别:

Webpack HMR 流程:

文件修改

重新编译受影响的 Chunk(可能包含多个模块)

生成增量更新补丁(.hot-update.js / .hot-update.json)

通过 WebSocket 通知客户端

客户端下载整个更新后的 Chunk

替换旧模块,执行 accept 回调

问题:修改一个文件,整个 Chunk 需要重新编译和传输
     项目越大,HMR 越慢
Vite HMR 流程:

文件修改

分析该模块的 HMR 边界(import.meta.hot.accept)

仅重新编译修改的单个模块(或几个直接关联的模块)

通过 WebSocket 发送更新通知(仅包含模块路径)

客户端通过 ESM 动态 import 加载更新后的模块
GET /src/App.tsx?t=1709123456789  (时间戳使缓存失效)

模块级精确替换

优势:更新粒度是单个模块,与项目规模无关
     修改一个组件,只重新请求该组件的文件

Vite HMR 的核心 API:

js
if (import.meta.hot) {
  import.meta.hot.accept((newModule) => {
    render(newModule.default)
  })

  import.meta.hot.accept(['./foo.js', './bar.js'], ([fooModule, barModule]) => {
    update(fooModule, barModule)
  })

  import.meta.hot.dispose((data) => {
    clearInterval(timer)
    data.savedState = currentState
  })

  import.meta.hot.prune(() => {
    cleanupSideEffects()
  })
}

accept 定义了 HMR 边界:

  • 自身接受(Self-accepting):accept((mod) => {...}) — 模块自己处理更新
  • 依赖接受accept(['./dep'], ([dep]) => {...}) — 接受特定依赖的更新
  • 无回调接受accept() — 接受更新并完全替换模块(框架插件常用)

当一个文件修改后,Vite 沿着模块依赖图向上查找最近的 HMR 边界。如果找不到边界,则整页刷新。React/Vue 等框架插件会自动为组件文件注入 HMR accept 逻辑。

1.5 模块解析与路径重写

Vite dev server 在返回编译后的代码之前,会进行关键的路径重写

js
import React from 'react'
import { useState } from 'react'
import App from './App.tsx'
import styles from './index.module.css'

经过 Vite 处理后变成:

js
import React from '/node_modules/.vite/deps/react.js?v=abc123'
import { useState } from '/node_modules/.vite/deps/react.js?v=abc123'
import App from '/src/App.tsx?t=1709123456'
import styles from '/src/index.module.css?direct'

核心转换规则:

路径重写规则:

1. 裸模块(bare import)
   'react'  →  '/node_modules/.vite/deps/react.js?v=<hash>'
   指向预构建产物,?v= 用于强缓存

2. 相对路径
   './App.tsx'  →  '/src/App.tsx'
   转为绝对路径,dev server 可直接定位文件

3. 特殊后缀
   './icon.svg?url'     →  返回资源 URL 字符串
   './shader.glsl?raw'  →  返回文件原始文本
   './worker.js?worker' →  返回 Web Worker 构造

4. CSS Modules
   './x.module.css' → 返回 JS 对象 + 注入 <style>

二、Vite 配置深入

2.1 vite.config.ts 核心配置

ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'

export default defineConfig({
  root: process.cwd(),
  base: '/',

  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
      '@components': path.resolve(__dirname, 'src/components'),
      '@utils': path.resolve(__dirname, 'src/utils'),
    },
    extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json'],
  },

  server: {
    host: '0.0.0.0',
    port: 3000,
    strictPort: true,
    open: true,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
      '/ws': {
        target: 'ws://localhost:8080',
        ws: true,
      },
    },
    cors: true,
  },

  build: {
    target: 'es2015',
    outDir: 'dist',
    assetsDir: 'assets',
    cssCodeSplit: true,
    sourcemap: false,
    minify: 'esbuild',
    rollupOptions: {
      input: {
        main: path.resolve(__dirname, 'index.html'),
      },
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          router: ['react-router-dom'],
        },
        chunkFileNames: 'assets/js/[name]-[hash].js',
        entryFileNames: 'assets/js/[name]-[hash].js',
        assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
      },
    },
    chunkSizeWarningLimit: 500,
  },

  css: {
    modules: {
      localsConvention: 'camelCaseOnly',
      scopeBehaviour: 'local',
    },
    preprocessorOptions: {
      scss: {
        additionalData: `@use "@/styles/variables" as *;`,
      },
      less: {
        math: 'always',
        javascriptEnabled: true,
      },
    },
    postcss: './postcss.config.js',
    devSourcemap: true,
  },

  define: {
    __APP_VERSION__: JSON.stringify('1.0.0'),
    __DEV__: JSON.stringify(true),
  },

  plugins: [react()],
})

2.2 核心配置项详解

vite.config.ts 配置结构一览:

defineConfig({
  ├── root              项目根目录(index.html 所在目录)
  ├── base              公共基础路径(部署到子路径时设置)
  ├── mode              模式:'development' | 'production'
  ├── define            全局常量替换(编译时替换)

  ├── resolve
  │   ├── alias         路径别名映射
  │   ├── extensions    省略扩展名的解析顺序
  │   └── conditions    package.json exports 的条件解析

  ├── server
  │   ├── host          监听地址
  │   ├── port          端口号
  │   ├── proxy         代理配置(基于 http-proxy)
  │   ├── cors          CORS 配置
  │   ├── hmr           HMR 连接配置
  │   └── watch         文件监听配置(基于 chokidar)

  ├── build
  │   ├── target        编译目标(默认 'modules')
  │   ├── outDir        输出目录
  │   ├── minify        压缩方式:'esbuild' | 'terser' | false
  │   ├── sourcemap     是否生成 sourcemap
  │   ├── cssCodeSplit  是否拆分 CSS
  │   ├── lib           库模式配置
  │   └── rollupOptions 直接传递给 Rollup 的配置

  ├── css
  │   ├── modules       CSS Modules 配置
  │   ├── preprocessorOptions   预处理器选项
  │   ├── postcss       PostCSS 配置
  │   └── devSourcemap  开发环境 CSS sourcemap

  ├── plugins           插件数组
  ├── optimizeDeps      预构建配置
  └── ssr               SSR 相关配置
})

2.3 环境变量

Vite 使用 dotenv 从项目根目录的 .env 文件中加载环境变量:

环境变量文件加载优先级(从低到高):

.env                  所有模式共享
.env.local            所有模式共享(被 git 忽略)
.env.[mode]           指定模式专用
.env.[mode].local     指定模式专用(被 git 忽略)

模式由 --mode 参数决定:
  vite dev          → mode = 'development'
  vite build        → mode = 'production'
  vite build --mode staging  → mode = 'staging'

.env 文件示例:

VITE_API_BASE=https://api.example.com
VITE_APP_TITLE=My App
DB_PASSWORD=secret123

在代码中使用:

ts
console.log(import.meta.env.VITE_API_BASE)
console.log(import.meta.env.VITE_APP_TITLE)
console.log(import.meta.env.MODE)
console.log(import.meta.env.DEV)
console.log(import.meta.env.PROD)
console.log(import.meta.env.BASE_URL)

安全机制:只有以 VITE_ 前缀开头的变量才会暴露给客户端代码。上面示例中的 DB_PASSWORD 不会被打包到客户端,防止敏感信息泄露。

vite.config.ts 中如果需要访问环境变量,不能直接使用 import.meta.env,需要用 loadEnv

ts
import { defineConfig, loadEnv } from 'vite'

export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '')

  return {
    define: {
      __APP_ENV__: JSON.stringify(env.APP_ENV),
    },
  }
})

TypeScript 类型支持:

ts
interface ImportMetaEnv {
  readonly VITE_API_BASE: string
  readonly VITE_APP_TITLE: string
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}

2.4 CSS 处理

Vite 内置了对 CSS 的完整支持,无需额外安装 loader。

CSS Modules

任何以 .module.css 结尾的文件都会被视为 CSS Modules 文件:

css
.container {
  max-width: 1200px;
  margin: 0 auto;
}

.title {
  font-size: 24px;
  color: #333;
}
tsx
import styles from './App.module.css'

function App() {
  return (
    <div className={styles.container}>
      <h1 className={styles.title}>Hello</h1>
    </div>
  )
}

编译后 CSS 类名会被转换为带 hash 的唯一标识,如 _container_1a2b3c

预处理器

Vite 原生支持 .scss.sass.less.styl.stylus 文件,只需安装对应的预处理器即可:

bash
npm install -D sass
npm install -D less
npm install -D stylus

不需要额外安装 loader 或配置规则,Vite 自动识别并处理。

全局注入变量:

ts
export default defineConfig({
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `
          @use "@/styles/variables" as *;
          @use "@/styles/mixins" as *;
        `,
      },
    },
  },
})

PostCSS

Vite 自动读取项目根目录下的 PostCSS 配置文件(postcss.config.js):

js
module.exports = {
  plugins: {
    'postcss-preset-env': {
      stage: 2,
      features: {
        'nesting-rules': true,
      },
    },
    autoprefixer: {},
    ...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {}),
  },
}

2.5 静态资源处理

Vite 对静态资源提供了丰富的导入方式:

ts
import imgUrl from './img.png'

import imgUrl from './img.png?url'

import imgRaw from './shader.glsl?raw'

import Worker from './worker.js?worker'

import init from './example.wasm?init'

各种导入方式的行为:

静态资源导入方式对比:

┌──────────────────┬────────────────────────────────────────┐
│ 导入方式          │ 行为                                    │
├──────────────────┼────────────────────────────────────────┤
│ import x from    │ 默认导入,返回解析后的 URL 字符串          │
│ './img.png'      │ 小于 assetsInlineLimit 的文件会内联为      │
│                  │ base64 data URI                          │
├──────────────────┼────────────────────────────────────────┤
│ ?url             │ 显式以 URL 形式导入,始终返回 URL           │
│                  │ 不会内联为 base64                          │
├──────────────────┼────────────────────────────────────────┤
│ ?raw             │ 以字符串形式导入资源的原始内容                │
│                  │ 适合导入着色器、SVG 源码、文本文件等          │
├──────────────────┼────────────────────────────────────────┤
│ ?worker          │ 导入为 Web Worker                         │
│                  │ 返回 Worker 类的构造函数                    │
├──────────────────┼────────────────────────────────────────┤
│ ?worker&inline   │ 导入为内联 Worker(base64 编码)            │
│                  │ 不生成独立的 Worker 文件                    │
├──────────────────┼────────────────────────────────────────┤
│ ?init            │ WASM 模块导入                             │
│                  │ 返回初始化函数                              │
└──────────────────┴────────────────────────────────────────┘

public/ 目录下的资源不会被 Vite 处理,直接原样复制到输出目录根路径。适合放 favicon.icorobots.txt 等不需要编译处理的文件。引用方式:

html
<img src="/logo.png" />

src/assets/ 目录下的资源会被 Vite 处理(加 hash、内联等)。引用方式:

tsx
import logo from '@/assets/logo.png'

function Header() {
  return <img src={logo} alt="logo" />
}

三、Vite 插件系统

3.1 插件 API 概览

Vite 的插件系统基于 Rollup 插件接口扩展而来。一个 Vite 插件本质上就是一个返回对象的函数,对象中包含各种钩子函数:

ts
function myPlugin(): Plugin {
  return {
    name: 'my-plugin',

    config(config, env) {
      return {
        define: {
          __MY_VAR__: JSON.stringify('hello'),
        },
      }
    },

    configResolved(resolvedConfig) {
      console.log('最终配置:', resolvedConfig.build.outDir)
    },

    configureServer(server) {
      server.middlewares.use((req, res, next) => {
        if (req.url === '/health') {
          res.end('ok')
          return
        }
        next()
      })
    },

    transformIndexHtml(html) {
      return html.replace(
        '</head>',
        '<script>window.__BUILD_TIME__ = Date.now()</script></head>'
      )
    },

    resolveId(source, importer) {
      if (source === 'virtual:my-module') {
        return '\0virtual:my-module'
      }
    },

    load(id) {
      if (id === '\0virtual:my-module') {
        return `export const msg = "from virtual module"`
      }
    },

    transform(code, id) {
      if (id.endsWith('.custom')) {
        return {
          code: `export default ${JSON.stringify(code)}`,
          map: null,
        }
      }
    },
  }
}

3.2 钩子执行顺序

Vite 插件钩子分为 Vite 独有钩子兼容 Rollup 的通用钩子两类,执行顺序如下:

Vite 插件钩子执行顺序:

                    ┌─ 配置阶段 ──────────────────────┐
                    │                                   │
                    │  1. config          修改配置       │
                    │  2. configResolved  读取最终配置    │
                    │                                   │
                    └───────────────────────────────────┘

                    ┌─ 服务器阶段(仅 dev)─────────────┐
                    │                                   │
                    │  3. configureServer  配置 dev 服务  │
                    │                                   │
                    └───────────────────────────────────┘

                    ┌─ 构建阶段 ──────────────────────┐
                    │                                   │
                    │  4. buildStart     构建开始        │
                    │        │                          │
                    │  ┌─ 模块解析循环 ─────────────┐    │
                    │  │                             │    │
                    │  │  5. resolveId   解析模块 ID  │    │
                    │  │        ↓                    │    │
                    │  │  6. load        加载模块内容  │    │
                    │  │        ↓                    │    │
                    │  │  7. transform   转换模块代码  │    │
                    │  │                             │    │
                    │  └─────────────────────────────┘    │
                    │                                     │
                    │  8. buildEnd       构建结束          │
                    │                                     │
                    └─────────────────────────────────────┘

                    ┌─ 输出阶段(仅 build)────────────┐
                    │                                   │
                    │  9.  renderStart                  │
                    │  10. renderChunk                  │
                    │  11. generateBundle               │
                    │  12. writeBundle                  │
                    │  13. closeBundle                  │
                    │                                   │
                    └───────────────────────────────────┘

                    ┌─ HTML 转换 ─────────────────────┐
                    │                                   │
                    │  transformIndexHtml               │
                    │  (dev:每次请求触发)              │
                    │  (build:generateBundle 后触发)  │
                    │                                   │
                    └───────────────────────────────────┘

enforce 属性控制插件执行顺序:

插件排序:

enforce: 'pre'    → 在 Vite 核心插件之前执行
无 enforce         → 在 Vite 核心插件之后执行(默认)
enforce: 'post'   → 在 Vite 构建插件之后执行

实际执行顺序:
1. alias 解析
2. enforce: 'pre' 的用户插件
3. Vite 核心插件(模块解析、CSS、esbuild 等)
4. 无 enforce 的用户插件
5. Vite 构建插件(minify 等)
6. enforce: 'post' 的用户插件

apply 属性控制插件在哪个阶段生效:

ts
function myPlugin(): Plugin {
  return {
    name: 'my-plugin',
    apply: 'build',
  }
}

function myPlugin(): Plugin {
  return {
    name: 'my-plugin',
    apply: 'serve',
  }
}

function myPlugin(): Plugin {
  return {
    name: 'my-plugin',
    apply(config, { command }) {
      return command === 'build' && !config.build?.ssr
    },
  }
}

3.3 常用插件

插件功能包名
React 支持JSX 转换 + Fast Refresh HMR@vitejs/plugin-react
React SWC用 SWC 替代 Babel,更快的 React 编译@vitejs/plugin-react-swc
Vue 支持SFC 编译 + HMR@vitejs/plugin-vue
SVG 转 React 组件SVG → React Componentvite-plugin-svgr
PWA 支持Service Worker + Manifestvite-plugin-pwa
自动导入自动导入 API,无需手写 importunplugin-auto-import
组件自动注册自动注册组件,无需手动 importunplugin-vue-components
构建可视化分析产物体积rollup-plugin-visualizer
Legacy 兼容为旧浏览器生成兼容 chunk@vitejs/plugin-legacy
压缩gzip / brotli 压缩vite-plugin-compression
mock 数据本地 mock APIvite-plugin-mock

3.4 手写 Vite 插件实战

实战一:虚拟模块插件

虚拟模块是 Vite/Rollup 的强大特性——允许你创建不存在于文件系统中的模块:

ts
import type { Plugin } from 'vite'

interface BuildInfo {
  version: string
  buildTime: string
  gitCommit: string
}

function virtualBuildInfoPlugin(): Plugin {
  const virtualModuleId = 'virtual:build-info'
  const resolvedVirtualModuleId = '\0' + virtualModuleId

  return {
    name: 'vite-plugin-build-info',

    resolveId(id) {
      if (id === virtualModuleId) {
        return resolvedVirtualModuleId
      }
    },

    load(id) {
      if (id === resolvedVirtualModuleId) {
        const info: BuildInfo = {
          version: process.env.npm_package_version || '0.0.0',
          buildTime: new Date().toISOString(),
          gitCommit: 'unknown',
        }

        return `export default ${JSON.stringify(info)}`
      }
    },
  }
}

export default virtualBuildInfoPlugin

使用方式:

ts
import buildInfo from 'virtual:build-info'

console.log(buildInfo.version)
console.log(buildInfo.buildTime)

类型声明(env.d.tsvite-env.d.ts):

ts
declare module 'virtual:build-info' {
  interface BuildInfo {
    version: string
    buildTime: string
    gitCommit: string
  }
  const info: BuildInfo
  export default info
}

实战二:自动导入插件

ts
import type { Plugin } from 'vite'
import fs from 'fs'
import path from 'path'

function autoImportPlugin(options: { dirs: string[] }): Plugin {
  const importMap = new Map<string, string>()

  function scanDir(dir: string) {
    if (!fs.existsSync(dir)) return
    const files = fs.readdirSync(dir, { recursive: true }) as string[]

    for (const file of files) {
      if (/\.(ts|js)$/.test(file) && !file.includes('.d.ts')) {
        const fullPath = path.join(dir, file)
        const content = fs.readFileSync(fullPath, 'utf-8')
        const exportMatches = content.matchAll(/export\s+(?:function|const|class)\s+(\w+)/g)

        for (const match of exportMatches) {
          const exportName = match[1]
          const relativePath = fullPath.replace(/\\/g, '/')
          importMap.set(exportName, relativePath)
        }
      }
    }
  }

  return {
    name: 'vite-plugin-auto-import',
    enforce: 'pre',

    configResolved(config) {
      for (const dir of options.dirs) {
        const fullDir = path.resolve(config.root, dir)
        scanDir(fullDir)
      }
    },

    transform(code, id) {
      if (!id.endsWith('.ts') && !id.endsWith('.tsx') &&
          !id.endsWith('.js') && !id.endsWith('.jsx')) {
        return null
      }

      let hasChanges = false
      let imports: string[] = []

      for (const [name, filePath] of importMap) {
        if (filePath === id) continue

        const usageRegex = new RegExp(`\\b${name}\\b`)
        const importRegex = new RegExp(`import.*${name}.*from`)

        if (usageRegex.test(code) && !importRegex.test(code)) {
          const relative = path.relative(path.dirname(id), filePath)
            .replace(/\\/g, '/')
            .replace(/\.(ts|js)$/, '')

          const importPath = relative.startsWith('.') ? relative : `./${relative}`
          imports.push(`import { ${name} } from '${importPath}';`)
          hasChanges = true
        }
      }

      if (!hasChanges) return null

      return {
        code: imports.join('\n') + '\n' + code,
        map: null,
      }
    },
  }
}

export default autoImportPlugin

实战三:Markdown 转 Vue/React 组件插件

ts
import type { Plugin } from 'vite'
import { marked } from 'marked'

function markdownPlugin(): Plugin {
  return {
    name: 'vite-plugin-markdown',
    enforce: 'pre',

    transform(code, id) {
      if (!id.endsWith('.md')) return null

      const html = marked.parse(code) as string
      const escaped = JSON.stringify(html)

      return {
        code: `export default ${escaped}`,
        map: null,
      }
    },
  }
}

export default markdownPlugin

实战四:configureServer 实现 Mock API 中间件

ts
import type { Plugin } from 'vite'

interface MockRoute {
  url: string
  method: 'GET' | 'POST' | 'PUT' | 'DELETE'
  response: (params: { query: Record<string, string>; body: any }) => any
}

function mockPlugin(routes: MockRoute[]): Plugin {
  return {
    name: 'vite-plugin-mock',
    apply: 'serve',

    configureServer(server) {
      for (const route of routes) {
        server.middlewares.use(route.url, (req, res, next) => {
          if (req.method?.toUpperCase() !== route.method) {
            next()
            return
          }

          const url = new URL(req.url || '', `http://${req.headers.host}`)
          const query = Object.fromEntries(url.searchParams)

          let body = ''
          req.on('data', (chunk) => { body += chunk })
          req.on('end', () => {
            let parsedBody = {}
            try { parsedBody = JSON.parse(body) } catch {}

            const data = route.response({ query, body: parsedBody })
            res.setHeader('Content-Type', 'application/json')
            res.end(JSON.stringify(data))
          })
        })
      }
    },
  }
}

export default mockPlugin

四、性能优化

4.1 预构建优化

Vite 自动检测需要预构建的依赖,但有些场景需要手动干预:

ts
export default defineConfig({
  optimizeDeps: {
    include: [
      'lodash-es',
      'axios',
      'dayjs',
      'dayjs/plugin/relativeTime',
    ],

    exclude: [
      '@vueuse/core',
    ],

    esbuildOptions: {
      target: 'esnext',
      supported: {
        'top-level-await': true,
      },
    },
  },
})
何时需要手动配置 optimizeDeps?

┌──────────────────────────────────────────────────────────┐
│ include(强制预构建)                                       │
│                                                            │
│ 1. 动态 import 的依赖(Vite 静态分析检测不到)               │
│    const module = await import(`./locale/${lang}.js`)       │
│                                                            │
│ 2. 依赖的依赖(深层依赖)导致请求瀑布流                      │
│    例如 A 依赖 B,B 依赖 C,C 依赖 D...                     │
│    include: ['A > B > C > D']                              │
│                                                            │
│ 3. monorepo 中 linked 的包                                 │
│    workspace 包不在 node_modules 中,不会自动预构建           │
│                                                            │
├──────────────────────────────────────────────────────────┤
│ exclude(排除预构建)                                       │
│                                                            │
│ 1. 本身已经是 ESM 格式且模块数量少的包                       │
│    预构建没有收益反而增加启动时间                              │
│                                                            │
│ 2. 需要在 Vite 插件中自定义处理的包                          │
│    预构建会绕过 Vite 插件链                                  │
│                                                            │
│ 3. 含有特殊 import 语法的包                                 │
│    esbuild 可能无法正确处理                                  │
└──────────────────────────────────────────────────────────┘

4.2 代码分割策略

生产构建时,合理的代码分割对性能至关重要:

ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes('node_modules')) {
            if (id.includes('react') || id.includes('react-dom') || id.includes('scheduler')) {
              return 'react-vendor'
            }
            if (id.includes('lodash') || id.includes('dayjs') || id.includes('axios')) {
              return 'lib-vendor'
            }
            if (id.includes('@ant-design') || id.includes('antd')) {
              return 'ui-vendor'
            }
            return 'vendor'
          }
        },
      },
    },
  },
})

常见的分割策略:

代码分割策略对比:

策略一:按更新频率分割
┌─────────────────────────────────────────┐
│ 框架(react/vue)   → framework.js       │  极少更新,强缓存
│ UI 库(antd)       → ui.js             │  偶尔更新
│ 工具库(lodash)    → lib.js            │  较少更新
│ 业务代码            → app-[hash].js     │  频繁更新
│ 路由页面            → page-[hash].js    │  按需加载
└─────────────────────────────────────────┘

策略二:按体积分割
┌─────────────────────────────────────────┐
│ 大于 200KB 的包单独成 chunk              │
│ 其余 node_modules 合并为 vendor          │
│ 业务代码自动按路由分割                    │
└─────────────────────────────────────────┘

策略三:按功能分割
┌─────────────────────────────────────────┐
│ 编辑器相关 → editor.js  (monaco 等)    │  仅编辑页加载
│ 图表相关   → chart.js   (echarts 等)   │  仅报表页加载
│ 地图相关   → map.js     (mapbox 等)    │  仅地图页加载
│ 核心功能   → core.js                    │  所有页面加载
└─────────────────────────────────────────┘

路由级别的代码分割(以 React 为例):

tsx
import { lazy, Suspense } from 'react'
import { createBrowserRouter } from 'react-router-dom'

const Home = lazy(() => import('./pages/Home'))
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Settings = lazy(() => import('./pages/Settings'))

const router = createBrowserRouter([
  {
    path: '/',
    element: (
      <Suspense fallback={<div>Loading...</div>}>
        <Home />
      </Suspense>
    ),
  },
  {
    path: '/dashboard',
    element: (
      <Suspense fallback={<div>Loading...</div>}>
        <Dashboard />
      </Suspense>
    ),
  },
  {
    path: '/settings',
    element: (
      <Suspense fallback={<div>Loading...</div>}>
        <Settings />
      </Suspense>
    ),
  },
])

Vite 会自动将 lazy(() => import(...)) 的模块分割为独立 chunk。

4.3 依赖外部化

对于 CDN 加载的库或服务端渲染不需要打包的依赖:

ts
export default defineConfig({
  build: {
    rollupOptions: {
      external: ['react', 'react-dom'],
      output: {
        globals: {
          react: 'React',
          'react-dom': 'ReactDOM',
        },
      },
    },
  },
})

配合 transformIndexHtml 钩子注入 CDN 链接:

ts
function cdnPlugin(externals: Record<string, string>): Plugin {
  return {
    name: 'vite-plugin-cdn',
    apply: 'build',

    transformIndexHtml(html) {
      const scripts = Object.values(externals)
        .map((url) => `<script src="${url}"></script>`)
        .join('\n    ')

      return html.replace('</head>', `    ${scripts}\n  </head>`)
    },
  }
}

4.4 构建分析

使用 rollup-plugin-visualizer 分析产物体积:

ts
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    visualizer({
      open: true,
      gzipSize: true,
      brotliSize: true,
      filename: 'stats.html',
    }),
  ],
})
构建分析产出 stats.html:

┌──────────────────────────────────────────────────┐
│                                                    │
│  ┌────────────────────┐  ┌──────────────────────┐ │
│  │                    │  │                      │ │
│  │   react-vendor     │  │    ui-vendor         │ │
│  │   142 KB (gzip)    │  │    238 KB (gzip)     │ │
│  │                    │  │                      │ │
│  │  ┌──────┐┌──────┐ │  │  ┌────────────────┐  │ │
│  │  │react ││r-dom │ │  │  │    antd         │  │ │
│  │  │ 42KB ││100KB │ │  │  │    238KB        │  │ │
│  │  └──────┘└──────┘ │  │  └────────────────┘  │ │
│  └────────────────────┘  └──────────────────────┘ │
│                                                    │
│  ┌────────────┐ ┌──────────┐ ┌────────────────┐  │
│  │ lib-vendor │ │ app      │ │  pages (lazy)  │  │
│  │  56 KB     │ │  32 KB   │ │  18 KB each    │  │
│  └────────────┘ └──────────┘ └────────────────┘  │
│                                                    │
└──────────────────────────────────────────────────┘

其他有用的构建优化配置:

ts
export default defineConfig({
  build: {
    target: 'es2015',

    minify: 'esbuild',

    cssMinify: 'esbuild',

    reportCompressedSize: false,

    assetsInlineLimit: 4096,

    chunkSizeWarningLimit: 500,

    sourcemap: false,
  },
})

4.5 SSR 支持

Vite 原生支持 SSR,提供了框架无关的底层 API:

ts
import express from 'express'
import { createServer as createViteServer } from 'vite'

async function createServer() {
  const app = express()

  const vite = await createViteServer({
    server: { middlewareMode: true },
    appType: 'custom',
  })

  app.use(vite.middlewares)

  app.use('*', async (req, res, next) => {
    const url = req.originalUrl

    try {
      let template = fs.readFileSync(
        path.resolve(__dirname, 'index.html'),
        'utf-8'
      )

      template = await vite.transformIndexHtml(url, template)

      const { render } = await vite.ssrLoadModule('/src/entry-server.tsx')

      const appHtml = await render(url)

      const html = template.replace('<!--ssr-outlet-->', appHtml)

      res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
    } catch (e) {
      vite.ssrFixStacktrace(e as Error)
      next(e)
    }
  })

  app.listen(5173)
}

createServer()
Vite SSR 架构:

┌──────────── 开发模式 ──────────────────────────────┐
│                                                      │
│  Express Server                                      │
│       │                                              │
│       ├── vite.middlewares(静态资源 + HMR)          │
│       │                                              │
│       └── SSR 路由处理                                │
│              │                                       │
│              ├── vite.transformIndexHtml()            │
│              │   处理 HTML 模板                       │
│              │                                       │
│              ├── vite.ssrLoadModule()                │
│              │   加载服务端入口(支持 HMR!)          │
│              │                                       │
│              └── render() → HTML 字符串               │
│                                                      │
└──────────────────────────────────────────────────────┘

┌──────────── 生产模式 ──────────────────────────────┐
│                                                      │
│  vite build                 → dist/client/           │
│  vite build --ssr           → dist/server/           │
│                                                      │
│  Express Server                                      │
│       │                                              │
│       ├── sirv('dist/client')  静态资源服务           │
│       │                                              │
│       └── import('dist/server/entry-server.js')      │
│              │                                       │
│              └── render() → HTML 字符串               │
│                                                      │
└──────────────────────────────────────────────────────┘

关键的 SSR 相关配置:

ts
export default defineConfig({
  ssr: {
    external: ['express'],
    noExternal: ['my-shared-lib'],
    target: 'node',
    format: 'esm',
  },
})
  • external:不打包的依赖,保持 require()/import 语句,由 Node.js 运行时解析
  • noExternal:强制打包的依赖(通常是 ESM only 的包需要转为 CJS 时使用)
  • target'node''webworker'

五、Vite vs 其他构建工具

5.1 全面对比表

维度ViteWebpackTurbopackRspack
开发启动速度⚡ 极快(毫秒级)🐌 慢(秒-十秒级)⚡ 极快⚡ 快(亚秒级)
HMR 速度⚡ 极快(模块级)🐌 较慢(chunk 级)⚡ 极快⚡ 快
生产构建速度🔵 中等(Rollup)🔵 中等🟡 快(Turbo 引擎)⚡ 快(Rust)
开发原理Native ESMBundleBundle(Turbo 引擎)Bundle(Rust)
生产原理RollupWebpackWebpack 兼容Webpack 兼容
语言实现JS + Go(esbuild)JSRustRust
配置复杂度🟢 简单🔴 复杂🟢 简单(Next.js 内置)🟡 中等(兼容 Webpack)
插件生态🟡 中等(Rollup 生态)🟢 最丰富🔴 较少(封闭在 Next.js)🟡 兼容 Webpack 插件
Loader/Plugin 兼容Rollup 插件兼容Webpack 专有Webpack 部分兼容Webpack 高度兼容
Tree-shaking🟢 优秀(Rollup)🟡 一般🟡 一般🟡 一般
CSS 处理内置完善需 loader 配置Next.js 内置需 loader 配置
TypeScript内置(esbuild)需 ts-loader/babel内置内置(SWC)
SSR 支持内置基础 API需额外配置Next.js 内置需额外配置
适用场景新项目首选存量大型项目Next.js 项目Webpack 项目迁移
成熟度🟢 成熟🟢 最成熟🟡 较新🟡 较新

5.2 如何选择

选择决策树:

新项目?
├── 是 → 使用 React/Vue/Svelte 等?
│        ├── 是 → Vite(首选)
│        └── Next.js 项目 → Turbopack(内置)

└── 否(存量项目迁移)→ 当前用 Webpack?
     ├── 是 → 迁移成本可接受?
     │        ├── 是 → Vite(完整迁移,收益大)
     │        └── 否 → Rspack(兼容 Webpack 配置,迁移成本低)

     └── 否 → 具体分析

5.3 Vite vs Webpack 深度对比

开发服务器架构对比:

Webpack Dev Server:
┌──────────────────────────────────────┐
│  entry.js                             │
│    ├── moduleA.js                     │
│    ├── moduleB.js                     │
│    │    ├── moduleC.js                │
│    │    └── moduleD.js                │
│    └── moduleE.js                     │
│                                       │
│  全部模块 → Webpack 编译 → Bundle      │
│                  ↓                    │
│           bundle.js (完整)            │
│                  ↓                    │
│           Dev Server 提供服务          │
└──────────────────────────────────────┘

Vite Dev Server:
┌──────────────────────────────────────┐
│  entry.js                             │
│    ├── moduleA.js  ← 请求时编译        │
│    ├── moduleB.js  ← 请求时编译        │
│    │    ├── moduleC.js  ← 可能不请求   │
│    │    └── moduleD.js  ← 可能不请求   │
│    └── moduleE.js  ← 请求时编译        │
│                                       │
│  浏览器 ESM → 按需请求 → 即时编译返回    │
│                                       │
│  只有实际访问到的模块才会被编译           │
└──────────────────────────────────────┘

HMR 更新粒度对比:

场景:修改了 moduleD.js

Webpack HMR:
moduleD.js 变化

重新编译包含 moduleD 的整个 Chunk
(可能包含 moduleB + moduleC + moduleD + 其他模块)

客户端下载整个更新后的 Chunk

替换旧 Chunk 中的模块
更新时间:~300ms - 2000ms(取决于 Chunk 大小)

Vite HMR:
moduleD.js 变化

仅重新编译 moduleD.js

WebSocket 通知:{ type: 'update', path: '/src/moduleD.js' }

客户端:import('/src/moduleD.js?t=timestamp')

仅下载和替换 moduleD 这一个模块
更新时间:~10ms - 50ms(几乎恒定)

5.4 Vite 的潜在问题

Vite 不是银弹,也有一些需要注意的问题:

Vite 的潜在挑战:

1. 开发/生产一致性
   ┌─────────────────────────────────────────┐
   │ 开发环境:esbuild + Native ESM           │
   │ 生产环境:Rollup Bundle                  │
   │                                           │
   │ 两套不同的编译流程可能导致:                 │
   │ - 开发正常但生产构建报错                    │
   │ - 依赖解析行为不一致                        │
   │ - CSS 处理差异                             │
   │                                           │
   │ 解决:CI 中始终运行 build 验证              │
   └─────────────────────────────────────────┘

2. 首屏请求瀑布流(大型项目)
   ┌─────────────────────────────────────────┐
   │ 首次访问页面时,浏览器需要逐级加载 ESM:     │
   │                                           │
   │ main.tsx                                  │
   │   → import App.tsx                        │
   │      → import Header.tsx                  │
   │         → import Button.tsx               │
   │            → import Icon.tsx              │
   │                                           │
   │ 每一级都是一个 HTTP 请求                    │
   │ 深层嵌套会导致首屏加载变慢                   │
   │                                           │
   │ 缓解:预构建 + HTTP/2 多路复用              │
   └─────────────────────────────────────────┘

3. CommonJS 兼容性
   ┌─────────────────────────────────────────┐
   │ 原生 ESM 环境下,CJS 模块需要预构建转换     │
   │                                           │
   │ 部分 CJS 包的动态 require 可能转换失败:     │
   │ const mod = require(condition ? 'a' : 'b')│
   │                                           │
   │ 解决:optimizeDeps.include 手动指定         │
   │ 或联系包作者发布 ESM 版本                   │
   └─────────────────────────────────────────┘

六、面试高频问题

问题一:Vite 为什么比 Webpack 快?底层原理是什么?

回答思路

分两个层面来解释——开发环境和构建工具选择。

开发环境:Webpack 采用 Bundle-based 架构,启动 dev server 前需要把所有模块打包成 bundle。项目越大,启动越慢。Vite 利用浏览器原生 ESM 能力,dev server 只需启动 HTTP 服务器,不做全量打包。浏览器发出 ESM 请求时才按需编译对应模块。因此启动时间几乎与项目规模无关。

构建工具选择:Vite 使用 esbuild(Go 编写)做依赖预构建和 TS/JSX 转译,比 Babel/tsc(JS 编写)快 10-100 倍。esbuild 利用多核并行和高效内存管理。

HMR 层面:Webpack 的 HMR 需要重新编译整个 Chunk 并传输,Vite 只需重新编译修改的单个模块文件,通过原生 ESM 动态 import 加载,更新速度恒定不受项目规模影响。

追问:那 Vite 有什么劣势?

开发和生产环境使用不同的构建工具(esbuild vs Rollup),可能存在行为不一致。首次访问大型页面时,深层 ESM 依赖链可能导致请求瀑布流。Vite 团队正在通过 Rolldown 项目解决开发/生产统一的问题。

问题二:Vite 的依赖预构建(Pre-Bundling)是什么?为什么需要它?

回答思路

预构建解决两个核心问题:

第一,格式兼容。npm 上大量包仍使用 CommonJS 格式(如 React),浏览器原生 ESM 无法直接执行 CJS 代码。esbuild 将 CJS/UMD 转为 ESM。

第二,减少请求数。某些 ESM 包内部有大量细碎模块(如 lodash-es 有 600+ 文件),如果每个都发起 HTTP 请求会严重影响性能。esbuild 将这些内部模块合并为单个文件。

预构建产物缓存在 node_modules/.vite/deps,通过 hash 比对 package.jsondependencies、lock 文件、Vite 配置来判断是否需要重新预构建。

追问:如果依赖预构建失败怎么办?

可以通过 optimizeDeps.include 强制指定需要预构建的包,用 optimizeDeps.exclude 排除有问题的包。删除 node_modules/.vite 目录可以强制重新预构建。也可以启动时加 --force 参数。

问题三:Vite 的插件机制是怎样的?和 Rollup 插件有什么关系?

回答思路

Vite 的插件 API 是 Rollup 插件 API 的超集。在通用构建钩子(resolveIdloadtransformbuildStartbuildEnd 等)上与 Rollup 完全兼容,很多 Rollup 插件可以直接在 Vite 中使用。

Vite 在此基础上扩展了一些独有钩子:config(修改配置)、configResolved(读取最终配置)、configureServer(配置 dev server,可添加自定义中间件)、transformIndexHtml(转换 HTML)、handleHotUpdate(自定义 HMR 处理)。

插件可以通过 enforce: 'pre' | 'post' 控制执行顺序,通过 apply: 'serve' | 'build' 控制生效环境。

追问:如何开发一个 Vite 虚拟模块插件?

使用 resolveId 钩子拦截特定的模块 ID(如 virtual:xxx),返回带 \0 前缀的 resolved ID(\0 是 Rollup 约定,表示虚拟模块,告诉其他插件不要处理)。然后在 load 钩子中匹配该 ID,返回动态生成的模块内容。

问题四:Vite 生产构建为什么用 Rollup 而不是 esbuild?

回答思路

esbuild 虽然极快,但在生产构建的某些关键能力上还不够成熟:

  1. 代码分割不够灵活,无法精细控制 chunk 的分割策略(如 manualChunks
  2. CSS 处理能力有限,不支持 CSS Code Splitting
  3. 插件生态不如 Rollup 丰富
  4. Tree-shaking 不如 Rollup 精确,Rollup 的 scope hoisting 可以生成更小的产物

Rollup 是专为 ESM 设计的打包器,Tree-shaking 最为精确,插件生态完善,产物质量高。Vite 选择 Rollup 是在构建速度和产物质量之间做的权衡。

未来 Rolldown(Rust 重写的 Rollup 兼容工具)可能会统一开发和生产构建。

问题五:Vite 的 HMR 是如何实现的?和 Webpack 有什么区别?

回答思路

核心区别在于更新粒度。

Webpack HMR:文件修改后,重新编译该文件所在的整个 Chunk,将增量更新补丁通过 WebSocket 发送给客户端。客户端下载新的 Chunk 并替换旧模块。项目越大、Chunk 越大,HMR 越慢。

Vite HMR:文件修改后,Vite 通过模块依赖图找到该模块的 HMR 边界(由 import.meta.hot.accept 定义)。仅重新编译该单一模块,通过 WebSocket 发送更新通知(只包含模块路径),客户端通过 ESM 动态 import 加上时间戳 query 参数请求新模块,实现模块级精确替换。更新速度与项目规模无关。

框架插件(如 @vitejs/plugin-react)会自动为组件注入 HMR accept 代码,开发者通常不需要手写 HMR API。

问题六:如何优化 Vite 项目的生产构建产物体积?

回答思路

  1. 代码分割:通过 build.rollupOptions.output.manualChunks 实现合理的 chunk 分割。按更新频率分离框架代码、UI 库、工具库和业务代码,最大化缓存利用率。路由级别使用 lazy() + import() 做按需加载。

  2. Tree-shaking:确保使用 ESM 格式的依赖(如用 lodash-es 替代 lodash)。避免 barrel files(index.tsexport * from ...)导致的 Tree-shaking 失效。

  3. 依赖外部化:大型不常变的依赖(如 React)通过 build.rollupOptions.external 外部化,走 CDN 加载。

  4. 压缩优化:使用 esbuild 或 terser 压缩。配合 vite-plugin-compression 生成 gzip/brotli 压缩产物。

  5. 分析产物:用 rollup-plugin-visualizer 可视化分析各 chunk 的体积分布,找出异常大的依赖进行针对性优化。

  6. 图片和资源:配置 build.assetsInlineLimit 控制小资源内联阈值。使用现代图片格式(WebP/AVIF)。

问题七:Vite 中如何处理环境变量?安全机制是什么?

回答思路

Vite 使用 .env 文件管理环境变量,通过 import.meta.env 访问。文件加载优先级为:.env.env.local.env.[mode].env.[mode].local,后加载的覆盖先加载的。

安全机制:只有以 VITE_ 前缀开头的变量才会暴露给客户端代码(通过 import.meta.env.VITE_XXX 访问)。不带 VITE_ 前缀的变量不会被注入客户端 bundle,防止数据库密码、API Secret 等敏感信息泄露。

vite.config.ts 中需要使用 loadEnv(mode, root) 来读取环境变量,因为配置文件在 Vite 初始化前执行,import.meta.env 还不可用。

define 配置项做的是编译时的字符串替换(类似 C 的宏),和环境变量不同,它在 bundle 中会被直接替换为字面量值。

问题八:Vite 开发环境和生产环境的差异可能导致哪些问题?如何规避?

回答思路

Vite 开发环境使用原生 ESM + esbuild,生产环境使用 Rollup 打包。两套不同的系统可能导致:

  1. 模块解析差异:开发时通过浏览器 ESM 逐个请求模块,生产时 Rollup 静态分析依赖图。某些动态 import 模式在开发正常但生产构建可能失败。

  2. CSS 处理差异:开发时 CSS 通过 JS 注入 <style> 标签,生产时提取为独立 CSS 文件。可能出现样式顺序不一致。

  3. esbuild vs Rollup 的 Tree-shaking 差异:esbuild 不做 Tree-shaking(开发不需要),Rollup 会做。某些有副作用的代码可能在生产被误删。

规避方案:

  • CI/CD 中必须运行 vite build 验证
  • 使用 vite preview 本地预览生产构建产物
  • 关注依赖的 sideEffects 声明
  • 对关键功能做 E2E 测试覆盖

七、延伸阅读

用心学习,用代码说话 💻