一、Vite 适用前提
Vite 适合解决的,本质上不是“怎么把前端项目跑起来”,而是“怎么让现代前端项目跑得更快、改得更顺手、构建得更轻”。如果你的项目已经进入 ESM、组件化、工程化阶段,那么 Vite 往往是非常自然的选择。
更具体地说,以下场景通常非常适合 Vite:
- 开发现代前端应用,如 Vue、React、Svelte、Solid 等项目。
- 项目依赖较多、页面较多,传统打包工具的冷启动和热更新已经开始拖慢开发节奏。
- 团队希望减少配置成本,让项目尽快进入业务开发阶段。
- 项目运行在 Monorepo 中,需要和 pnpm Workspaces、Turborepo、共享包协同工作。
- 需要同时兼顾开发阶段的极致速度和生产阶段的稳定构建产物。
以下场景则建议先评估,再决定是否采用:
- 项目必须兼容 IE11 这类传统浏览器,且无法接受额外兼容成本。
- 依赖大量历史遗留的 CommonJS、UMD 或浏览器全局脚本包,迁移成本较高。
- 已经深度绑定 Webpack 专属 loader、plugin 或内部构建链,替换的收益未必大于改造成本。
- 项目本身并不属于现代前端工程体系,例如老旧 jQuery 项目、非模块化项目。
一句话概括:Vite 不是“适合所有前端项目”,但它非常适合现代前端项目。
二、Vite 简介
Vite 由尤雨溪发起,核心目标是提升现代前端开发体验。它最重要的设计思路有两个:
- 开发环境尽量不做整包打包,而是利用浏览器原生 ESM 做按需加载。
- 生产环境交给成熟的打包器完成优化,保证上线产物的稳定性和性能。
传统工具在开发时,往往需要先把整个依赖图打包一遍,项目越大,等待越久。Vite 则把这件事拆开了:
- 对第三方依赖做预构建,避免浏览器一次次解析庞杂的依赖树。
- 对业务源码按请求即时转换,浏览器请求到哪个模块,服务端就处理哪个模块。
- 当文件变更时,只让受影响的模块热更新,而不是重新打整个包。
这让 Vite 在开发阶段通常表现为:冷启动很快、HMR 很快、配置更少、对主流框架支持成熟。
同时,Vite 并不是只管“开发快”,它在生产环境下会走完整构建流程,支持代码分割、资源压缩、Tree-Shaking、静态资源处理等工程化能力。因此它更准确的定位不是“只适合本地开发的小工具”,而是一个完整的现代前端构建方案。
Vite 核心特性
mindmap
root((Vite))
开发体验
秒级冷启动
精准 HMR
原生 ESM
工程能力
插件体系
环境变量
代理转发
静态资源处理
生产构建
Rollup 打包
Tree Shaking
代码分割
资源压缩
协作场景
Monorepo
共享包
多应用
- 极速冷启动:开发服务器启动时,不需要先把整个应用完整打包。
- 高效 HMR:模块变更后尽量只更新受影响部分,反馈更快。
- 原生 ESM 支持:开发期直接拥抱现代浏览器模块能力。
- 插件体系清晰:既能使用 Vite 插件,也能复用一部分 Rollup 插件能力。
- 生产构建稳定:上线阶段仍然具备成熟打包优化能力,而不是把开发模式直接搬到生产环境。
- Monorepo 友好:在多应用、多共享包协作中表现自然。
三、Vite 与其他前端构建工具对比
Vite 并不是为了“全面取代所有工具”而出现的,它更像是在现代前端语境下,对开发速度与工程复杂度做了一次重新平衡。
| 工具 | 核心定位 | 优势 | 劣势 | 更适合的场景 |
|---|---|---|---|---|
| Vite | 现代前端开发与构建工具 | 冷启动快、HMR 快、配置轻、开发体验好 | 对老旧生态兼容成本较高 | Vue、React、Monorepo、现代 Web 应用 |
| Webpack | 高可定制的全能构建工具 | 插件生态深、定制能力强、兼容历史项目能力好 | 配置复杂、开发期速度通常偏慢 | 历史项目、深度定制构建链、大型存量工程 |
| Parcel | 零配置构建工具 | 上手简单、适合原型验证 | 大型项目可控性与生态相对弱一些 | 小型项目、快速试验 |
四、Vite 快速上手
这一部分先解决“怎么把项目跑起来”,再顺手把几个最常见的坑放进对应步骤里说明,避免读者先照着命令敲,后面再回头查问题。
1. 前置准备
建议先确认两件事:
- 使用受当前 Vite 版本支持的 Node.js 版本,优先选择当前 LTS。
- 包管理器保持团队一致,本文以 pnpm 为例。
# 查看 Node.js 版本
node -v
# 查看 pnpm 版本
pnpm -v
如果后续运行时报出与 ESM、依赖解析或语法转换有关的问题,第一步通常不是急着改配置,而是先确认 Node.js 版本是否匹配。
2. 创建项目
# 交互式创建
pnpm create vite@latest
# 创建 Vue 项目
pnpm create vite@latest my-vite-vue -- --template vue
# 创建 Vue + TypeScript 项目
pnpm create vite@latest my-vite-vue-ts -- --template vue-ts
# 创建 React 项目
pnpm create vite@latest my-vite-react -- --template react
# 创建 React + TypeScript 项目
pnpm create vite@latest my-vite-react-ts -- --template react-ts
创建完成后,模板通常已经帮你准备好了基础的 vite.config.*、入口文件和启动脚本。大多数情况下,不需要一上来就改配置,先跑起来再说,效率反而更高。
3. 安装依赖并启动开发服务
cd my-vite-vue
pnpm install
pnpm dev
默认情况下,Vite 会启动本地开发服务器,并输出访问地址。项目启动后,修改源码通常会立即在浏览器中体现。
如果你发现“代码改了但页面没有热更新”,优先检查这些点:
- 当前修改的是不是被模块系统接管的文件,而不是
public/里的静态文件。 - 项目路径、文件路径是否包含一些容易引发工具链兼容问题的特殊字符。
- 是否有编辑器、虚拟机、网络盘等环境影响文件监听。
- 是否误配了
server.hmr,或者缓存异常,此时可尝试删除node_modules/.vite后重启。
4. 生产构建与预览
pnpm build
pnpm preview
pnpm build用于生成生产环境产物,默认输出到dist/。pnpm preview用于本地预览构建结果,帮助你在部署前发现路径、资源引用等问题。
如果构建没报错,但打开页面后是空白页,先别急着怀疑业务代码,优先检查:
- 是否部署在子路径下,却没有正确设置
base。 - 是否手写了以
/开头的绝对资源路径,导致部署后资源地址错误。 - 是否把开发期代理当成了生产期能力,导致接口地址在上线后失效。
5. 单应用目录结构
my-vite-vue/
├── public/ # 原样拷贝到构建产物中的静态资源
├── src/ # 业务源码
│ ├── assets/ # 会进入构建流程的资源
│ ├── components/
│ ├── views/
│ ├── App.vue
│ └── main.ts
├── .env # 所有模式共享的环境变量
├── .env.development # 开发环境变量
├── .env.production # 生产环境变量
├── index.html # Vite 的 HTML 入口
├── package.json
├── tsconfig.json
└── vite.config.ts
这里有一个很容易忽略的区别:
public/中的资源不会经过打包器处理,适合放 favicon、robots.txt 这类稳定文件。src/assets/中的资源会参与构建、哈希命名和依赖分析,更适合放业务图片、字体、样式资源。
五、配置文件介绍
很多人第一次接触 Vite 时,会把注意力都放在 vite.config.ts 上。其实真正影响项目体验的,往往不是某一个配置文件,而是几个文件共同组成的协作关系。
graph LR
A[index.html] --> B[Vite Dev Server]
C[vite.config.ts] --> B
D[.env.*] --> B
E[tsconfig.json] --> F[TypeScript 工具链]
B --> G[src 业务代码]
H[.gitignore] --> I[仓库整洁度]
可以把它们理解成这样:
vite.config.*决定 Vite 怎样开发、怎样构建。tsconfig.*决定 TypeScript 怎样检查、怎样解析模块。.env*决定不同环境下可注入哪些变量。index.html是应用真正进入浏览器的入口。.gitignore决定哪些本地产物不该进入版本库。
1. vite.config.ts:项目的构建与开发中枢
这是最核心的配置文件。一个相对完整、适合讲解的示例如下:
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'node:path'
import { fileURLToPath, URL } from 'node:url'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
const rootDir = fileURLToPath(new URL('.', import.meta.url))
return {
base: env.VITE_PUBLIC_BASE || '/',
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(rootDir, './src')
}
},
server: {
host: '0.0.0.0',
port: 5173,
open: true,
proxy: {
'/api': {
target: env.VITE_API_TARGET,
changeOrigin: true,
rewrite: (url) => url.replace(/^\/api/, '')
}
}
},
preview: {
port: 4173
},
build: {
outDir: 'dist',
assetsDir: 'assets',
sourcemap: false,
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue']
}
}
}
}
}
})
这份配置里最常用、也最值得理解的字段有:
base:部署基础路径,默认值是/。只有应用部署到子目录或 CDN 前缀下时通常才需要改;一旦配错,最常见表现就是资源 404、页面空白。plugins:插件入口,默认只保留框架必需插件即可。只有要接入额外文件类型或增强能力时再扩展,插件越多,调试和升级成本越高。resolve.alias:路径别名。只有相对路径过深、共享包较多、Monorepo 协作时才特别值得加;如果只在 Vite 中配置、不在 TS 中同步,编辑器会先“闹脾气”。server:开发服务器相关配置,包括端口、host、代理、HMR 等。通常在本地联调、多端口开发或局域网调试时修改。preview:本地预览构建产物的配置。大多数项目很少改,但它适合用来模拟部署前的最终访问效果。build:生产构建配置,如输出目录、sourcemap、分包策略、压缩方式等。只有当你开始关心部署目录、调试能力和体积优化时,才需要逐步细调。
几个高频注意点也建议直接记住:
proxy只在开发服务器阶段生效,不会带到生产环境。base一旦配置错误,最常见表现就是“打包正常,但部署后资源 404 或页面空白”。- 如果项目是 ESM 配置环境,路径工具建议使用
node:path,并留意__dirname在不同模块模式下的写法差异。 - 某些老旧第三方包如果不能被顺利解析,优先检查包本身的模块格式,再考虑
optimizeDeps、build.commonjsOptions或替代包,而不是一上来堆插件。
2. tsconfig.json:类型检查与路径解析的基础
如果项目使用 TypeScript,那么 tsconfig.json 不只是“让 TS 不报错”,它还会影响编辑器提示、路径别名识别、模块解析方式和团队协作体验。
示例:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"jsx": "preserve",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"types": ["vite/client"]
},
"include": ["src", "vite.config.ts"]
}
重点说明:
moduleResolution: "Bundler"更贴近 Vite 的现代解析方式,适合新项目。types: ["vite/client"]可以让import.meta.env等 Vite 提供的类型提示生效。- 如果你在
vite.config.ts中配了@别名,但tsconfig.json没同步paths,编辑器通常会提示路径找不到,虽然项目未必立刻跑不起来,但协作体验会很差。
从使用角度看,可以把它理解得更简单一点:
- 默认情况下,脚手架生成的 TypeScript 配置已经够启动项目,没必要一开始就把它改得很“满”。
- 当你开始配置路径别名、提高类型严格度、拆分
tsconfig.app.json和tsconfig.node.json时,说明项目已经从“能跑”进入“更适合协作”阶段。 - 如果
tsconfig改错了,最常见的现象不是页面直接报错,而是编辑器提示异常、路径解析失效、import.meta.env没类型,或者 Node 侧脚本类型检查不对劲。
如果是更完整的工程,常见做法还包括:
- 用
tsconfig.app.json管应用源码。 - 用
tsconfig.node.json管 Node 侧脚本和配置文件。 - 根
tsconfig.json只做共享继承。
这在 Monorepo 或大型项目中会更清晰。
3. .env 系列文件:环境变量的边界
Vite 支持使用 .env、.env.local、.env.development、.env.production 等文件管理环境变量。
常见示例:
VITE_APP_TITLE=管理后台
VITE_API_TARGET=http://localhost:8080
VITE_PUBLIC_BASE=/
在业务代码中可以这样访问:
console.log(import.meta.env.VITE_APP_TITLE)
console.log(import.meta.env.VITE_API_TARGET)
这里最容易踩的坑只有一个,但非常高频:想在前端代码里访问的变量,必须以 VITE_ 开头。
如果你写成:
API_URL=http://localhost:8080
那么在前端代码里默认是拿不到的。这样设计的目的,是明确区分“可以暴露给前端”的变量和“只应存在于 Node/服务端上下文”的变量。
另外还要注意两点:
.env.local、.env.development.local这类本地私有变量通常不应提交到仓库。- 环境变量改动后,通常需要重启开发服务器,才能保证配置完全生效。
如果只想抓住最实用的原则,记住这三条就够了:
- 默认把
.env当作“按环境切换前端运行参数”的地方,而不是“存放秘密”的地方。 - 只有真的需要因环境不同而变化的值,才值得进入
.env,例如接口基地址、站点标题、功能开关。 - 如果变量写了却取不到,优先怀疑前缀、模式文件是否写对,以及开发服务器是否已经重启。
4. index.html:Vite 的真正入口
和很多传统构建工具不同,Vite 把 index.html 也纳入了构建体系,而不是仅仅把它当作一个静态模板文件。
常见写法:
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>%VITE_APP_TITLE%</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
它的重要性体现在三个方面:
- 它决定浏览器最先加载哪个模块入口。
- 它可以直接参与 Vite 的变量替换和 HTML 转换流程。
- 它往往也是部署路径问题最先暴露的地方。
如果项目部署到子路径,index.html 中的资源引用方式和 base 配置必须一致,否则就容易出现首页能开、资源却加载失败的问题。
在实际项目里,index.html 通常只会在这些时候改动:
- 调整页面标题、描述信息、SEO 元信息。
- 修改入口脚本或接入多页面结构。
- 插入极少量必须在首屏前执行的脚本。
如果这里改错了,最常见的表现是入口脚本没加载、标题变量没替换,或者部署后路径和资源地址对不上。
5. .gitignore:工程卫生的一部分
.gitignore 看起来不起眼,但它直接影响仓库是否干净、协作是否省心。
# 依赖
node_modules/
.pnpm-store/
# 构建产物
dist/
dist-ssr/
coverage/
# 本地环境变量
.env*.local
# 日志
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Vite 缓存
node_modules/.vite/
# 编辑器
.idea/
.vscode/
*.swp
*.swo
尤其是 dist/、node_modules/、.env*.local,几乎不应该进入版本库。前者会污染提交历史,后者可能带来安全和协作风险。
六、Vite 核心能力实操
理解完配置文件以后,再来看 Vite 的几个高频实战能力,会更容易建立“配置为什么这样写”的感觉。
1. 常见资源在 Vite 中的处理方式
如果想快速建立对 Vite 的整体认知,最有效的方法不是先背配置,而是先看“不同资源进入 Vite 后会发生什么”。
| 资源类型 | 开发阶段 | 生产构建后 | 你需要记住的点 |
|---|---|---|---|
main.ts、main.js | 按请求即时转换为浏览器可执行模块 | 进入打包入口,参与代码分割 | 它们决定应用从哪里启动 |
.vue、.tsx、.jsx | 先经过插件转换,再交给浏览器加载 | 被打进构建产物,和依赖一起优化 | 框架文件几乎都依赖对应插件 |
.css、.scss、.less | 可以直接被导入,修改后支持热更新 | 被抽取、合并、压缩 | 样式也是模块图的一部分,不是“额外附属物” |
src/ 下图片、字体、SVG | 会参与依赖分析和资源处理 | 一般会生成带哈希的资源文件 | 适合放业务资源,便于缓存控制 |
public/ 下静态文件 | 原样对外提供,不参与模块转换 | 原样拷贝到产物目录 | 适合放 favicon、robots.txt 这类稳定资源 |
.json | 可直接导入并参与模块图 | 会进入打包流程 | 适合小型静态配置,不适合大体量动态数据 |
2. CSS 与静态资源处理
Vite 对 CSS 的态度非常“现代前端化”:样式不是构建流程外的补充,而是和 JavaScript、组件一样,直接纳入模块系统。
最常见的几种写法如下:
import './styles/base.css'
import './styles/theme.scss'
import logoUrl from './assets/logo.png'
普通 CSS
普通 CSS 文件可以直接导入,开发阶段支持热更新,生产阶段会参与压缩与提取。
CSS Modules
如果文件名是 *.module.css、*.module.scss,它会被当作 CSS Modules 处理,适合组件级样式隔离:
import styles from './button.module.css'
console.log(styles.primary)
预处理器
如果你要用 Sass、Less、Stylus,只需要安装对应依赖即可,Vite 会把它们接入处理流程:
pnpm add -D sass
资源引用
在 CSS 里通过 url(...) 引用的图片、字体,只要它们位于 src/ 下,通常也会进入资源处理链;如果你希望某个文件完全原样保留,再考虑放到 public/。
这里最值得记住的不是配置细节,而是两条经验:
- 样式文件能被
import,说明它和脚本、组件一样,都是 Vite 模块图的一部分。 src/里的资源适合参与构建优化,public/里的资源适合保持稳定 URL,这两类资源不要混着理解。
3. 热模块替换(HMR)
Vite 默认已经内置 HMR,Vue、React 这类主流框架的日常开发一般无需额外配置。
flowchart LR
A["修改源码文件"] --> B["Vite 识别受影响模块"]
B --> C["失效相关模块缓存"]
C --> D["推送 HMR 更新"]
D --> E["浏览器局部更新"]
如果你需要定制一些行为,可以这样写:
import { defineConfig } from 'vite'
export default defineConfig({
server: {
hmr: {
overlay: true,
timeout: 3000
}
}
})
这里的重点不是“怎么把 HMR 配出来”,而是理解它什么时候会失效或看起来像失效:
- 修改的是不会进入模块图的文件。
- 文件监听环境不稳定。
- 某些状态不是热更新能安全保留的,框架会主动触发整页刷新。
- 插件转换链不完整,导致模块边界判断异常。
因此,看到热更新表现不符合预期时,先判断是“完全不触发”,还是“触发了但退化成整页刷新”,排查方向会更清楚。
4. 插件使用
选择插件请遵循核心原则:先保证必要,再追求丰富。插件数量并非越多越好,过度添加会带来三大问题:
- 模块转换链路变长,执行效率降低
- 代码调试难度提升,问题定位更复杂
- 版本升级时兼容点增多,维护成本变高
Vite 的插件系统是其核心工程能力,插件机制高度兼容 Rollup。官方插件 API 明确规定:开发环境下,Vite Dev Server 会创建插件容器,调用 Rollup 兼容的构建钩子(hooks)来处理模块。
我们可以把插件的核心工作流程简化为三类核心钩子,对应模块处理的完整链路:
resolveId:解析导入路径,确定模块的真实地址load:加载模块,读取模块的原始内容transform:转换源码,将代码编译为浏览器可执行的格式
插件分类(按使用场景)
日常开发中,可将 Vite 插件分为三类,方便按需选择:
-
框架必需插件
项目运行的基础依赖,无此插件框架无法工作。
示例:Vue 项目 →
@vitejs/plugin-vue、React 项目 →@vitejs/plugin-react。 -
工程增强插件
提升开发效率与体验的优化工具,非项目起步必需。
示例:自动导入、路径别名、SVG 转组件、代码检查器、打包体积分析等。
-
场景型插件
针对特定业务需求的专用插件,仅在对应场景下使用。
示例:文件系统路由、PWA 渐进式网页应用、Mock 数据、跨平台适配插件等。
最佳实践:先仅使用官方框架插件跑通项目,再根据实际需求逐步添加其他插件,避免一开始就堆砌插件。
最小化配置示例(Vue 项目)
# 仅安装框架必需插件
pnpm add -D @vitejs/plugin-vue
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
// 最小化插件配置,保证项目正常运行
plugins: [vue()]
})
强制插件排序
为兼容部分 Rollup 插件,需要手动控制插件执行顺序,或限定插件仅在构建阶段生效。
通过 enforce 修饰符可强制指定插件优先级:
pre:优先执行,在 Vite 核心插件之前调用- 默认:常规顺序,在 Vite 核心插件之后调用
post:延后执行,在 Vite 构建插件之后调用
// vite.config.js
import image from '@rollup/plugin-image'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
{
...image(),
enforce: 'pre' // 强制该插件优先执行
}
]
})
详细规则可查阅:Vite 插件排序 API
按需应用插件
默认插件会同时在 开发(serve)和生产构建(build) 模式生效。
通过 apply 属性可指定插件仅在单一模式运行,减少不必要的性能消耗。
// vite.config.js
import typescript2 from 'rollup-plugin-typescript2'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
{
...typescript2(),
apply: 'build' // 仅在生产构建时执行,开发环境不启用
}
]
})
6. 生产构建优化
import { defineConfig } from 'vite'
export default defineConfig({
build: {
minify: 'esbuild',
sourcemap: false,
rollupOptions: {
output: {
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
manualChunks: {
framework: ['vue']
}
}
}
}
})
优化时最常见的三个方向是:
- 减体积:合理分包、移除无用依赖、按需加载。
- 提稳定性:避免把过于激进的压缩或分包策略直接上线。
- 提可观测性:按需保留 sourcemap,方便排错。
如果应用部署在二级目录,例如 https://example.com/admin/,务必补上:
export default defineConfig({
base: '/admin/'
})
很多“本地一切正常,线上白屏”的问题,根因都在这里。
7. 与后端联调
以前后端分离开发为例,可以通过代理转发解决本地跨域和接口地址切换问题:
import { defineConfig } from 'vite'
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
})
这样前端请求 /api/user 时,会由开发服务器转发给后端服务。
这里要特别注意:
- 这只是开发期代理,不是生产环境网关。
- 如果代理配置正确却仍然报跨域,往往不是 Vite 本身失效,而是请求没有真正经过 Vite 开发服务器,例如你请求了完整后端地址。
- 如果后端接口本身依赖特定前缀,就不要盲目
rewrite,否则可能把路径改坏。
七、Monorepo 场景使用
Vite 在 Monorepo 中很常见,因为它本身只负责前端应用的开发和构建,而不试图接管整个仓库的任务编排。这一点和 pnpm、Turborepo 正好互补。
graph LR
A[pnpm Workspaces] --> B[apps/web]
A --> C[apps/admin]
A --> D[packages/ui]
A --> E[packages/utils]
F[Turborepo] --> B
F --> C
B --> D
C --> D
B --> E
C --> E
linkStyle 0 stroke:#4285F4,stroke-width:2px
linkStyle 1 stroke:#4285F4,stroke-width:2px
linkStyle 2 stroke:#4285F4,stroke-width:2px
linkStyle 3 stroke:#4285F4,stroke-width:2px
linkStyle 4 stroke:#0F9D58,stroke-width:2px
linkStyle 5 stroke:#0F9D58,stroke-width:2px
linkStyle 6 stroke:#F4B400,stroke-width:2px
linkStyle 7 stroke:#F4B400,stroke-width:2px
linkStyle 8 stroke:#DB4437,stroke-width:2px
linkStyle 9 stroke:#DB4437,stroke-width:2px
1. 推荐目录结构
monorepo-project/
├── apps/
│ ├── web/ # Vite + Vue
│ ├── admin/ # Vite + React
│ └── api/ # 后端服务
├── packages/
│ ├── ui/ # 共享组件库
│ └── utils/ # 共享工具库
├── package.json
├── pnpm-workspace.yaml
└── turbo.json
2. 创建应用
cd apps
pnpm create vite@latest web -- --template vue
pnpm create vite@latest admin -- --template react
3. 配置 pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
相比把每个包都手写出来,使用通配写法通常更易维护;除非你的仓库结构非常特殊,否则没必要把每个应用路径一条条写死。
4. 配置 turbo.json
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"dev": {
"cache": false,
"persistent": true
},
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"lint": {},
"test": {}
}
}
这里要注意一个细节:如果示例写的是 JSON,就不要混入注释,否则严格 JSON 解析会失败。很多教程为了讲解方便把注释直接写进 json 代码块,读者复制后反而容易踩坑。
5. 共享包与别名
如果应用需要引用 packages/ui,可以在 Vite 和 TypeScript 中同时配置别名。例如:
import { defineConfig } from 'vite'
import { fileURLToPath, URL } from 'node:url'
export default defineConfig({
resolve: {
alias: {
'@ui': fileURLToPath(new URL('../../packages/ui/src', import.meta.url))
}
}
})
与此同时,也要在 tsconfig.json 中同步 paths,否则运行时能找到、编辑器却报错,是 Monorepo 新手最常见的不协调体验之一。
八、生产部署与协作建议
这一章不再单独做“常见问题”,而是把真正影响上线和协作的点提炼成几个部署前应确认的结论。
1. 部署前至少确认三件事
base是否与真实部署路径一致。- 构建后的资源是否都通过
pnpm preview本地验证过。 - 环境变量是否区分了“前端可见”和“仅服务端可见”。
2. 不要把开发代理当成上线方案
server.proxy 很方便,但它只是开发阶段的本地转发。真正上线后,请求通常需要由以下角色接手:
- Nginx
- 网关层
- BFF
- 后端服务本身
如果团队成员误以为“本地代理配好了,线上也会自动转发”,那部署后大概率会遇到接口 404 或跨域问题。
3. 让构建结果可验证、可复现
建议至少保留这些脚本:
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
}
保持脚本直白有一个好处:无论是本地调试、CI 构建还是排查问题,大家看到的入口都一致,不容易因为包装层太多而失去判断依据。
九、Vite 核心架构与工作流程
理解 Vite 最好的方式,不是背配置项,而是搞清楚它在开发和生产两条链路里分别做了什么。
1. 整体架构:开发与构建分离
Vite 的底层可以先拆成两条链路:
- 开发链路:Dev Server + 依赖预构建 + 按请求转换 + HMR(关注“快反馈”,所以按需处理源码)
- 生产链路:完整 bundler 构建 + 代码分割 + 资源产出(关注“可交付”,所以走完整打包与优化流程)
graph TD
A["源码"] --> B["index.html"]
A --> C["src/"]
A --> D["vite.config.ts"]
B --> E["开发链路"]
C --> E
D --> E
E --> F["依赖预构建"]
E --> G["按请求转换"]
E --> H["模块图 + HMR"]
A --> I["生产链路"]
I --> J["完整 bundler 构建"]
J --> K["dist/"]
这也是为什么 Vite 的体验常常是“双相”的:
- 本地开发时像一个很聪明、很轻的服务器。
- 生产构建时又像一个成熟的工程化打包器前端。
2. 开发环境工作流程
当你执行 pnpm dev 时,Vite 大致会经历下面的过程:
flowchart TD
subgraph 启动
direction LR
A["执行 pnpm dev"] --> B["读取 vite.config.ts"]
B --> C["创建插件容器<br>加载插件"]
C --> D["扫描并预构建第三方依赖"]
D --> E["启动 Dev Server"]
end
subgraph 浏览器
direction LR
F["浏览器请求<br>index.html"]
F --> G["分析<br>入口HTML<br>并返回"]
G --> H["浏览器请求<br>src/main.ts 等模块"]
H --> I["按需执行<br><br>resolveId<br>↓<br>load<br>↓<br>transform<br><br>转换源码为浏览器可执行模块"]
I --> J["Vite<br>构建模块图"]
J --> K["浏览器原生 ESM 加载"]
K --> L["监听<br>文件变化"]
L --> M["变更后<br>发送 HMR 更新"]
end
启动~~~浏览器
这里最关键的是三件事:
(1)依赖预构建不是“完整打包项目”
Vite 在开发阶段并不是完全不处理依赖。它通常会先把第三方依赖做一次预构建,主要目的包括:
- 把 CommonJS、UMD 等包整理成更适合浏览器处理的 ESM 形式。
- 把一个包内部过深、过碎的模块引用收敛起来,减少浏览器请求压力。
- 利用缓存,让后续启动更快。
这一步通常由 esbuild 参与完成,所以速度很高。
(2)入口到底是什么
在很多传统构建工具心智里,入口往往是 main.ts、main.js。 但对 Vite 来说,真正的入口是 index.html。
原因很简单:
- 浏览器最先请求的是 HTML
- Vite 先拿到 HTML
- 再把 HTML 里的
<script type="module">和相关资源继续展开成模块请求
也就是说:
index.html是 应用入口main.ts是 模块入口
这就是为什么 Vite 能把 HTML 也纳入插件和变量替换流程。
(3)开发时到底“扫描”了什么
Vite 不扫描所有源码,而是从入口和依赖图出发。Vite 没有 Nuxt 那种大规模的“约定目录扫描生成路由”逻辑,它更像是:
-
浏览器先打开
index.html -
Vite 看到
index.html里引用了/src/main.ts<script type="module" src="/src/main.ts"></script> -
浏览器再请求
/src/main.ts -
Vite 处理
main.ts时,发现它又 import 了别的模块 -
然后再继续处理这些被 import 的模块
-
就这样一层一层往下走
-
没有被入口链路引用到的模块,暂时不处理
也就是说,Vite 开发期更核心的不是“扫目录”,而是“建立模块依赖图”。
flowchart LR
A["请求HTML"] --> B["发现 import 模块"]
B --> C["解析 import"]
C --> D["继续解析子模块"]
D --> E["建立模块图"]
理解什么是“依赖图”
假设有以下文件,那这些文件之间就形成了一张关系网:
graph LR
subgraph 细化链路说明
direction LR
a["浏览器打开页面"] --> b["请求<br>index.html"]
b --> c["发现入口<br>/src/main.ts"]
c --> d["main.ts<br>引入<br>App.vue<br>和<br>style.css"]
d --> e["继续<br>处理 App.vue 里的<br> import 模块"]
e --> f["把这些引用关系<br>记录成模块图"]
end
subgraph 依赖图
A[index.html] --> B[src/main.ts]
B --> C[App.vue]
B --> D[style.css]
C --> E[Header.vue]
end
subgraph 文件实际内容
1["index.html"]---2["src="/src/main.ts""]
3["main.ts"]---4["import App from './App.vue'"]
3---5["import './style.css'"]
6["App.vue"]---7["import Header from './Header.vue'"]
end
这张“谁依赖谁”的关系网,就叫模块依赖图;也可以简称:模块图。
(4)一次模块请求进来后,Vite 是怎么处理的
源码是“按请求转换”的。因此开发阶段的等待时间,不再集中爆发在“先打完整个包”,而是分散到“请求到哪里,处理到哪里”。
浏览器请求一个模块时,例如 /src/main.ts,Vite 不一定会把文件原样返回,而是会让这个请求经过一条处理链。
graph LR
A["浏览器请求<br>/src/main.ts"] --> B["<b>resolveId</b><br/>路径解析"]
B --> C["<b>load</b><br/>获取模块内容"]
C --> D["<b>transform</b><br/>源码转换"]
D --> E["返回<br/>浏览器可执行的<br/>ESM 内容"]
E --> F["记录到<br/>模块图"]
-
resolveId:路径解析,先确认这个模块到底是谁。-
例如,分析以下导入语句:
import App from './App.vue'Vite 要先确认:是不是 绝对路径、别名路径、虚拟模块、要不要被某个插件接管;
-
-
load:获取内容,决定这个模块的原始内容从哪里来。-
大多数普通文件,其实就是直接从磁盘读出来,例如:
App.vue、main.ts、styles.css但也有一些情况不是直接读文件,比如:插件返回一个“虚拟模块”、某个模块其实是插件动态生成的、某些内容不是磁盘里的真实文件
-
-
transform:源码转换,决定怎么变成浏览器能运行的内容。-
因为很多源码,浏览器其实不能直接运行,比如:
.vue 文件、JSX、TypeScript、特殊语法;所以要把原始内容变成浏览器能执行的
JavaScript/CSS/Module内容。
-
插件体系在流程中的位置
这里最容易让人误解的一点是:resolveId -> load -> transform 这条链,并不等于“插件体系本身”。
它更准确的定位是:插件体系最核心参与的一段工作流程,也是插件最常见的介入点。
也就是说:
- 插件体系 是一整套“让插件接入 Vite、扩展 Vite 能力”的机制
- resolveId、load、transform 是这套机制里最常见的几个接口点
Vue 单文件组件、React Fast Refresh、SVG 转组件、Markdown 转页面,本质上都建立在这类机制之上。
插件们经常在同一条处理链上接力工作,它们不是简单“并排存在”,这也解释了为什么插件顺序有时会影响结果。
(5)HMR 是什么,怎么工作的
HMR(Hot Module Replacement,热模块替换)改了一个模块后,Vite 尽量只替换这个模块,以及真正受它影响的那一小部分,而不是把整个页面重新加载一遍。所以它和“浏览器整页刷新”最大的区别是:
- 整页刷新:整个页面重新来一次
- HMR:只更新变动的那一小块
举例解释:
- 假设页面里有三个部分:顶部导航用户列表页脚。
- 现在你只改了“用户列表”组件里的一个文字样式。
- 如果没有 HMR,浏览器通常会:整页刷新重新加载,页面状态可能丢失(你刚展开的面板、输入框内容可能也没了)
- 如果有 HMR,Vite 会尽量做到:只更新“用户列表”这个模块,其他部分不动,页面尽量不整页刷新
很多人以为 Vite 热更新快,只是因为“监听到文件改了”。 其实更关键的是它维护了一张模块图,知道:
- 一个模块被谁依赖
- 这个模块改了之后应该通知谁
- 哪些更新可以局部替换,哪些更新必须整页刷新
flowchart LR
1["记忆 HMR 原理"]
subgraph 开发服务器持续监听文件变化
direction LR
A["文件变化"] --> B["模块失效"]
B --> C["查模块图依赖关系<br>找出影响范围"]
C --> D["推送 HMR 更新"]
D --> E["能热替换,就只更新相关模块"]
D --> F["不能安全热替换,就整页刷新"]
end
开发服务器和浏览器之间会保持一条实时通信通道(通常可理解为 WebSocket)。文件变化后,Vite 通过这条通道把 HMR 更新消息推给浏览器
为什么有时候是局部更新,有时候又整页刷新
因为不是所有模块都能“安全替换”。
通常可以直接 HMR 的状况:
- 改了 CSS
- 改了 Vue 组件模板
- 改了某些局部逻辑
- 框架插件知道怎么安全处理更新
部分不能安全局部更新的情况:
- 改动影响了模块边界
- 某些状态不能安全保留
- 插件没法判断如何热替换
- 改的是 HTML 入口、配置文件、某些全局逻辑
3. 生产构建工作流程
当你执行 pnpm build 时,Vite 的目标就从“快速反馈”切换成“稳定交付”:把整个应用完整分析、优化、拆分,并输出一份真正可部署的生产产物。
flowchart LR
A[执行 pnpm build] --> B[读取配置与模式]
B --> C[加载插件]
subgraph 构建起点
D[index.html<br>或<br>指定入口]
end
subgraph 构建分析
E[完整解析模块依赖图]
F[Tree-Shaking]
G[代码分割]
end
subgraph 产物处理
H[处理 CSS 与静态资源]
I[压缩与优化产物]
J[生成 dist]
end
C --> 构建起点
构建起点 --> 构建分析
E --> F
F --> G
构建分析 --> 产物处理
H --> I
I --> J
简要说说下面几个需要解释的阶段。
(1)以入口为起点,完整解析依赖图
开发阶段是“浏览器请求到哪,Vite 处理到哪”。但生产阶段不一样,生产阶段必须把整个应用都构建完整,所以它会:从 index.html 或指定入口开始,一路向下分析所有 import 关系,把整个应用的依赖关系完整梳理出来。
这里最重要的区别是:
- 开发阶段:按请求处理模块
- 生产阶段:完整分析整个应用依赖图
所以这一阶段不再是“当前页面需要什么”,而是整个应用最终要上线的内容到底有哪些。
(2)Tree-Shaking:移除没有实际用到的代码
当完整依赖图建立起来以后,Vite 会进入优化阶段,其中一个很重要的环节就是 Tree-Shaking。
它的作用是找出没有被真正使用的导出,尽量把这些无用代码从最终产物里移除。
你可以把它理解成:源码里可能 import 了一个很大的工具库,但你实际只用了其中一部分,那构建时就尽量不要把没用到的部分也打进去。
所以 Tree-Shaking 本质上是在做:删掉不会被真正执行的无用代码。
这也是为什么现代构建工具特别强调:
- ESM
- 静态导入
- 可分析的依赖关系
因为只有这样,Tree-Shaking 才更有效。
(3)代码分割:把一个大应用拆成更合理的多个文件
接下来,Vite 会根据入口和依赖关系做 代码分割。
这一步的目标不是“拆得越碎越好”,而是:
- 把公共代码抽出来
- 把异步加载的部分拆出去
- 让首屏加载更轻
- 让缓存更有效
例如:
- 首页不需要的页面逻辑,不应该都塞进首屏包里
- 第三方依赖可能适合单独拆包
- 路由懒加载的页面可以单独生成 chunk
所以代码分割本质上是在做:让最终产物的组织方式更适合浏览器加载和缓存。
(4)处理 CSS 与静态资源
生产构建阶段,Vite 还会统一处理这些内容:CSS、图片、字体、SVG、其他静态资源
这一步通常会涉及:
- 抽取样式
- 合并样式
- 资源重命名
- 哈希命名
- 路径重写
- 构建产物中的资源归类
例如你在源码里写:
import './styles/base.css' import logoUrl from './assets/logo.png'
构建后它们不会简单“原样复制”,而是会根据构建规则进入最终产物结构中。
这一步的意义在于:让静态资源既能正确引用,又适合长期缓存和部署。
(5)压缩与优化产物
在最终输出前,Vite 还会做构建后处理,例如:
- 压缩 JavaScript、CSS
- 清理一部分无意义内容
这一阶段的目标是:减小体积,提升加载效率,让最终产物更适合上线环境
(6)生成最终构建产物
所有步骤完成后,Vite 最终会输出构建结果,默认是dist/ 。里面常见会包含:
- 入口 HTML
- JS chunk
- CSS 文件
- 静态资源文件
- 带 hash 的构建产物
到这一步为止,构建流程才真正结束。所以可以把 dist/ 理解成:Vite 把整个应用按生产环境要求整理后的最终交付物。
4. 开发与生产简比
| 维度 | pnpm dev | pnpm build |
|---|---|---|
| 核心目标 | 快反馈 | 可交付 |
| 处理方式 | 按请求转换 | 完整构建 |
| 面向对象 | 当前请求到的模块 | 整个应用 |
| 重点能力 | 模块图、HMR、预构建 | 分包、压缩、Tree-Shaking |
换句话说,开发阶段的“快”来自于少做无意义的整包工作;生产阶段的“稳”来自于把该做的优化认真做完。
这也是为什么开发阶段和生产阶段看起来像两个世界:
- 开发阶段关注的是:快反馈、按需处理、局部更新
- 生产阶段关注的是:完整构建、体积优化、资源组织、最终交付