快速了解熟悉 Vite ,即刻上手使用

1 阅读24分钟

一、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 在不同模块模式下的写法差异。
  • 某些老旧第三方包如果不能被顺利解析,优先检查包本身的模块格式,再考虑 optimizeDepsbuild.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.jsontsconfig.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.tsmain.js按请求即时转换为浏览器可执行模块进入打包入口,参与代码分割它们决定应用从哪里启动
.vue.tsx.jsx先经过插件转换,再交给浏览器加载被打进构建产物,和依赖一起优化框架文件几乎都依赖对应插件
.css.scss.less可以直接被导入,修改后支持热更新被抽取、合并、压缩样式也是模块图的一部分,不是“额外附属物”
src/ 下图片、字体、SVG会参与依赖分析和资源处理一般会生成带哈希的资源文件适合放业务资源,便于缓存控制
public/ 下静态文件原样对外提供,不参与模块转换原样拷贝到产物目录适合放 favicon、robots.txt 这类稳定资源
.json可直接导入并参与模块图会进入打包流程适合小型静态配置,不适合大体量动态数据

把这张表记住后,很多问题都会自然变得好判断:

  • 某个文件为什么能热更新,是因为它进入了模块图。
  • 某个资源为什么带哈希,是因为它走了构建处理链。
  • 某个文件为什么只能通过 URL 访问,是因为它在 public/ 下而不是 src/ 下。

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 这类主流框架的日常开发一般无需额外配置。

如果你需要定制一些行为,可以这样写:

import { defineConfig } from 'vite'

export default defineConfig({
  server: {
    hmr: {
      overlay: true,
      timeout: 3000
    }
  }
})

这里的重点不是“怎么把 HMR 配出来”,而是理解它什么时候会失效或看起来像失效:

  • 修改的是不会进入模块图的文件。
  • 文件监听环境不稳定。
  • 某些状态不是热更新能安全保留的,框架会主动触发整页刷新。
  • 插件转换链不完整,导致模块边界判断异常。

因此,看到热更新表现不符合预期时,先判断是“完全不触发”,还是“触发了但退化成整页刷新”,排查方向会更清楚。

4. 环境变量

# .env.development
VITE_API_BASE_URL=http://localhost:8080/api
VITE_APP_TITLE=开发环境 - 我的应用
console.log(import.meta.env.VITE_API_BASE_URL)
console.log(import.meta.env.VITE_APP_TITLE)
console.log(import.meta.env.DEV)
console.log(import.meta.env.PROD)

在实际项目里,比较推荐的做法是:

  • 用环境变量控制接口基地址、应用标题、资源前缀等“环境相关但不涉密”的信息。
  • 不把真正的服务端密钥放进前端环境变量里,因为最终仍会进入前端产物。
  • 约定命名规则,并在团队内保持统一,避免有人写 VITE_API_URL,有人写 VITE_BASE_API

5. 插件使用

插件是 Vite 可扩展性的核心,但在教程语境里,更重要的不是“列出很多插件”,而是先建立一个克制的选择顺序。

可以先把常见插件分成三类:

  • 框架必需插件:例如 Vue 项目的 @vitejs/plugin-vue、React 项目的 @vitejs/plugin-react。这类插件通常是“项目正常工作”的基础。
  • 工程增强插件:例如自动导入、路径别名增强、SVG 转组件、检查器、可视化分析等。它们提升开发体验,但不是项目起步必需。
  • 场景型插件:例如文件系统路由、PWA、Mock、特定平台适配插件。这类插件通常只在明确业务场景下才需要。

对于大多数读者,最稳妥的起点反而很简单:先只用官方框架插件,把项目跑顺,再按场景加插件。

以 Vue 项目为例,最小可用配置通常就是:

pnpm add -D @vitejs/plugin-vue
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()]
})

选择插件时建议保持一个原则:先保证必要,再追求丰富。

因为插件越多,意味着:

  • 转换链越长。
  • 调试成本越高。
  • 升级时的兼容点越多。

如果某个旧包在 Vite 里表现不好,不一定要立刻找一个“兼容插件”去补。更稳妥的顺序通常是:

  1. 确认包是否仍在维护。
  2. 确认它导出的模块格式。
  3. 再决定是否通过 Vite 配置、插件或替代方案解决。

换句话说,好的插件策略不是“看到好用就装”,而是“先有明确问题,再引入明确工具”。

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. 整体架构:开发与构建分离

graph TB
    A[源码与配置] --> B[Vite]
    B --> C[开发服务器链路]
    B --> D[生产构建链路]
    C --> E[依赖预构建]
    C --> F[源码按需转换]
    C --> G[HMR 通知]
    D --> H[Rollup 打包]
    D --> I[Tree-Shaking]
    D --> J[代码分割]
    D --> K[产出 dist]

这张图可以概括 Vite 的核心设计:

  • 开发阶段 关注“快反馈”,所以按需处理源码。
  • 生产阶段 关注“可交付”,所以走完整打包与优化流程。

这也是为什么 Vite 的体验常常是“双相”的:

  • 本地开发时像一个很聪明、很轻的服务器。
  • 生产构建时又像一个成熟的工程化打包器前端。

2. 开发环境工作流程

当你执行 pnpm dev 时,Vite 大致会经历下面的过程:

flowchart TD
    A[执行 pnpm dev] --> B[读取 vite.config.*]
    B --> C[加载插件]
    C --> D[扫描并预构建依赖]
    D --> E[启动 Dev Server]
    E --> F[浏览器请求 index.html]
    F --> G[返回入口 HTML]
    G --> H[浏览器继续请求 src/main.ts 等模块]
    H --> I[Vite 按需转换源码为浏览器可执行模块]
    I --> J[浏览器原生 ESM 加载]
    J --> K[监听文件变化]
    K --> L[变更后发送 HMR 更新]

这里最关键的是三件事:

(1)依赖预构建不是“完整打包项目”

Vite 在开发阶段并不是完全不处理依赖。它通常会先把第三方依赖做一次预构建,主要目的包括:

  • 把 CommonJS、UMD 等包整理成更适合浏览器处理的 ESM 形式。
  • 把一个包内部过深、过碎的模块引用收敛起来,减少浏览器请求压力。
  • 利用缓存,让后续启动更快。

这一步通常由 esbuild 参与完成,所以速度很高。

(2)源码是“按请求转换”的

浏览器请求 src/main.ts,Vite 才去转换 main.ts;请求到某个 Vue 组件,再去处理对应的 SFC;请求到 CSS、图片、JSON,也按相应规则返回转换后的结果。

因此开发阶段的等待时间,不再集中爆发在“先打完整个包”,而是分散到“请求到哪里,处理到哪里”。

(3)HMR 依赖的是模块图,而不是简单文件刷新

Vite 内部会维护模块之间的依赖关系。当某个文件变化时,它会判断:

  • 哪些模块受影响。
  • 这些模块能否局部替换。
  • 如果不能安全替换,是否需要退化成整页刷新。

所以 HMR 的快,不只是因为“监听到了文件变化”,更因为它知道“应该通知谁更新”。

3. 插件体系在流程中的位置

插件不是附属功能,而是 Vite 工作流的重要组成部分。

graph LR
    A[浏览器请求模块] --> B[resolveId]
    B --> C[load]
    C --> D[transform]
    D --> E[返回可执行内容]

你可以把插件理解成一组介入点:

  • resolveId:决定某个导入路径最终解析到哪里。
  • load:决定某个模块内容从哪里来。
  • transform:决定源码如何被转换。

Vue 单文件组件、React Fast Refresh、SVG 转组件、Markdown 转页面,本质上都建立在这类机制之上。

这也解释了为什么插件顺序有时会影响结果:因为它们不是简单“并排存在”,而是在同一条解析与转换链上协作。

4. 生产构建工作流程

当你执行 pnpm build 时,Vite 的目标就从“快速反馈”切换成“稳定交付”:

flowchart TD
    A[执行 pnpm build] --> B[读取配置与模式]
    B --> C[加载插件]
    C --> D[Rollup 构建模块图]
    D --> E[Tree-Shaking]
    E --> F[代码分割]
    F --> G[处理 CSS 与静态资源]
    G --> H[压缩与产物输出]
    H --> I[生成 dist]

此时关注点变成了:

  • 有没有无用代码可以移除。
  • 首屏资源是否合理拆分。
  • 公共依赖是否应该单独抽离。
  • 资源文件是否应加哈希,便于缓存。

换句话说,开发阶段的“快”来自于少做无意义的整包工作;生产阶段的“稳”来自于把该做的优化认真做完。

5. 用一句话串起整套机制

如果要用一句话概括 Vite 的架构理念,可以这样理解:

开发时尽量把工作推迟到真正被请求的那一刻,构建时再把所有应该系统化完成的优化集中做完。

理解这句话之后,再看 vite.config.ts 中的 serverpluginsbuildresolve 等配置,你会更容易判断它们分别影响的是哪一条链路,以及某个问题应该去开发链路排查,还是去生产链路排查。