Vite 深入
概述
Vite(法语意为"快速",发音 /vit/)是由尤雨溪创建的现代前端构建工具,旨在解决开发环境下 Webpack 等传统打包工具存在的性能瓶颈。Vite 利用浏览器原生 ES Module(ESM)支持,在开发模式下实现近乎即时的模块热更新(HMR),同时在生产构建中集成了 Rollup 的强大打包能力。本文将从底层原理到高级配置,全面深入地剖析 Vite 的设计与实现。
ES Module 原理
浏览器原生 ESM
ES Module(ESM)是 ECMAScript 2015(ES6)正式定义的 JavaScript 模块系统。与传统 CommonJS 和 AMD 不同,ESM 是 JavaScript 语言规范的一部分,现代浏览器(Chrome 61+、Firefox 60+、Safari 16.4+)已原生支持通过 <script type="module"> 加载模块。
浏览器原生 ESM 的核心行为:
<script type="module">
import { createApp } from 'vue'
// 浏览器会发起 HTTP 请求获取 vue 模块
</script>原生 ESM 的加载流程:
- 解析:浏览器解析 HTML,遇到
<script type="module">时,下载并解析入口模块 - 构建依赖图:递归解析
import语句,构建完整的模块依赖图 - 下载:并行下载所有静态
import引用的模块文件 - 执行:按照依赖顺序深度优先执行模块代码
ESM 的关键特性:
- 静态结构:
import和export必须在模块顶层,不能在条件语句或函数内部使用。这一限制使得构建工具可以在编译时确定依赖关系,实现 tree-shaking - 严格模式:ESM 默认在严格模式下执行,无需显式声明
"use strict" - 模块作用域:每个模块拥有独立作用域,变量不会泄漏到全局
- 循环依赖:ESM 支持循环依赖,通过"实时绑定"(live binding)机制处理
Import Maps
Import Maps 是一项浏览器标准,允许页面控制 JavaScript import 说明符(specifier)的解析方式。Vite 开发模式下大量使用 Import Maps 来映射第三方依赖的路径。
<script type="importmap">
{
"imports": {
"vue": "/node_modules/.vite/deps/vue.js?v=abc123",
"vue/server-renderer": "/node_modules/.vite/deps/vue_server_renderer.js?v=abc123",
"pinia": "/node_modules/.vite/deps/pinia.js?v=abc123"
}
}
</script>Import Maps 解决了裸模块导入(bare module import)的问题。在 Node.js 环境中,import { ref } from 'vue' 中的 'vue' 会通过 node_modules 解析路径;但在浏览器中,这种裸说明符没有意义。Vite 在开发服务器中自动生成 Import Maps,将裸模块说明符映射为预构建后的可访问 URL。
开发模式编译
Vite 开发模式并不执行完整的打包,而是利用浏览器原生 ESM 按需加载。当浏览器请求一个模块时,Vite 开发服务器会:
- 接收请求:浏览器请求入口模块(如
main.ts) - 编译转换:Vite 对模块进行即时编译——将 TypeScript 转译为 JavaScript、将 Vue SFC 编译为渲染函数、将 JSX 转换为
React.createElement - 返回单文件:返回编译后的单个模块文件,不对其进行打包
- 浏览器侧加载:浏览器解析返回的文件,发现其中的
import语句后,继续发起新的 HTTP 请求 - 循环上述过程:直到所有依赖模块被加载
这一机制意味着:只有当前页面实际用到的模块才会被编译和传输,而不是像 Webpack 那样在启动时打包全部代码。这就是 Vite 冷启动极快和按需编译的核心原因。
Vite 开发服务器
按需编译
Vite 开发服务器基于一个简单的原则——浏览器需要什么就编译什么。当浏览器请求一个文件时,Vite 使用 ESBuild 或 Babel 对该文件进行即时转换,而不会预先打包整个应用。
这种按需编译(on-demand compilation)的工作流程:
- 浏览器请求
src/main.ts - Vite 检查文件是否需要转换(TypeScript、Vue、JSX 等)
- 使用 ESBuild 快速转译该文件(ESBuild 比传统工具快 10-100 倍)
- 返回转换后的 JavaScript 内容
- 浏览器解析后遇到
import,发起下一个请求 - 重复直到所有模块加载完成
相比之下,Webpack 在启动时需要从入口开始遍历整个依赖图,将所有模块打包成 bundle(即使页面尚未渲染任何内容),然后才能响应浏览器请求。这就是为何 Webpack 在大型项目中冷启动可能需要数十秒,而 Vite 几乎瞬间完成。
预构建
虽然 Vite 开发模式不打包应用代码,但会对 第三方依赖 进行预构建(pre-bundling)。这是 Vite 启动时的必要步骤,原因有二:
1. 将 CommonJS 转换为 ESM
绝大多数 npm 包仍然使用 CommonJS 格式(require / module.exports),浏览器无法直接识别。Vite 使用 ESBuild 将 CJS 依赖转换为 ESM 格式。
2. 合并小模块以减少请求数量
部分第三方包(如 Lodash、Ant Design)由大量小模块组成,如果直接依赖原生 ESM,浏览器可能需要发起数百甚至数千个 HTTP 请求来加载所有小模块,严重影响页面加载性能。Vite 通过预构建将这些小模块合并为少量 chunk,大幅减少请求数。
预构建发生在两个时机:
- 首次冷启动时:Vite 启动后会立即扫描入口文件的依赖,对裸导入的第三方依赖进行预构建
- 依赖变更时:当
package.json的依赖发生变化时,会触发重新预构建
预构建产物默认存储在 node_modules/.vite/deps/ 目录中,包含:
node_modules/.vite/deps/
vue.js # 预构建后的 Vue 核心
vue.js.map # Source map
pinia.js # 预构建后的 Pinia
_metadata.json # 预构建的元数据(版本、hash 等)依赖缓存
Vite 使用多层缓存策略来提升性能:
HTTP 缓存:Vite 对预构建的依赖文件设置强缓存头 Cache-Control: max-age=31536000, immutable,因为依赖的 URL 包含内容 hash,内容不变时 URL 不变,浏览器会永久缓存。
文件系统缓存:预构建产物缓存于 node_modules/.vite/deps/。Vite 通过以下条件判断缓存是否有效:
package.json中依赖的版本是否变化package-lock.json/yarn.lock/pnpm-lock.yaml是否变化- Vite 配置文件是否变化
- 预构建时使用的
optimizeDeps配置是否变化
Vite 内部模块图缓存:开发服务器维护一个模块图(Module Graph),记录所有已处理模块的转换结果和依赖关系。只有当模块文件内容发生变化时才会重新编译该模块及其受影响的子树。
ESBuild 加速
ESBuild 是 Vite 开发模式下各种编译操作的底层引擎,由 Go 语言编写,利用并行处理和高效的内存管理实现了极致的编译速度。
Vite 使用 ESBuild 的场景:
| 场景 | 说明 | 性能优势 |
|---|---|---|
| 依赖预构建 | CJS -> ESM 转换,模块合并 | 比 Webpack 的打包快 10-100 倍 |
| TypeScript 转译 | .ts 文件去掉类型注解(不进行类型检查) | 仅类型擦除,瞬间完成 |
| JSX/TSX 编译 | JSX 语法转换为 React.createElement | 高速转译 |
| 压缩(实验性) | 可在构建阶段使用 ESBuild 压缩 | 比 Terser 快 20 倍 |
需要注意的是,ESBuild 只做转译(transpile)不做类型检查(type checking)。类型检查工作应由 IDE 或单独的 tsc --noEmit 命令完成。这是 Vite 处理 TypeScript 的核心设计理念——将类型检查与编译分离,让开发服务器专注于最快的转译路径。
HMR 机制
WebSocket 通信
Vite 的热模块替换(Hot Module Replacement, HMR)基于 WebSocket 实现客户端与开发服务器之间的实时通信。
整体通信流程:
- 建立连接:Vite 开发服务器启动时,创建一个 WebSocket 服务端。客户端代码(
@vite/client)注入到页面中,自动与服务器建立 WebSocket 连接 - 文件监听:Vite 使用
chokidar(基于fs.watch的文件监听库)监听项目文件变化 - 变更通知:当文件发生变化时,服务器通过 WebSocket 向客户端发送消息,格式为:json
{ "type": "update", "path": "/src/components/Counter.vue", "changeSrcPath": "/src/components/Counter.vue", "timestamp": 1678901234567 } - 客户端处理:浏览器端的 HMR runtime 接收到消息后,判断如何处理变更
模块热替换链
当 Vite 检测到某个模块文件发生变化时,会触发一个有序的替换链:
- 找到受影响的模块:开发服务器维护的模块图中记录了每个模块的所有父模块(importer)
- 判断是否接受 HMR:检查模块自身或其父链上的模块是否调用了
import.meta.hot.accept - 沿依赖链冒泡:如果当前模块未调用
accept,HMR 事件会向上冒泡到它的父模块,直到找到能处理该事件的模块 - 到达根模块:如果冒泡到根模块(即入口文件)仍未找到
accept处理,则执行全量页面刷新(location.reload())
文件变更
│
▼
模块 /src/components/Button.tsx 变更
│
├─ 自身有 accept() → 仅替换该模块,保留状态
│
└─ 自身无 accept()
│
▼
父模块 /src/pages/Home.tsx
│
├─ 有 accept() → 替换 Home 及其子模块
│
└─ 无 accept()
│
▼
根模块 /src/main.tsx
│
└─ 未处理 → 全量刷新import.meta.hot.accept 处理
import.meta.hot 是 Vite 在开发环境下注入的 HMR API。核心方法 accept 告诉 Vite 当前模块可以接受热替换:
接受自身更新:
export const count = ref(0)
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
// 模块更新后的回调
console.log('模块已更新:', newModule)
})
}接受特定依赖的更新:
import { foo } from './foo'
import { bar } from './bar'
if (import.meta.hot) {
import.meta.hot.accept(['./foo', './bar'], ([newFoo, newBar]) => {
// 当 foo 或 bar 更新时执行
foo = newFoo.foo
bar = newBar.bar
})
}清理副作用:
if (import.meta.hot) {
import.meta.hot.dispose(() => {
// 在模块被替换前执行清理
// 如移除事件监听器、清除定时器等
clearInterval(timer)
element.removeEventListener('click', handler)
})
}React Fast Refresh
React 官方推荐的 HMR 方案是 React Fast Refresh(RFC),Vite 通过 @vitejs/plugin-react 插件提供支持。它与传统的 React Hot Loader 有本质区别:
React Fast Refresh 的核心规则:
- 仅保留函数组件的状态:当只修改函数组件的代码时,React 保持组件内部的状态(
useState、useReducer的结果) - 不保留 Class 组件的状态:Class 组件更新会触发完整重渲染
- 保留 React 组件树中的状态:如果编辑的文件只导出 React 组件,App 中的其他组件不受影响
- 对非 React 导出进行全量刷新:如果模块还导出其他非组件内容(如自定义 Hook、工具函数),为保证正确性,Vite 在编辑后会对该模块进行全量刷新
工作流程:
- 检测到文件变化,WebSocket 发送
update消息 - Vite HMR runtime 判断是否为 React 组件(基于文件名或导出内容)
- 如果是 React 组件,触发 React Fast Refresh 以保留状态
- React Fresh runtime 执行"安全替换"——保留组件内部状态,仅更新渲染函数
- 如果 React Fast Refresh 无法处理(如 Class 组件、Hook 顺序变化),降级为模块级 HMR 或全量刷新
Vue HMR
Vue 3 的 HMR 支持内置于 @vitejs/plugin-vue 插件中,基于 Vue 3 编译器对 SFC 单文件组件的特殊处理。
Vue SFC(Single File Component)的热替换分为三个部分:
- 模板(
<template>)更新:仅重新编译模板部分,组件实例和状态完全保留 - 样式(
<style>)更新:通过 CSS 热更新,甚至不需要 JS 模块替换 - 脚本(
<script>)更新:重新执行组件的 setup 函数,保留组件实例的 DOM(通过 Vue 3 的渲染副作用实现)
Vue 3 编译器在编译 SFC 时,会将 setup 函数提取到一个可被 HMR 处理的导出中:
// 编译后的 SFC(简化)
import { defineComponent, ref } from 'vue'
import { createHotContext } from '/@vite/client'
const __VITE_HMR__ = createHotContext('/src/components/Counter.vue')
const _sfc_main = defineComponent({
setup() {
const count = ref(0)
return { count }
}
})
// Vite HMR 自动注入 accept 处理
__VITE_HMR__.accept((mod) => {
// 使用 Vue 3 的 __hmrId 进行热替换
// 保持组件实例的 DOM 元素
__VUE_HMR_RUNTIME__.reload('/src/components/Counter.vue', mod.default)
})Rollup 插件体系
插件钩子
Vite 的插件体系基于 Rollup 的插件接口,并在此基础上扩展了 Vite 特有的钩子。这种设计使得大多数 Rollup 插件可以直接在 Vite 中使用。
Rollup 插件钩子分为以下几个阶段:
构建阶段(Build Hooks):
| 钩子名称 | 类型 | 说明 |
|---|---|---|
options | async/sync | 读取和修改 Rollup 配置选项 |
buildStart | async | 构建开始,可以访问选项 |
resolveId | async | 解析模块导入路径,返回模块 ID |
load | async | 加载模块内容,返回源码 |
transform | async | 转换模块内容(核心 Hook,用于编译) |
moduleParsed | async | 模块解析完成后调用 |
buildEnd | async | 构建完成,所有模块已处理 |
closeBundle | async | 关闭 bundle,用于清理 |
输出生成阶段(Output Generation Hooks):
| 钩子名称 | 类型 | 说明 |
|---|---|---|
outputOptions | async/sync | 读取和修改输出配置 |
renderStart | async | 生成输出开始 |
banner/footer | async/sync | 在输出文件头/尾添加内容 |
intro/outro | async/sync | 在输出内部代码头/尾添加内容 |
renderDynamicImport | async | 自定义动态导入渲染 |
augmentChunkHash | sync | 增加 chunk 的 hash 值 |
resolveFileUrl | sync | 解析 import.meta.ROLLUP_FILE_URL_* |
resolveImportMeta | sync | 解析 import.meta 属性 |
renderChunk | async | 转换单个 chunk 的内容 |
generateBundle | async | 写入输出文件前调用,可在此修改输出 |
writeBundle | async | 写入所有文件后调用 |
closeBundle | async | 关闭 bundle |
Vite 专属钩子
Vite 在 Rollup 插件接口基础上扩展了以下专属钩子:
| 钩子名称 | 说明 |
|---|---|
config | 在解析 Vite 配置前调用,可修改配置 |
configResolved | Vite 配置解析完成后调用 |
configureServer | 用于配置开发服务器,可添加自定义中间件 |
configurePreviewServer | 用于配置预览服务器 |
transformIndexHtml | 转换 index.html 文件 |
handleHotUpdate | 自定义 HMR 更新行为,可过滤或修改更新模块 |
handleHotUpdate 是 Vite 特有的重要钩子,允许插件精细控制 HMR 行为:
function myPlugin() {
return {
name: 'my-hmr-plugin',
handleHotUpdate({ file, server, modules, timestamp }) {
// file: 变更的文件路径
// server: Vite 开发服务器实例
// modules: 受影响的模块数组
// timestamp: 变更时间戳
// 可以过滤模块,只处理特定文件
if (file.endsWith('.css')) {
// CSS 变更走 CSS HMR 通道
return []
}
// 返回自定义模块列表
return modules.filter(mod => mod.id !== 'unnecessary-module')
}
}
}Rollup 兼容
Vite 的插件系统设计为最大化兼容 Rollup 生态。本质上,Vite 在开发模式下的文件转换和模块处理使用了 Rollup 的插件管道,这意味着:
- 大多数 Rollup 插件无需修改即可在 Vite 中使用
- 插件的
resolveId、load、transform钩子在开发和生产模式中行为一致 - 输出相关钩子(
renderChunk、generateBundle等)仅在vite build时触发
兼容性注意事项:
- 使用
this.emitFile的 Rollup 插件需要测试,Vite 的 dev server 对 emit 文件的支持有限 - 修改 bundle 输出的插件仅在
vite build时生效 - 依赖 Node.js 文件系统的 Rollup 插件可能在浏览器端不适用
- Vite 插件的
enforce: 'pre'和enforce: 'post'机制与 Rollup 一致
完整的 Vite 插件编写示例:
// Vite 插件工厂函数
function vitePluginExample(options = {}) {
return {
// 插件名称,必须唯一
name: 'vite-plugin-example',
// 执行顺序:pre / normal(默认)/ post
enforce: 'pre',
// —— Vite 独有钩子 ——
config(config, { command, mode }) {
// 修改 Vite 配置
return {
define: {
__CUSTOM__: JSON.stringify(options.value)
}
}
},
configResolved(resolvedConfig) {
// 获取最终解析后的配置
console.log('构建模式:', resolvedConfig.mode)
},
configureServer(server) {
// 添加自定义中间件
server.middlewares.use((req, res, next) => {
console.log(`[请求] ${req.url}`)
next()
})
},
transformIndexHtml(html) {
// 向 index.html 注入内容
return html.replace('</head>', '<link rel="stylesheet" href="/custom.css"></head>')
},
handleHotUpdate(ctx) {
// 自定义 HMR 行为
},
// —— Rollup 通用钩子 ——
resolveId(source, importer) {
// 自定义模块解析
},
load(id) {
// 加载模块
},
transform(code, id) {
// 转换模块内容
},
buildEnd() {
// 构建结束
}
}
}环境变量与模式
import.meta.env
Vite 在编译时将 import.meta.env 对象注入到代码中,其中包含环境变量信息。这些环境变量在构建时被静态替换为实际值,而非运行时动态获取。
import.meta.env 内置属性:
| 属性 | 类型 | 说明 |
|---|---|---|
import.meta.env.MODE | string | 当前运行模式,如 development、production |
import.meta.env.BASE_URL | string | 部署基础 URL,对应 Vite 配置中的 base 选项 |
import.meta.env.PROD | boolean | 是否为生产环境(使用 vite build 构建) |
import.meta.env.DEV | boolean | 是否为开发环境(使用 vite 启动) |
import.meta.env.SSR | boolean | 是否运行在服务端渲染环境中 |
Vite 使用 define 配置实现环境变量的静态替换,从编译层面确保未使用的环境变量会被 tree-shake 移除。例如,构建工具会将 import.meta.env.DEV 直接替换为 true 或 false,在 production build 中,永远不会执行的代码分支会被自动消除。
.env 文件
Vite 使用 .env 文件加载环境变量,遵循 dotenv 规范。文件的加载优先级从高到低为:
.env.local(本地覆盖,不提交到 git).env.[mode](特定模式,如.env.production).env.[mode].local(特定模式本地覆盖).env(通用默认值)
Vite 的 env 文件示例:
# .env — 所有模式共享
VITE_APP_TITLE=我的应用
VITE_API_BASE_URL=/api
# .env.development — 仅在 development 模式使用
VITE_APP_TITLE=我的应用(开发版)
VITE_API_BASE_URL=http://localhost:3000/api
# .env.production — 仅在 production 模式使用
VITE_APP_TITLE=我的应用(正式版)
VITE_API_BASE_URL=https://api.example.com重要规则:Vite 仅暴露以 VITE_ 为前缀的环境变量到客户端代码中,以防止意外暴露敏感信息。在配置中使用 define 或 loadEnv 可以访问所有环境变量。
在代码中使用:
console.log(import.meta.env.VITE_APP_TITLE) // 输出:我的应用
console.log(import.meta.env.VITE_API_BASE_URL) // 输出对应模式的值在 vite.config.ts 中使用:
import { defineConfig, loadEnv } from 'vite'
export default defineConfig(({ command, mode }) => {
// 加载所有环境变量(包括非 VITE_ 前缀的)
const env = loadEnv(mode, process.cwd(), '')
return {
define: {
__APP_VERSION__: JSON.stringify(env.APP_VERSION)
}
}
})模式切换
Vite 的模式(mode)机制允许项目在不同环境下使用不同的配置和行为。
- 默认模式:
vite dev或vite serve默认development;vite build默认production - 自定义模式:通过
--mode参数指定任意模式,如vite build --mode staging
模式切换触发的行为:
- 加载对应的 env 文件:加载
.env.[mode]文件 - 配置函数接收 mode:
defineConfig(({ mode }) => { ... })中可以根据 mode 应用不同配置 - 条件编译:
import.meta.env.MODE可用于条件编译 - 插件可以感知模式:插件钩子中可以获取当前模式
export default defineConfig(({ command, mode }) => ({
// 根据模式应用不同配置
define: {
__DEV__: mode === 'development',
},
build: {
sourcemap: mode === 'development' ? true : false,
minify: mode === 'production' ? 'esbuild' : false,
}
}))Vite 配置深入
resolve.alias
resolve.alias 用于为模块导入路径创建别名,是大型项目中管理路径的必备配置。Vite 内部使用 @rollup/plugin-alias 实现此功能。
import { defineConfig } from 'vite'
import { fileURLToPath, URL } from 'node:url'
export default defineConfig({
resolve: {
alias: {
// 方式一:使用对象
'@': fileURLToPath(new URL('./src', import.meta.url)),
'@components': fileURLToPath(new URL('./src/components', import.meta.url)),
// 方式二:使用数组(支持更复杂的匹配)
// {
// find: /^@icons\/(.*)/,
// replacement: fileURLToPath(new URL('./src/assets/icons/$1', import.meta.url))
// }
}
}
})配置别名后,在代码中可以使用简洁的路径:
// 之前
import Button from '../../../components/Button.vue'
// 之后
import Button from '@/components/Button.vue'
// 或
import Button from '@components/Button.vue'注意:如果你使用 TypeScript,还需要在 tsconfig.json 的 paths 中同步配置别名,以便 IDE 和类型检查器正确识别:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"]
}
}
}plugins
plugins 配置接受一个 Vite/Rollup 插件数组。Vite 官方和社区提供了丰富的插件生态:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import react from '@vitejs/plugin-react'
import legacy from '@vitejs/plugin-legacy' // 传统浏览器兼容
export default defineConfig({
plugins: [
vue(), // Vue 3 SFC 支持
vueJsx(), // Vue JSX/TSX 支持
react(), // React Fast Refresh 支持
legacy({
targets: ['defaults', 'not IE 11']
})
]
})插件数组中的 false 值会被 Vite 自动忽略,这有利于条件启用插件:
plugins: [
vue(),
// 仅在分析模式(ANALYZE=true)下启用
process.env.ANALYZE === 'true' && visualizer()
].filter(Boolean)插件的执行顺序由 enforce 属性控制:
enforce: 'pre':在核心插件之前执行(如代码转换)- 无
enforce:在 pre 和 post 之间执行 enforce: 'post':在核心插件之后执行(如压缩处理)
build
build 配置控制生产构建的行为,Vite 内部使用 Rollup 执行打包:
export default defineConfig({
build: {
// 输出目录,默认 dist
outDir: 'dist',
// 资源文件存放目录
assetsDir: 'assets',
// 生成 sourcemap
sourcemap: false,
// 压缩方式:'esbuild'(默认,极快)| 'terser'(更小体积)| false
minify: 'esbuild',
// 目标浏览器
target: 'modules', // 默认,支持原生 ESM 的浏览器
// chunk 大小警告阈值(kb)
chunkSizeWarningLimit: 500,
// 库模式配置
lib: {
entry: 'src/index.ts',
name: 'MyLib',
formats: ['es', 'umd']
},
// Rollup 配置
rollupOptions: {
// 外部化依赖
external: ['vue', 'react'],
output: {
// 自定义 chunk 拆分
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('lodash')) return 'vendor-lodash'
if (id.includes('antd')) return 'vendor-antd'
return 'vendor'
}
}
}
}
}
})css
Vite 对 CSS 的处理有完善的配置支持:
export default defineConfig({
css: {
// CSS 模块配置
modules: {
localsConvention: 'camelCaseOnly', // 导入时只生成驼峰形式
scopeBehaviour: 'local', // local | global
generateScopedName: '[name]__[local]___[hash:base64:5]' // 自定义类名格式
},
// CSS 预处理器配置,传递给对应预处理器
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/variables.scss";`, // 全局注入
api: 'modern-compiler' // 使用现代 SCSS 编译器
},
less: {
javascriptEnabled: true,
modifyVars: {
'primary-color': '#1890ff'
}
}
},
// PostCSS 配置(也可通过 postcss.config.js 文件)
postcss: {
plugins: [
require('autoprefixer'),
require('tailwindcss')
]
}
}
})optimizeDeps
optimizeDeps 控制依赖预构建行为:
export default defineConfig({
optimizeDeps: {
// 强制预构建的依赖包列表
include: ['lodash-es', 'dayjs'],
// 排除预构建的依赖(这些包已经被正确打包为 ESM)
exclude: ['@vueuse/core'],
// 强制重新预构建
force: true,
// 预构建时的 ESBuild 选项
esbuildOptions: {
target: 'es2020',
define: {
global: 'globalThis'
}
}
}
})当第三方包变更(如通过 npm link 链接本地包)或预构建缓存出现异常时,合理使用 optimizeDeps.include 和 force: true 可以解决大部分依赖相关问题。
Vite vs Webpack 对比
开发模式架构差异
| 对比维度 | Vite | Webpack |
|---|---|---|
| 核心思路 | 利用浏览器原生 ESM,不打包应用代码 | 从入口出发,打包整个应用为 bundle |
| 冷启动 | 几乎瞬间(毫秒级),只需启动服务器 + 预构建 | 较慢(数秒到数十秒),需遍历整个依赖图打包 |
| 模块编译 | 按需编译,仅编译浏览器请求的文件 | 启动时全量编译 |
| HMR 性能 | 仅替换变更模块,不重新打包 | 模块变更后需重新构建 chunk(即使使用增量构建) |
| 内存占用 | 低,不维护打包产物 | 高,维护整个 bundle |
| 大型项目 | 优势明显,启动速度和 HMR 不受代码量影响 | 代码量越大,启动和 HMR 越慢 |
构建模式差异
| 对比维度 | Vite (Rollup) | Webpack |
|---|---|---|
| 打包器 | Rollup(专注 ES 模块打包) | 自研打包器 |
| Tree-shaking | 原生支持,基于 ES Module 静态分析 | 支持(TerserPlugin),依赖副作用标记 |
| 代码分割 | 基于 Rollup 的分割策略 | SplitChunksPlugin |
| 输出优化 | 默认 ES2020 输出 | 默认 ES5 兼容(需配置 target) |
| 插件生态 | Rollup 插件 + Vite 插件 | Webpack Plugin + Loader |
| 构建速度 | 较快,对中小型项目尤其快 | 相对较慢,但通过缓存策略可优化 |
配置复杂度
Vite 配置(最少化):
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()]
})Webpack 配置(最少化 Vue 项目):
// webpack.config.js
const { VueLoaderPlugin } = require('vue-loader')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
module: {
rules: [
{ test: /\.vue$/, use: 'vue-loader' },
{ test: /\.js$/, exclude: /node_modules/, use: 'babel-loader' },
{ test: /\.css$/, use: ['style-loader', 'css-loader'] }
]
},
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({ template: './index.html' })
]
}可见,Vite 的配置显著简化。对于常见的框架和功能,Vite 提供了官方插件,开箱即用;而 Webpack 需要手动配置各种 loader 和 plugin。
生态与迁移
| 维度 | Vite | Webpack |
|---|---|---|
| 稳定性 | 成熟,2.x 版本起广泛用于生产 | 极其成熟,大规模生产验证多年 |
| 生态丰富度 | 持续增长中,主流框架均已支持 | 最丰富,几乎所有需求都有现成方案 |
| 自定义能力 | 适中,通过插件和配置可满足大部分需求 | 极高,几乎可以定制打包的每个细节 |
| 从 Webpack 迁移 | 大多数情况下可以直接替换,需要调整 plugins 和 loaders 配置 | — |
| 社区支持 | 快速增长的社区,Vue/Nuxt/SvelteKit 等框架默认使用 | 最大的社区,海量教程和解决方案 |
选择建议
- 新项目推荐 Vite:几乎所有现代前端框架(Vue、React、Svelte、Solid)都推荐或默认使用 Vite
- Webpack 无需强行迁移:如果已有项目在 Webpack 上运行良好且没有性能问题,不必急于迁移
- 大型遗留项目:Webpack 的成熟生态和丰富的自定义能力仍然是复杂构建场景的保险选择
- 库/工具开发:Vite 的库模式(
build.lib)配合 Rollup 的打包能力,是开发 npm 包的优秀选择
总结
Vite 通过利用浏览器原生 ESM 从根本上改变了前端开发服务器的工作方式。在开发模式下,按需编译和基于 ESBuild 的预构建带来了近乎即时的冷启动和无与伦比的 HMR 性能;在生产模式下,基于 Rollup 的构建保证了优秀的打包质量和 Tree-shaking 效果。结合直观的配置设计和丰富的插件生态,Vite 已经成为现代前端开发的事实标准。
随着前端生态的持续演进,Vite 正在从单纯的构建工具演变为更广泛的前端开发平台——Vite 插件系统、Vite 驱动的框架(Nuxt、SvelteKit、Astro)以及 Vite 核心功能的持续进化,都在巩固其在前端工程化领域的核心地位。
参考资源
在线演示
以下 Demo 通过可视化方式展示 Vite HMR 热更新流程,帮助理解模块热替换的工作机制: