外观
Vite 系统学习文档
适合人群:已经学过 HTML、CSS、JavaScript,准备进入现代前端工程化的学习者。
学习目标:从会创建 Vite 项目,到理解开发服务器、依赖预构建、环境变量、构建优化、部署和进阶配置。
核对时间:2026-06-18。2026-07-09 已按 Vite 8.1.4、@vitejs/plugin-vue 6.0.7、PostCSS 8.5.16 补充 PostCSS 配置、Vue SFC 解析、路径解析、插件生命周期与源码级实现原理。2026-09-01 已按 Vite 8.2.x 补充依赖预构建缓存机制、HMR 客户端 API、Environment API 与 Rolldown-Vite / Vite 8 架构。版本细节请以官方文档为准,本文重点讲学习路线和工程化思路。
目录
- Vite 到底是什么
- 学习 Vite 前需要具备什么基础
- 快速开始:创建第一个 Vite 项目
- Vite 项目结构详解
- Vite 为什么开发时很快
- vite.config 配置文件系统讲解
- 开发服务器 server 配置
- 路径别名与模块解析
- 静态资源、CSS 与预处理器
- 环境变量与模式
- TypeScript 在 Vite 中的正确用法
- 插件机制与常见插件
- 生产构建 build 配置
- 代码分割、缓存与性能优化
- 代理、跨域与本地联调
- 部署上线与 base 路径
- 进阶主题:库模式、SSR、Monorepo
- Vite 与 Webpack 的关系
- 常见问题与排查思路
- 推荐学习顺序
- 核心机制深度补全
- 学习资源
1. Vite 到底是什么
Vite 是一个现代前端构建工具。它主要解决两个阶段的问题:
- 开发阶段:启动本地开发服务器,让你边写代码边快速预览。
- 生产阶段:把源码构建成可以部署上线的静态资源。
很多初学者会把 Vite 理解成脚手架。这个说法只对了一半。
更准确地说:
create-vite是创建项目的工具。vite是开发服务器和构建工具本体。vite.config.ts是项目工程化配置入口。- Vite 底层开发阶段主要利用浏览器原生 ESM;从 Vite 8 开始,依赖预构建和生产构建的核心打包能力转向 Rolldown,语法转换和压缩能力也更多依赖 Oxc。Vite 插件 API 仍保持 Rollup 风格的兼容思路,但配置项要以当前 Vite 文档为准。
你可以把 Vite 理解为:
一个面向现代浏览器和现代框架的前端工程化底座。
它不是 Vue 专属工具。Vite 可以服务于 Vue、React、Svelte、Solid、Vanilla JavaScript、Vanilla TypeScript 等项目。
1.1 Vite 解决了什么痛点
传统构建工具在开发时经常需要先分析整个项目,再生成一个或多个 bundle,最后启动开发服务器。项目越大,启动越慢。
Vite 的核心思路是:
- 开发时先不急着整体打包。
- 浏览器需要哪个模块,开发服务器就按需转换哪个模块。
- 第三方依赖提前预构建,减少浏览器请求和 CommonJS 兼容问题。
- 保存文件后只更新受影响模块,尽量不整页刷新。
所以 Vite 带来的直接感受是:
- 启动快。
- 热更新快。
- 默认配置少。
- 现代前端项目上手成本低。
2. 学习 Vite 前需要具备什么基础
建议你先具备下面这些基础:
- 会写基本的 HTML、CSS、JavaScript。
- 理解
import和export。 - 知道
npm install、npm run dev、npm run build的作用。 - 知道浏览器环境和 Node.js 环境不是同一个环境。
- 理解前端项目为什么需要构建:转换语法、处理资源、压缩、拆包、生成生产产物。
如果你正在学 Vue,建议至少了解:
- 单文件组件
.vue。 main.ts或main.js是应用入口。App.vue是根组件。- 路由、状态管理、组件库只是业务层,Vite 是工程层。
3. 快速开始:创建第一个 Vite 项目
官方推荐用下面命令创建项目:
bash
npm create vite@latest执行后按提示选择:
- 项目名。
- 框架,例如 Vue、React、Vanilla。
- 语言,例如 JavaScript、TypeScript。
直接创建 Vue 项目:
bash
npm create vite@latest my-vue-app -- --template vue直接创建 Vue + TypeScript 项目:
bash
npm create vite@latest my-vue-app -- --template vue-ts安装依赖并启动:
bash
cd my-vue-app
npm install
npm run dev通常会看到类似输出:
bash
Local: http://localhost:5173/这说明开发服务器已经启动。
3.1 常见 npm scripts
json
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
}含义如下:
npm run dev:启动开发服务器。npm run build:构建生产环境产物。npm run preview:本地预览构建后的dist目录。
注意:vite preview 只适合本地检查构建结果,不应该当生产服务器使用。
3.2 pnpm 和 yarn 写法
bash
pnpm create vite my-vue-app --template vue-tsbash
yarn create vite my-vue-app --template vue-ts学习时不必纠结包管理器。团队用什么你就用什么,关键是理解 Vite 项目结构和配置。
4. Vite 项目结构详解
一个典型 Vue + Vite + TypeScript 项目通常长这样:
text
my-vue-app/
├── public/
│ └── favicon.ico
├── src/
│ ├── assets/
│ ├── components/
│ ├── App.vue
│ ├── main.ts
│ └── vite-env.d.ts
├── index.html
├── package.json
├── tsconfig.json
├── tsconfig.app.json
├── tsconfig.node.json
└── vite.config.ts4.1 index.html
Vite 把 index.html 当作入口的一部分处理。
典型内容:
html
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>这和很多旧工具链不同。在一些旧脚手架里,HTML 经常由插件生成;在 Vite 中,HTML 本身就是入口,Vite 会分析里面的模块引用。
4.2 src/
业务源码目录。组件、路由、状态管理、样式、工具函数通常都放在这里。
常见组织方式:
text
src/
├── api/
├── assets/
├── components/
├── composables/
├── router/
├── stores/
├── styles/
├── utils/
├── views/
├── App.vue
└── main.ts4.3 public/
public 里的文件不会被 Vite 当成模块处理,而是原样复制到构建结果中。
适合放:
favicon.ico。- 不需要 hash 的静态文件。
- 第三方要求固定路径访问的文件。
不适合放:
- 需要通过
import引入并参与构建优化的图片。 - 希望被 hash 缓存管理的资源。
如果图片会被组件引用,优先放在 src/assets。
4.4 vite.config.ts
Vite 主配置文件。项目小的时候可以很少配置,项目变大后会越来越重要。
最基础写法:
ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
})defineConfig 不会改变运行逻辑,它主要提供类型提示。
5. Vite 为什么开发时很快
Vite 快的原因不是“用了某个神奇优化”,而是一整套开发阶段策略组合。
5.1 传统开发服务器的常见流程
传统 bundler 在开发时通常会这样做:
- 找到入口文件。
- 分析完整依赖图。
- 对所有模块执行 loader / plugin。
- 生成一个或多个 bundle。
- 启动开发服务器。
项目越大,这个启动过程越重。
5.2 Vite 的开发阶段思路
Vite 开发时利用浏览器原生 ES Modules:
ts
import { createApp } from 'vue'
import App from './App.vue'浏览器看到 type="module" 后,会按模块请求资源。Vite 开发服务器拦截这些请求,按需转换并返回结果。
这意味着:
- 首页实际访问到的模块会被处理。
- 没访问到的模块不需要一开始全部处理。
- 修改文件时可以精准更新相关模块。
5.3 依赖预构建
Vite 仍然需要处理 node_modules,原因有两个:
- 很多依赖仍然以 CommonJS 或 UMD 形式发布。
- 某些包内部模块很多,如果浏览器逐个请求会很慢。
因此 Vite 会在开发启动时进行依赖预构建:
- 使用 Rolldown 将依赖转成浏览器更容易消费的 ESM。
- 把内部许多模块合并成较少请求。
- 缓存在
node_modules/.vite中。
遇到依赖更新或配置变化时,Vite 会重新预构建。必要时可以手动清理缓存:
bash
rm -rf node_modules/.viteWindows PowerShell:
powershell
Remove-Item -Recurse -Force node_modules/.vite5.4 HMR 热更新
HMR 是 Hot Module Replacement,意思是热模块替换。
它不是简单刷新页面,而是尽量只替换变更模块。例如你改了一个 Vue 组件:
- Vite 检测文件变化。
- 重新转换这个组件。
- 通知浏览器替换该模块。
- 页面状态尽量保留。
这就是为什么修改组件样式或模板时,反馈很快且状态不容易丢。
6. vite.config 配置文件系统讲解
常见完整配置示例:
ts
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'node:path'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), 'VITE_')
return {
base: env.VITE_PUBLIC_PATH || '/',
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
server: {
host: '0.0.0.0',
port: 5173,
open: true,
},
build: {
outDir: 'dist',
sourcemap: false,
assetsDir: 'assets',
},
}
})6.1 root
项目根目录。默认是当前工作目录。
ts
export default defineConfig({
root: '.',
})多数项目不需要改。Monorepo 或特殊目录结构才常用。
6.2 base
部署基础路径。它影响构建后资源引用路径。
ts
export default defineConfig({
base: '/',
})常见场景:
- 部署到域名根路径:
base: '/'。 - 部署到子路径:
base: '/admin/'。 - 相对路径部署:
base: './',但路由和资源路径要额外检查。
如果你的项目部署到 https://example.com/admin/,通常要设置:
ts
base: '/admin/'Vue Router 也要保持一致:
ts
createWebHistory('/admin/')6.3 publicDir
指定公共资源目录,默认是 public。
ts
export default defineConfig({
publicDir: 'public',
})如果设置为 false,表示禁用公共目录。
6.4 cacheDir
依赖预构建缓存目录,默认是 node_modules/.vite。
ts
export default defineConfig({
cacheDir: 'node_modules/.vite',
})一般不用修改。Monorepo 中有时会调整缓存位置。
6.5 define
用来定义编译时常量。
ts
export default defineConfig({
define: {
__APP_VERSION__: JSON.stringify('1.0.0'),
},
})业务代码中可以使用:
ts
console.log(__APP_VERSION__)注意:define 是静态替换,不适合放运行时才变化的值。
7. 开发服务器 server 配置
7.1 固定端口
ts
export default defineConfig({
server: {
port: 5173,
strictPort: true,
},
})port:期望端口。strictPort: true:端口被占用就直接报错,不自动换端口。
学习时可以不写 strictPort。团队项目建议写清楚,避免不同人启动地址不一致。
7.2 允许局域网访问
ts
export default defineConfig({
server: {
host: '0.0.0.0',
},
})这样同一局域网的手机或其他电脑可以访问你的开发服务。
7.3 自动打开浏览器
ts
export default defineConfig({
server: {
open: true,
},
})也可以打开指定路径:
ts
open: '/login'7.4 HTTPS 开发
ts
export default defineConfig({
server: {
https: true,
},
})如果业务涉及安全上下文能力,例如某些浏览器 API、Cookie 策略、OAuth 回调,本地 HTTPS 可能会用到。
8. 路径别名与模块解析
路径别名最常见写法:
ts
import path from 'node:path'
import { defineConfig } from 'vite'
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
})之后可以这样写:
ts
import UserCard from '@/components/UserCard.vue'而不是:
ts
import UserCard from '../../../components/UserCard.vue'8.1 TypeScript 也要同步配置
只配 Vite 不够。TypeScript 编辑器提示也需要知道 @ 指向哪里。
tsconfig.json 或 tsconfig.app.json:
json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}常见错误:
- 项目能运行,但编辑器报找不到模块。
- 原因通常是只配了
vite.config.ts,没配tsconfig。
8.2 扩展名解析
Vite 默认会解析常见扩展名。不要滥用省略扩展名,尤其在库模式或跨工具链项目中,明确一些反而更稳。
9. 静态资源、CSS 与预处理器
9.1 src/assets 与 public 的区别
放在 src/assets 的资源:
- 可以通过
import引入。 - 会参与构建处理。
- 构建后通常带 hash,利于缓存。
ts
import logoUrl from '@/assets/logo.png'放在 public 的资源:
- 不经过构建转换。
- 以根路径访问。
- 文件名不会自动 hash。
html
<img src="/favicon.ico" />判断规则:
- 业务里 import 的资源放
src/assets。 - 必须固定路径访问的资源放
public。
9.2 CSS 引入
在入口引入全局样式:
ts
import './styles/main.css'在 Vue 组件中写局部样式:
vue
<style scoped>
.title {
color: #42b883;
}
</style>9.3 CSS Modules
文件命名:
text
Button.module.css使用:
ts
import styles from './Button.module.css'
console.log(styles.primary)适合需要样式局部隔离的场景。
9.4 Sass / Less
安装 Sass:
bash
npm install sass -DVue 组件中使用:
vue
<style lang="scss" scoped>
$brand: #42b883;
.title {
color: $brand;
}
</style>9.5 全局注入 Sass 变量
ts
export default defineConfig({
css: {
preprocessorOptions: {
scss: {
additionalData: `@use "@/styles/variables.scss" as *;`,
},
},
},
})不要把大量真实 CSS 注入到每个文件里。这里更适合放变量、mixin、函数。
9.6 PostCSS
Vite 会自动读取 PostCSS 配置。
postcss.config.js:
js
export default {
plugins: {
autoprefixer: {},
},
}如果使用 Tailwind CSS,也通常通过 PostCSS 或官方集成接入。
10. 环境变量与模式
Vite 使用 .env 文件管理环境变量。
常见文件:
text
.env
.env.local
.env.development
.env.development.local
.env.production
.env.production.local10.1 加载顺序
一般理解为:
.env:所有模式都会加载。.env.local:所有模式都会加载,但通常不提交 Git。.env.[mode]:指定模式加载。.env.[mode].local:指定模式加载,通常不提交 Git。
模式由命令决定:
bash
vite --mode staging
vite build --mode staging会加载:
text
.env
.env.local
.env.staging
.env.staging.local10.2 只有 VITE_ 前缀会暴露给客户端
.env.development:
env
VITE_API_BASE_URL=http://localhost:3000
VITE_APP_TITLE=学习系统
DB_PASSWORD=should-not-expose业务代码:
ts
console.log(import.meta.env.VITE_API_BASE_URL)
console.log(import.meta.env.VITE_APP_TITLE)DB_PASSWORD 不会暴露到客户端代码中。
这很重要:前端环境变量不是服务器秘密。只要暴露到客户端,用户就可能在构建产物里看到。
10.3 内置变量
Vite 提供一些内置变量:
ts
import.meta.env.MODE
import.meta.env.BASE_URL
import.meta.env.PROD
import.meta.env.DEV
import.meta.env.SSR常见判断:
ts
if (import.meta.env.DEV) {
console.log('只在开发环境打印')
}10.4 在 vite.config.ts 中读取 env
vite.config.ts 运行在 Node.js 环境,不是浏览器环境。要读取 .env,使用 loadEnv。
ts
import { defineConfig, loadEnv } from 'vite'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), 'VITE_')
return {
server: {
proxy: {
'/api': {
target: env.VITE_PROXY_TARGET,
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, ''),
},
},
},
}
})如果第三个参数传空字符串 '',会加载所有变量:
ts
loadEnv(mode, process.cwd(), '')但不要轻易把敏感变量暴露给客户端。
11. TypeScript 在 Vite 中的正确用法
Vite 天然支持 TypeScript 文件转换,但这不等于它会完整类型检查。
11.1 Vite 做什么
开发和构建时,Vite 会快速转换 TypeScript:
ts
const count: number = 1转换成浏览器能运行的 JavaScript。
11.2 Vite 默认不负责完整类型检查
为了速度,Vite 通常只做转译,不阻塞完整类型检查。
Vue + TS 项目常见脚本:
json
{
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"type-check": "vue-tsc -b"
}
}这样生产构建前会先做类型检查。
11.3 环境变量类型声明
src/vite-env.d.ts:
ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_BASE_URL: string
readonly VITE_APP_TITLE: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}这样写之后,import.meta.env.VITE_API_BASE_URL 才有类型提示。
11.4 路径别名类型同步
再次强调:别名要同时配 Vite 和 TypeScript。
vite.config.ts 负责运行和构建:
ts
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
}tsconfig 负责编辑器和类型检查:
json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}12. 插件机制与常见插件
Vite 插件基于 Rolldown/Rollup 兼容风格的插件接口扩展,并增加了一些开发服务器相关能力。
最常见插件:
ts
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
})12.1 插件能做什么
插件可以参与:
- 解析模块。
- 转换代码。
- 处理虚拟模块。
- 修改 HTML。
- 注入开发服务器中间件。
- 影响生产构建。
12.2 插件执行时机
部分插件支持:
ts
{
enforce: 'pre'
}或:
ts
{
enforce: 'post'
}含义:
pre:尽量提前执行。- 默认:普通阶段执行。
post:尽量靠后执行。
12.3 常见插件类型
Vue 项目常见:
@vitejs/plugin-vue:处理 Vue 单文件组件。@vitejs/plugin-vue-jsx:支持 Vue JSX。vite-plugin-inspect:观察插件转换过程。rollup-plugin-visualizer:分析构建产物体积。vite-plugin-pwa:接入 PWA。
不要为了“看起来高级”堆插件。每个插件都会增加维护成本。
13. 生产构建 build 配置
Vite 的生产构建常见配置:
ts
export default defineConfig({
build: {
outDir: 'dist',
assetsDir: 'assets',
sourcemap: false,
minify: 'oxc',
target: 'es2018',
cssCodeSplit: true,
rolldownOptions: {
output: {
advancedChunks: {
groups: [
{ name: 'vue', test: /[\\/]node_modules[\\/](vue|vue-router|pinia)[\\/]/ },
],
},
},
},
},
})13.1 outDir
构建输出目录,默认是 dist。
13.2 assetsDir
静态资源输出目录,默认是 assets。
构建后可能类似:
text
dist/
├── assets/
│ ├── index-a1b2c3.js
│ └── index-d4e5f6.css
└── index.html13.3 sourcemap
是否生成 sourcemap。
ts
sourcemap: true优点:
- 线上错误可以映射回源码。
风险:
- 如果公开 sourcemap,可能暴露源码结构。
常见做法:
- 内部测试环境打开。
- 生产环境上传到错误监控平台,但不公开访问。
13.4 minify
Vite 8 默认使用 Oxc 压缩,速度快。
ts
minify: 'oxc'如果需要更细的压缩控制,可以使用 terser(terser 在 Vite 8 中已是可选依赖,使用前需先 npm install -D terser):
ts
minify: 'terser'但通常没有必要一开始就切换。
13.5 rolldownOptions
Vite 8 生产构建底层基于 Rolldown,因此优先使用 build.rolldownOptions 透传打包配置。build.rollupOptions 在 Vite 8 类型定义中仍保留兼容入口,但已经标记为建议迁移到 rolldownOptions。
常用场景:
- 自定义 chunk。
- 外部化某些依赖。
- 配置多入口。
- 调整输出文件名。
14. 代码分割、缓存与性能优化
14.1 动态导入
最基础的代码分割方式:
ts
const UserPage = () => import('@/views/UserPage.vue')路由懒加载:
ts
const routes = [
{
path: '/users',
component: () => import('@/views/UserList.vue'),
},
]这样用户第一次进入首页时,不一定要下载所有页面代码。
14.2 手动拆包(Rolldown advancedChunks)
可以把稳定依赖拆成单独 chunk。Vite 8 生产构建底层是 Rolldown:旧 Rollup 风格的 output.manualChunks 对象写法已被移除、函数写法也已废弃,Rolldown 推荐使用 advancedChunks:
ts
build: {
rolldownOptions: {
output: {
advancedChunks: {
groups: [
{ name: 'vue', test: /[\\/]node_modules[\\/](vue|vue-router|pinia)[\\/]/ },
{ name: 'ui', test: /[\\/]node_modules[\\/]element-plus[\\/]/ },
],
},
},
},
}不要过度拆包。拆得太碎会增加请求和调度成本。
14.3 浏览器缓存策略
Vite 默认构建文件名带 hash:
text
index-a1b2c3.js这让你可以对静态资源设置长缓存:
nginx
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}index.html 不应该长缓存,因为它负责引用最新资源。
14.4 构建体积分析
安装:
bash
npm install rollup-plugin-visualizer -D配置:
ts
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
visualizer({
filename: 'dist/stats.html',
open: true,
}),
],
})用于检查:
- 哪些依赖体积大。
- 是否重复打包。
- 是否可以懒加载。
15. 代理、跨域与本地联调
开发时经常遇到跨域问题。Vite 可以配置本地代理。
ts
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, ''),
},
},
},
})业务代码:
ts
fetch('/api/users')浏览器请求的是 Vite 开发服务器,Vite 再转发到后端。
15.1 注意开发代理不等于生产代理
server.proxy 只对开发服务器生效。
生产环境要在 Nginx、网关、后端服务或部署平台上配置代理。
不要以为开发环境能请求成功,生产环境就一定能成功。
15.2 使用 env 管理代理目标
.env.development:
env
VITE_PROXY_TARGET=http://localhost:3000vite.config.ts:
ts
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), 'VITE_')
return {
server: {
proxy: {
'/api': {
target: env.VITE_PROXY_TARGET,
changeOrigin: true,
},
},
},
}
})16. 部署上线与 base 路径
16.1 构建
bash
npm run build构建结果默认输出到:
text
dist/16.2 本地预览
bash
npm run preview这个命令用于检查构建结果是否能正常访问。
16.3 部署到域名根路径
如果访问地址是:
text
https://example.com/通常:
ts
base: '/'16.4 部署到子路径
如果访问地址是:
text
https://example.com/admin/配置:
ts
base: '/admin/'路由:
ts
createWebHistory('/admin/')Nginx:
nginx
location /admin/ {
try_files $uri $uri/ /admin/index.html;
}16.5 SPA 刷新 404
Vue Router 使用 history 模式时,直接刷新 /users/1 可能 404。
原因:
- 浏览器请求
/users/1。 - 静态服务器找不到真实文件。
- 应该回退到
index.html。
Nginx 根路径部署:
nginx
location / {
try_files $uri $uri/ /index.html;
}17. 进阶主题:库模式、SSR、Monorepo
17.1 库模式
如果你要发布一个组件库或工具库,可以使用库模式:
ts
import { defineConfig } from 'vite'
import path from 'node:path'
export default defineConfig({
build: {
lib: {
entry: path.resolve(__dirname, 'src/index.ts'),
name: 'MyLib',
fileName: 'my-lib',
},
rolldownOptions: {
external: ['vue'],
output: {
globals: {
vue: 'Vue',
},
},
},
},
})关键点:
entry是库入口。external表示不把某些依赖打进库里。- 组件库通常应该把
vue作为 peer dependency。
17.2 SSR
SSR 是服务端渲染。Vite 支持 SSR 开发能力,但完整 SSR 应用通常会使用更上层框架,例如 Nuxt。
学习顺序建议:
- 先掌握普通 SPA。
- 再理解构建和路由。
- 最后学习 SSR 的服务端入口、客户端激活、数据预取。
17.3 Monorepo
Monorepo 中常见问题:
- 多包路径解析。
- 依赖预构建边界。
- workspace 包是否需要转译。
- 缓存目录位置。
常见工具:
- pnpm workspace。
- Turborepo。
- Nx。
初学阶段不要一上来就 Monorepo。先把单应用项目跑熟。
18. Vite 与 Webpack 的关系
Vite 和 Webpack 都能完成前端工程化,但核心策略不同。
18.1 开发阶段
Vite:
- 依赖浏览器原生 ESM。
- 按需转换源码。
- 开发启动快。
Webpack:
- 以完整依赖图和 bundle 为中心。
- 开发服务器能力成熟。
- 老项目和复杂定制场景多。
18.2 生产构建
Vite:
- Vite 8 默认使用 Rolldown 构建。
- 配置相对简洁。
- 适合现代应用和库。
Webpack:
- 插件和 loader 生态庞大。
- 配置能力细。
- 大量存量项目仍依赖它。
18.3 应该怎么学
推荐顺序:
- 先用 Vite 把项目做起来。
- 再学 Webpack 理解构建工具底层概念。
- 最后比较两者在开发、构建、插件、优化上的差异。
19. 常见问题与排查思路
19.1 端口被占用
现象:
text
Port 5173 is already in use解决:
- 关闭占用端口的进程。
- 修改
server.port。 - 不设置
strictPort,让 Vite 自动换端口。
19.2 别名运行正常但 TS 报错
原因:
- 只配置了
vite.config.ts。 - 没配置
tsconfig的paths。
解决:
json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}19.3 环境变量读取不到
检查:
- 是否以
VITE_开头。 - 是否重启了开发服务器。
- 是否加载了正确 mode。
- 是否在
vite.config.ts中使用了loadEnv。
19.4 生产环境接口地址不对
检查:
.env.production是否正确。- 构建命令是否使用了正确 mode。
- 前端是否把开发代理误当成生产代理。
- 部署平台是否配置了反向代理。
19.5 部署后白屏
常见原因:
base配错。- 路由 history 模式缺少回退配置。
- 静态资源路径不正确。
- 浏览器控制台有 JS 报错。
排查顺序:
- 打开浏览器控制台。
- 看 Network 是否有 404。
- 看资源路径是否带错前缀。
- 检查
base和路由 base。 - 检查服务器 fallback。
20. 推荐学习顺序
第一阶段:会用
目标:
- 能创建 Vue + Vite 项目。
- 能启动、构建、预览。
- 能理解
src、public、index.html、vite.config.ts。
练习:
- 创建一个 Vue + TS 项目。
- 配置
@别名。 - 添加一个全局样式文件。
- 写一个接口请求基础封装。
第二阶段:会配
目标:
- 会配置
server.proxy。 - 会配置
.env.development和.env.production。 - 会处理
base和部署子路径。 - 会配置 Sass 全局变量。
练习:
- 准备两个环境接口地址。
- 开发环境走代理。
- 生产环境使用真实 API 地址。
- 把项目部署到
/admin/子路径。
第三阶段:懂原理
目标:
- 理解 ESM 按需加载。
- 理解依赖预构建。
- 理解 HMR。
- 理解 Vite 生产构建和 Rolldown 的关系,以及旧版 Rollup 生态的迁移方式。
练习:
- 观察浏览器 Network 中模块请求。
- 删除
.vite缓存后重新启动。 - 使用
rollup-plugin-visualizer看构建体积。
第四阶段:能优化
目标:
- 会做路由懒加载。
- 会分析大依赖。
- 会处理 sourcemap。
- 会制定缓存策略。
练习:
- 给每个路由加动态导入。
- 分析构建产物体积。
- 设置 Nginx 静态资源长缓存。
21. 核心机制深度补全
本章补全 Vite 学习中最容易停留在“启动快、热更新快”的部分。你需要能解释 Vite 开发服务器如何利用 ESM,为什么需要依赖预构建,HMR 如何传播,插件如何介入开发和构建两个阶段。
21.1 Vite 开发阶段的请求链路
浏览器访问 Vite dev server 时,入口不是打包后的 bundle,而是原生 ESM。
text
浏览器请求 /
-> Vite 返回 index.html
-> 浏览器发现 <script type="module" src="/src/main.ts">
-> 请求 /src/main.ts
-> Vite 转换 TS/Vue/CSS/import 路径
-> 浏览器继续按 import 请求依赖模块这和 Webpack dev server 最大区别是:
- Webpack 开发时通常先构建一份 bundle,再交给浏览器。
- Vite 开发时按浏览器请求即时转换源码模块。
所以项目很大时,Vite 不需要一开始把整个应用都打包完,启动通常更快。
21.2 源码模块与依赖模块
Vite 把模块大致分两类:
| 类型 | 例子 | 处理方式 |
|---|---|---|
| 源码模块 | src/App.vue、src/main.ts | 按需转换,支持 HMR |
| 依赖模块 | vue、lodash-es、axios | 预构建并缓存 |
源码模块经常变化,适合按需转换。依赖模块变化少,但可能包含 CommonJS、多文件深层 import,如果每次都让浏览器逐个请求,会很慢,所以 Vite 会做依赖预构建。
21.3 依赖预构建
依赖预构建主要解决两个问题:
- 把 CommonJS/UMD 依赖转换为浏览器可直接加载的 ESM。
- 把有大量内部模块的依赖合并,减少浏览器请求数量。
缓存位置通常在:
text
node_modules/.vite/当依赖变化、lockfile 变化、配置变化时,Vite 会重新预构建。
常见配置:
typescript
import { defineConfig } from "vite";
export default defineConfig({
optimizeDeps: {
include: ["lodash-es"],
exclude: ["some-local-linked-package"],
},
});什么时候手动 include:
- 某个依赖是动态引入,Vite 扫描阶段没发现。
- 第一次访问某页面时才触发依赖构建,导致页面短暂卡顿。
什么时候 exclude:
- 本地 link 的包需要保留源码形态参与 HMR。
- 某些包预构建后行为异常,需要交给源码转换链路处理。
排查方式:
- 删除
node_modules/.vite。 - 使用
vite --force强制重新预构建。 - 看终端中的 dependency optimized 提示。
- 检查 lockfile 是否变化。
21.3.1 预构建的扫描、触发与缓存失效
预构建不是每次启动都重新做。首次启动(或缓存失效时)的内部流程是:
text
没有可用缓存
-> Vite 扫描源码,自动发现 bare import(期望从 node_modules 解析的导入)
-> 把这些依赖作为预构建入口
-> 用 Rolldown 执行预构建(所以通常很快)
-> 产物写入 node_modules/.vite服务已经启动之后,如果浏览器访问到一个缓存里没有的新依赖(例如只有某个懒加载页面才 import 的库),Vite 会重新运行依赖预构建,并在必要时自动刷新页面。这就是为什么"第一次点进某个页面时卡了一下"——对应解法就是把这类依赖提前写进 optimizeDeps.include。
官方文档明确列出,是否重新预构建取决于以下几个来源是否变化:
| 来源 | 说明 |
|---|---|
| 包管理器 lockfile 内容 | package-lock.json、pnpm-lock.yaml、yarn.lock、bun.lock 等 |
| patches 目录的修改时间 | 给依赖打补丁后需要重新预构建 |
vite.config.* 中相关字段 | 例如 optimizeDeps、alias 等影响解析的配置 |
NODE_ENV 的值 | 开发和生产取值不同会影响依赖行为 |
只有上述任一项变化时才会重新预构建,否则直接复用缓存。
两个容易忽略的细节:
- 浏览器缓存:开发时解析后的依赖请求带有强缓存响应头
max-age=31536000,immutable,一旦进入浏览器缓存就不会再打到 dev server。所以"改了依赖源码但浏览器还在用旧版本"时,需要在 Network 面板禁用缓存、或用--force重启后强刷页面。 - 自定义预构建行为:
optimizeDeps.rolldownOptions可以继续透传 Rolldown 配置,例如给依赖里的特殊文件加插件、调整构建 target。
⚠️ Monorepo 中 link 的本地包默认被当作源码处理(不预构建),前提是它以 ESM 形式导出;不是 ESM 时要手动 optimizeDeps.include,修改这类本地包后要用 --force 重启才能让改动生效。
21.4 Vite 如何处理 Vue 单文件组件
.vue 文件不是浏览器原生模块,Vite 通过 @vitejs/plugin-vue 转换。
一个 SFC 大致会被拆成:
text
App.vue
├── template -> render function
├── script/script setup -> JavaScript module
└── style -> CSS update当你修改 <template>:
- 插件重新编译 render 函数。
- HMR 尽量保留组件状态。
当你修改 <style>:
- 只更新样式。
- 通常不重载整个页面。
当你修改 <script> 中影响组件边界的内容:
- 可能触发组件热替换。
- 某些情况下会整页 reload。
21.5 HMR 的传播模型
HMR 不是“文件变了就刷新页面”,而是沿模块依赖图寻找可以接受更新的边界。
text
file changed
-> Vite 发现变更模块
-> 找到 importers
-> 判断模块或上层模块是否 accept HMR
-> 可以热替换:发送 update
-> 不可热替换:整页 reload普通模块可以手写 HMR 接收:
javascript
export function add(a, b) {
return a + b;
}
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
console.log("module updated", newModule);
});
}Vue 项目通常不需要自己写这段,因为插件已经处理了组件级 HMR。
HMR 失效常见原因:
- 模块有不可恢复的副作用。
- 修改了入口文件或全局初始化逻辑。
- 循环依赖导致边界不清晰。
- 插件没有正确实现 HMR。
- 依赖被预构建后不参与源码级 HMR。
21.5.1 import.meta.hot 客户端 API
框架插件已经帮你处理了组件级 HMR,但理解手动 HMR API 能解释很多"为什么热更新这样表现"。Vite 通过 import.meta.hot 暴露这套 API,官方定位是主要给框架和工具作者使用,业务代码一般不需要直接写。
第一个规则:所有 HMR 代码都要包在条件块里,生产构建才能把这段代码 tree-shake 掉:
ts
if (import.meta.hot) {
// HMR 代码
}常用方法一览:
| API | 作用 | 要点 |
|---|---|---|
hot.accept(cb) | 模块自接受更新,成为 HMR 边界 | 回调收到新模块;语法错误时新模块为 undefined |
hot.accept(deps, cb) | 接受指定依赖模块的更新 | 可以传一个或一组依赖路径 |
hot.dispose(cb) | 模块被替换前清理副作用 | 比如移除定时器、事件监听 |
hot.prune(cb) | 模块不再被页面导入时清理 | 比 dispose 更宽松,Vite 内部用它处理 .css 导入 |
hot.data | 跨模块新旧实例共享的数据对象 | 只能修改属性,不能整体重新赋值 |
hot.invalidate(message?) | 主动放弃本次更新,向 importers 传播 | 会同时打印到浏览器控制台和终端 |
hot.on(event, cb) / hot.off | 监听 HMR 事件 | 例如 vite:beforeUpdate、vite:error、vite:beforeFullReload |
hot.send(event, data) | 客户端向 dev server 发自定义事件 | 连接建立前发送的数据会被缓冲 |
两个容易踩坑的实现细节:
⚠️ 静态分析要求:Vite 要求源码中必须出现 import.meta.hot.accept( 这个字面调用(对空白敏感),才会把模块标记为可接受更新。把 import.meta.hot 存到变量里再调用、或拼出调用表达式,都可能让静态分析失效。
💡 不会真的"交换"模块:Vite 的 HMR 并不会把原来导入的模块对象替换掉——如果边界模块 re-export 了依赖的内容,更新这些 re-export 是边界模块自己的责任(对应导出要用 let 声明),边界之上的 importers 也不会收到通知。这个简化实现省掉了生成代理模块的高昂成本,对绝大多数开发场景够用。
invalidate 的正确用法是配合 accept 一起:先 accept 保证客户端持续监听该模块的后续变更,再在回调里判断是否需要把更新向上传播:
ts
import.meta.hot.accept((module) => {
if (!canHandleUpdate(module)) {
import.meta.hot.invalidate('状态无法迁移,需要上层处理')
}
})内置事件里最常用的是 vite:error(语法错误等)、vite:ws:disconnect(WebSocket 断开,通常意味着 dev server 重启)和 vite:beforeFullReload(整页刷新前)。
21.6 插件机制:开发与构建共用 Rolldown/Rollup 风格
Vite 插件继承 Rolldown/Rollup 兼容风格的插件模型,并增加了开发服务器相关钩子。
常见钩子:
| 钩子 | 阶段 | 作用 |
|---|---|---|
config | 配置解析前 | 修改配置 |
configResolved | 配置解析后 | 读取最终配置 |
configureServer | dev server | 添加中间件、读取 server |
resolveId | 解析模块 | 改写模块路径 |
load | 加载模块 | 返回虚拟模块内容 |
transform | 转换模块 | 编译源码 |
handleHotUpdate | HMR | 自定义热更新行为 |
generateBundle | build | 修改构建产物 |
虚拟模块示例:
typescript
function virtualBuildTimePlugin() {
const virtualId = "virtual:build-time";
const resolvedId = "\0" + virtualId;
return {
name: "virtual-build-time",
resolveId(id) {
if (id === virtualId) return resolvedId;
},
load(id) {
if (id === resolvedId) {
return `export const buildTime = ${JSON.stringify(new Date().toISOString())}`;
}
},
};
}业务代码:
javascript
import { buildTime } from "virtual:build-time";
console.log(buildTime);21.7 环境变量与安全边界
Vite 只会把以 VITE_ 开头的环境变量暴露给客户端。
text
VITE_API_BASE_URL=https://api.example.com
DB_PASSWORD=secret客户端可以读取:
javascript
console.log(import.meta.env.VITE_API_BASE_URL);不能读取:
javascript
console.log(import.meta.env.DB_PASSWORD); // undefined原则:
- 不要把数据库密码、服务端密钥、私有 token 放进
VITE_变量。 - 前端环境变量最终会进入构建产物,用户可以看到。
- 真正的密钥必须留在服务端。
21.8 生产构建与 Rolldown
Vite 8 生产构建底层使用 Rolldown。开发阶段按需 ESM,生产阶段仍然要打包,因为浏览器生产环境需要:
- 更少请求。
- 压缩代码。
- Tree Shaking。
- 代码分割。
- 文件 hash。
- CSS 抽取。
- 资源路径重写。
常见构建配置:
typescript
import { defineConfig } from "vite";
export default defineConfig({
build: {
sourcemap: false,
assetsDir: "assets",
rolldownOptions: {
output: {
advancedChunks: {
groups: [
{ name: "vue", test: /[\\/]node_modules[\\/](vue|vue-router|pinia)[\\/]/ },
],
},
},
},
},
});不要过早手动拆包。先用分析工具看真实产物,再决定是否用 advancedChunks 拆包。
21.9 CSS 和静态资源处理
Vite 对 CSS 的处理包括:
- 支持直接 import CSS。
- 支持 CSS Modules。
- 支持 Sass/Less/Stylus 等预处理器。
- 生产构建时抽取、压缩、加 hash。
- 处理
url()中的资源路径。
javascript
import "./style.css";
import logoUrl from "./logo.svg";
console.log(logoUrl);public 目录适合放不会经过构建处理的静态资源:
text
public/favicon.ico -> /favicon.icosrc/assets 中的资源会被构建工具处理、hash、优化路径。
选择:
- 需要被 import、hash、参与构建:放
src/assets。 - 固定路径、无需处理:放
public。
21.10 Monorepo 与本地包
Monorepo 中常见问题:
- 本地包是否被当作源码处理。
- 本地包是否被预构建。
- TS 路径别名和 Vite alias 是否一致。
- 多份 Vue 实例导致响应式失效。
排查重复 Vue:
bash
npm ls vue
pnpm why vue配置 alias:
typescript
import { fileURLToPath, URL } from "node:url";
export default defineConfig({
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
dedupe: ["vue"],
},
});dedupe 可以避免依赖解析出多份同名包,Vue 组件库联调时尤其重要。
21.11 Vite 排查清单
- 启动慢:检查依赖预构建、插件数量、Monorepo 本地包。
- HMR 不生效:看是否整页 reload、模块是否有副作用、插件是否支持 HMR。
- 页面白屏:检查控制台错误、资源 404、
base配置。 - 环境变量 undefined:确认变量以
VITE_开头,重启 dev server。 - 代理不生效:确认请求路径命中
server.proxy,生产环境另配 Nginx/网关。 - 构建后路径错误:检查
base、部署子路径、路由 history base。 - 包体积过大:用 visualizer 分析,再拆分大依赖。
- sourcemap 泄露:生产环境按安全策略关闭或上传到错误监控平台。
21.12 版本基线:Vite 8 之后要先修正旧认知
本文本次补全按 2026-07-09 核对的 npm 版本书写:
| 包 | 核对版本 | 说明 |
|---|---|---|
vite | 8.1.4 | Vite 主包 |
@vitejs/plugin-vue | 6.0.7 | Vue SFC 官方 Vite 插件 |
postcss | 8.5.16 | CSS AST 转换工具 |
💡 2026-09-01 复核:npm 上 vite latest 已进入 8.2.x(核对时为 8.2.2),本文 21.3.1、21.19、21.20 的内容按该版本核实。小版本迭代以官方 changelog 为准。
Vite 8 需要注意这些变化:
- 生产构建主线从 Rollup 转向 Rolldown,配置优先使用
build.rolldownOptions。 build.rollupOptions在 Vite 8 类型定义里仍存在,但标记为建议迁移到rolldownOptions。- JS 压缩默认方向是
oxc,build.minify允许false、true、"oxc"、"terser"、"esbuild";其中"esbuild"已废弃(deprecated),官方建议迁移到"oxc"。 - CSS 压缩默认方向是
lightningcss,build.cssMinify允许false、true、"lightningcss"、"esbuild"。 - 插件模型仍保留 Rollup 风格的
resolveId/load/transform/generateBundle,但 Vite 8 类型上是围绕 Rolldown 插件接口扩展。 - 学旧教程时看到
rollupOptions、esbuild minify、esbuild 依赖预构建,要判断它是 Vite 7 或更早版本的表述。
21.13 PostCSS 在 Vite 中的配置方法与作用
PostCSS 本质上不是 CSS 预处理器。它是“CSS AST 转换器”:读取 CSS,交给插件处理 AST,再输出 CSS。Autoprefixer、postcss-preset-env、px 转 rem、CSS 嵌套、Tailwind CSS v4 的 @tailwindcss/postcss 都属于 PostCSS 插件生态。
21.13.1 推荐方式一:独立 PostCSS 配置文件
适合团队项目,因为 Vite、Webpack、Storybook、组件库构建工具都可能复用同一份 PostCSS 配置。
js
// postcss.config.cjs
module.exports = {
plugins: {
autoprefixer: {},
'postcss-preset-env': {
stage: 3,
features: {
'nesting-rules': true,
},
},
},
};数组写法也可以,适合插件需要 require() 或函数调用的场景:
js
// postcss.config.cjs
module.exports = {
plugins: [
require('autoprefixer')(),
require('postcss-pxtorem')({
rootValue: 16,
propList: ['*'],
exclude: /node_modules/i,
}),
],
};常见文件名:
postcss.config.jspostcss.config.cjspostcss.config.mjs.postcssrc.postcssrc.json.postcssrc.js
实践建议:
- 如果项目
package.json使用"type": "module",优先用postcss.config.cjs避免 CommonJS/ESM 混乱。 - 插件顺序会影响结果。一般先做语法转换,再做兼容性和单位转换。
- 和 Vite 配置分离后,测试、文档站、组件库可以复用同一套 CSS 处理规则。
21.13.2 推荐方式二:写在 vite.config.ts 的 css.postcss
适合小项目、临时示例、组件库内部构建。
ts
import { defineConfig } from 'vite';
import autoprefixer from 'autoprefixer';
import postcssPresetEnv from 'postcss-preset-env';
export default defineConfig({
css: {
postcss: {
plugins: [
postcssPresetEnv({
stage: 3,
features: {
'nesting-rules': true,
},
}),
autoprefixer(),
],
},
},
});注意:一旦写了内联 css.postcss,团队成员通常会优先在 vite.config.ts 找 CSS 处理规则。如果你同时存在外部 postcss.config.*,要避免两边重复配置同一插件。
21.13.3 css.postcss 每个常见配置的作用
css.postcss 的类型来自 PostCSS ProcessOptions,Vite 8 类型定义允许它是字符串,或包含 plugins 的 PostCSS 处理选项对象。实际项目中最常用的是对象写法。
| 配置 | 作用 | 常见使用场景 |
|---|---|---|
plugins | PostCSS 插件列表,按顺序处理 CSS AST | 自动补浏览器前缀、CSS 新语法转换、px 转 rem、Tailwind |
map | 控制 PostCSS source map | 开发调试 CSS 转换来源 |
parser | 自定义 CSS 解析器 | 特殊语法输入,例如某些非标准 CSS 语法 |
syntax | 同时提供 parser 和 stringifier 的语法包 | 处理成套非标准语法 |
stringifier | 自定义 CSS 输出器 | 需要特殊输出格式时使用,普通业务很少需要 |
from | 输入文件名,影响 source map 和部分插件定位 | 一般由 Vite 自动传入,不建议手写 |
to | 输出文件名,影响 source map 和部分资源插件 | 一般由 Vite 自动处理 |
plugins 是你最常用、也最应该理解的配置。
ts
postcss: {
plugins: [
pluginA(),
pluginB(),
],
}处理顺序可以理解为:
text
CSS 源码
-> parse 成 AST
-> pluginA 修改 AST
-> pluginB 修改 AST
-> stringify 输出 CSS
-> sourcemap 合并21.13.4 Vite css.* 每个关键配置的作用
PostCSS 不是 Vite CSS 配置的全部。Vite 的 css 配置通常这样看:
| Vite 配置 | 作用 | 示例 |
|---|---|---|
css.transformer | 选择 CSS 转换器,Vite 8 类型中为 "postcss" 或 "lightningcss",默认 "postcss" | 想实验 Lightning CSS 处理 imports/modules/assets 时设置 |
css.postcss | 配置 PostCSS 处理选项或指定配置查找 | 配置 autoprefixer、preset-env、px 转 rem |
css.modules | 配置 CSS Modules | localsConvention、generateScopedName |
css.preprocessorOptions | 配置 Sass/Less/Stylus 等预处理器 | 注入全局变量、Less modifyVars |
css.preprocessorMaxWorkers | 控制预处理器 worker 数量 | 大型 Sass/Less 项目调优 |
css.devSourcemap | 开发环境是否生成 CSS sourcemap | 样式调试 |
css.lightningcss | 使用 Lightning CSS 时的选项 | CSS targets、drafts 等 |
完整示例:
ts
import { fileURLToPath, URL } from 'node:url';
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import autoprefixer from 'autoprefixer';
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
css: {
transformer: 'postcss',
devSourcemap: true,
modules: {
localsConvention: 'camelCaseOnly',
generateScopedName: '[name]__[local]___[hash:base64:5]',
},
preprocessorOptions: {
scss: {
additionalData: '@use "@/styles/tokens.scss" as *;',
},
less: {
javascriptEnabled: true,
modifyVars: {
'@primary-color': '#2563eb',
},
},
},
postcss: {
plugins: [
autoprefixer(),
],
},
},
});21.13.5 PostCSS、Sass/Less、CSS Modules 的先后关系
以 App.vue 中的 <style lang="scss" module> 为例,简化链路是:
text
@vitejs/plugin-vue 解析 .vue
-> 提取 style block,生成 ?vue&type=style 请求
-> Sass 把 SCSS 转成普通 CSS
-> PostCSS 插件处理普通 CSS
-> CSS Modules 生成 class 映射
-> Vite 注入 dev style 或 build 时抽取 CSS这里有三个容易混淆的点:
lang="scss"不是 PostCSS 做的,是 Sass 预处理器做的。autoprefixer、postcss-preset-env这类才是 PostCSS 插件做的。<style scoped>是 Vue SFC 编译能力的一部分,会给选择器加作用域标记;它和 CSS Modules 是两个不同机制。
21.13.6 常见 PostCSS 配置场景
自动前缀:
js
// postcss.config.cjs
module.exports = {
plugins: {
autoprefixer: {},
},
};现代 CSS 降级:
js
// postcss.config.cjs
module.exports = {
plugins: {
'postcss-preset-env': {
stage: 3,
autoprefixer: true,
},
},
};移动端 px 转 rem:
js
// postcss.config.cjs
module.exports = {
plugins: {
'postcss-pxtorem': {
rootValue: 37.5,
propList: ['*'],
selectorBlackList: ['.no-rem'],
exclude: /node_modules/i,
},
},
};Tailwind CSS v4:
js
// postcss.config.cjs
module.exports = {
plugins: {
'@tailwindcss/postcss': {},
},
};21.13.7 PostCSS 排查清单
- 插件是否安装到当前项目依赖?
- 配置文件是 CommonJS 还是 ESM,和
package.json的"type"是否冲突? - 是否同时写了
postcss.config.*和css.postcss,导致配置来源混乱? - 插件顺序是否正确?
- 是否把 Sass/Less 语法误以为 PostCSS 能直接识别?
- source map 不准时,先打开
css.devSourcemap,再检查 PostCSSmap。 - 生产结果和开发结果不同,检查
browserslist、build.target、插件条件配置。
21.14 Vite 如何解析 Vue 单文件组件
.vue 文件不是浏览器原生能执行的模块。Vite 通过 @vitejs/plugin-vue 把一个 Vue SFC 拆成多个虚拟模块,再交给浏览器按 ESM 加载。
以 import App from './App.vue' 为例:
text
浏览器请求 /src/App.vue
-> Vite dev server 收到请求
-> plugin container 调用 @vitejs/plugin-vue 的 transform
-> plugin-vue 使用 @vue/compiler-sfc parse 生成 descriptor
-> 主模块代码导入 script/template/style 子模块
-> 浏览器继续请求 ?vue&type=script / ?vue&type=template / ?vue&type=style
-> 各子模块分别编译
-> 组合成最终组件21.14.1 SFC descriptor 是什么
descriptor 可以理解为 .vue 文件的结构化结果:
text
descriptor
├─ script
├─ scriptSetup
├─ template
├─ styles[]
├─ customBlocks[]
└─ id / filename / errors插件源码定位:
@vitejs/plugin-vue发布包:dist/index.mjs- 关键函数名:
createDescriptor、transformMain、compileScript、compileTemplate - GitHub 源码入口:https://github.com/vitejs/vite-plugin-vue/tree/main/packages/plugin-vue
源码级简化示例,非逐字复制:
ts
function transformVueMain(code, filename) {
const descriptor = parseVueSfc(code, filename);
const scriptRequest = `${filename}?vue&type=script`;
const templateRequest = `${filename}?vue&type=template`;
const styleRequests = descriptor.styles.map((_, index) =>
`${filename}?vue&type=style&index=${index}`
);
return `
import script from ${JSON.stringify(scriptRequest)}
import { render } from ${JSON.stringify(templateRequest)}
${styleRequests.map((id) => `import ${JSON.stringify(id)}`).join('\n')}
script.render = render
export default script
`;
}真实源码比这个复杂得多,还要处理:
<script setup>。- TypeScript。
- scoped CSS。
- CSS Modules。
- custom element。
- template 内联策略。
- HMR 边界判断。
- sourcemap。
- SSR。
21.14.2 <script setup> 怎么处理
<script setup> 会由 @vue/compiler-sfc 的 compileScript 编译成普通组件脚本。你写的:
vue
<script setup lang="ts">
import { ref } from 'vue';
const count = ref(0);
</script>会被编译成组件 setup() 相关代码,并根据模板使用情况暴露变量。Vite 自己不理解 Vue 语法,真正理解 SFC 语义的是 @vue/compiler-sfc,Vite 插件负责把它接到模块请求和 HMR 流程里。
21.14.3 <template> 怎么处理
模板会通过 compileTemplate 编译成 render 函数模块。开发时模板变更如果不影响脚本,通常可以只触发 rerender,而不用重新加载整个组件状态。
简化链路:
text
template 字符串
-> parse template AST
-> transform 指令、插值、事件
-> generate render 函数代码
-> import 到主模块21.14.4 <style scoped> 怎么处理
Vue scoped CSS 不是 Shadow DOM。它通常通过给元素和选择器加同一个作用域标记实现。
简化理解:
vue
<template>
<button class="primary">保存</button>
</template>
<style scoped>
.primary { color: blue; }
</style>会被处理成类似:
html
<button class="primary" data-v-xxxx>保存</button>css
.primary[data-v-xxxx] { color: blue; }然后再进入 Vite 的 CSS 处理链路,包括 PostCSS、CSS 注入或抽取。
21.15 Vite 如何解析路径、依赖和别名
Vite 路径解析发生在插件容器的 resolveId 阶段。你可以把它理解成“把源码里的 import 字符串解析成确定模块 id”的过程。
常见 import 类型:
| 写法 | 类型 | 解析方式 |
|---|---|---|
./foo | 相对路径 | 基于 importer 所在目录解析 |
/src/main.ts | 项目根绝对路径 | 基于 Vite root 解析 |
@/utils | alias | 先命中 resolve.alias,再继续解析 |
vue | bare import | 查 node_modules、package exports、优化依赖 |
/@fs/C:/x/a.ts | 文件系统绝对路径 | Vite 特殊路径,用于 root 外文件访问 |
virtual:xxx | 虚拟模块 | 由插件 resolveId/load 接管 |
21.15.1 alias 的正确配置
推荐写法:
ts
import { fileURLToPath, URL } from 'node:url';
import { defineConfig } from 'vite';
export default defineConfig({
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
});如果你在 TypeScript 中也使用 @/*,还要同步 tsconfig.json:
json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}Vite 8 类型中还出现了 resolve.tsconfigPaths?: boolean。如果项目启用它,可以减少手动同步 TS paths 和 Vite alias 的重复工作;但团队项目仍建议明确知道“运行时解析”和“类型检查解析”是两条链路。
21.15.2 alias 解析顺序
简化顺序:
text
import "@/views/Home.vue"
-> alias: "@" 命中 "/absolute/project/src"
-> 得到 "/absolute/project/src/views/Home.vue"
-> 检查扩展名、文件、目录入口
-> 作为模块 id 进入 load/transform插件排序中 alias 阶段非常靠前,所以它能影响后续几乎所有解析。
21.15.3 bare import 怎么走
ts
import { createApp } from 'vue';简化过程:
text
识别为 bare import
-> 根据 package.json exports/module/main 等字段解析入口
-> 如果是开发环境依赖,优先走优化依赖缓存
-> 重写成浏览器可请求的 URL
-> 浏览器请求优化后的 ESM 文件resolve 相关配置:
| 配置 | 作用 |
|---|---|
resolve.alias | 路径别名 |
resolve.dedupe | 强制指定依赖只解析到项目根,避免多份 Vue/React |
resolve.conditions | package exports 条件解析 |
resolve.mainFields | package 入口字段优先级 |
resolve.extensions | 省略扩展名时尝试的扩展 |
resolve.preserveSymlinks | 是否保留符号链接真实路径 |
resolve.tsconfigPaths | Vite 8 类型中提供的 tsconfig paths 解析开关 |
Vue 组件库联调时常见:
ts
export default defineConfig({
resolve: {
dedupe: ['vue'],
},
});这样可以降低出现两份 Vue 实例导致响应式、inject/provide 或组件判断异常的概率。
21.16 Vite 生命周期:每个周期做什么
Vite 的“生命周期”可以分成配置期、开发服务器期、模块请求期、HMR 期、生产构建期和预览期。
| 周期 | 触发时机 | 主要工作 | 常见插件钩子 |
|---|---|---|---|
| 配置加载 | 执行 vite 或 vite build | 查找并加载 vite.config.*,合并 CLI 参数 | config |
| 配置解析 | 用户配置合并默认配置后 | 解析 root、base、plugins、resolve、css、server、build、环境 | configResolved |
| 插件排序 | 配置解析中 | 按 alias、enforce: 'pre'、核心插件、普通插件、构建插件、post 排序 | 插件排序规则 |
| dev server 创建 | vite dev | 创建中间件容器、HTTP server、文件 watcher、WebSocket、模块图、环境 | configureServer |
| HTML 请求 | 访问 / 或 index.html | 转换 HTML、注入 Vite client、处理入口 script | transformIndexHtml |
| 模块解析 | 浏览器请求 JS/CSS/Vue 等模块 | 把 import 字符串解析为模块 id | resolveId |
| 模块加载 | 解析出 id 后 | 从文件系统、虚拟模块或插件返回源码 | load |
| 模块转换 | 加载源码后 | 编译 TS/JSX/Vue/CSS,重写 import,生成 sourcemap | transform |
| 依赖预构建 | dev 初次启动或依赖变化 | 扫描 bare imports,预构建依赖到缓存 | dep optimizer |
| HMR 监听 | 文件变化 | 失效模块图、找 HMR 边界、发送 websocket 更新 | handleHotUpdate |
| build 启动 | vite build | 创建构建环境,调用 Rolldown 构建依赖图 | buildStart |
| build 转换 | 构建每个模块 | 复用 resolveId/load/transform 处理模块 | 通用构建钩子 |
| 产物生成 | chunk 和 asset 输出前后 | 代码分割、hash、CSS 抽取、manifest、压缩 | renderChunk、generateBundle |
| preview | vite preview | 静态服务 dist,用于本地预览生产产物 | configurePreviewServer |
21.16.1 开发阶段的一次模块请求
简化源码流程:
ts
async function handleModuleRequest(url) {
const id = await pluginContainer.resolveId(url);
const loaded = await pluginContainer.load(id);
const code = loaded ?? await readFile(id);
const transformed = await pluginContainer.transform(code, id);
const analyzed = rewriteImportsAndUpdateModuleGraph(transformed);
return sendJavaScript(analyzed);
}真实实现会多处理:
- 缓存。
- sourcemap。
- 文件系统访问边界。
- optimized deps。
- CSS 请求。
- Vue/Svelte/React 插件转换。
- SSR environment。
- HMR timestamp。
21.16.2 HMR 阶段发生了什么
简化流程:
text
chokidar 监听到文件变化
-> 找到对应 ModuleNode
-> 调用插件 handleHotUpdate
-> 失效 transform 缓存
-> 沿 moduleGraph 找到 HMR 边界
-> 通过 WebSocket 发 update/full-reload/prune
-> 浏览器端 Vite client 拉取新模块并执行 accept 回调哪些情况容易 full reload:
- 入口模块变化。
- HTML 变化。
- 没有找到可接受更新的 HMR 边界。
- 插件明确要求 full reload。
- 模块副作用太大,局部替换不安全。
21.17 Vite 插件生命周期最小示例
下面插件打印主要生命周期,适合放到实验项目里观察顺序:
ts
import type { Plugin } from 'vite';
export function inspectLifecycle(): Plugin {
return {
name: 'inspect-lifecycle',
config(config, env) {
console.log('[config]', env.command, env.mode);
},
configResolved(config) {
console.log('[configResolved]', config.root);
},
configureServer(server) {
console.log('[configureServer]');
server.middlewares.use((req, _res, next) => {
console.log('[request]', req.url);
next();
});
},
transformIndexHtml(html) {
console.log('[transformIndexHtml]');
return html;
},
resolveId(id, importer) {
if (id.startsWith('virtual:demo')) {
console.log('[resolveId]', id, importer);
return '\0' + id;
}
},
load(id) {
if (id === '\0virtual:demo') {
console.log('[load]', id);
return 'export const message = "hello from virtual module"';
}
},
transform(code, id) {
if (id.endsWith('.ts') || id.endsWith('.vue')) {
console.log('[transform]', id);
}
return null;
},
handleHotUpdate(ctx) {
console.log('[handleHotUpdate]', ctx.file);
},
generateBundle(_options, bundle) {
console.log('[generateBundle]', Object.keys(bundle));
},
};
}你应该观察到:
config/configResolved只在启动或构建时执行。configureServer只在开发服务器执行。resolveId/load/transform会随着模块请求频繁执行。transformIndexHtml只处理 HTML。handleHotUpdate只在 dev 文件变化时执行。generateBundle只在 build 生成产物时执行。
21.18 源码阅读定位
如果你要读源码,不建议从入口一路硬啃。按问题定位更有效。
| 问题 | 推荐看哪里 | 关注点 |
|---|---|---|
| CSS/PostCSS 怎么走 | vite/packages/vite/src/node/plugins/css.ts,发布包类型 dist/node/index.d.ts 的 CSSOptions | css.postcss、modules、preprocessorOptions、preprocessCSS |
| dev server 怎么创建 | vite/packages/vite/src/node/server/index.ts | HTTP server、中间件、watcher、WebSocket、environment |
| 一次请求怎么转换 | vite/packages/vite/src/node/server/transformRequest.ts | 缓存、load、transform、module graph |
| 路径怎么解析 | vite/packages/vite/src/node/plugins/resolve.ts | alias、bare import、exports、dedupe |
| import 怎么重写 | vite/packages/vite/src/node/plugins/importAnalysis.ts | import 分析、URL 重写、HMR 接入 |
| Vue SFC 怎么拆 | vite-plugin-vue/packages/plugin-vue/src/* | descriptor、script/template/style 子请求 |
| 插件类型定义 | vite/dist/node/index.d.ts | Plugin、CSSOptions、ResolveOptions |
源码阅读顺序建议:
- 先读类型定义,知道配置和 hook 名称。
- 再读插件入口,理解各 hook 做什么。
- 最后读具体函数,不要一开始陷入每个兼容分支。
21.19 Environment API:一个 dev server 服务多个运行环境
21.19.1 是什么
Vite 6 把 Environment(环境)这个概念正式化了。在此之前,Vite 内部只有两个隐式环境:client(浏览器)和可选的 ssr(Node 等服务端运行时)。Environment API 允许用户和框架作者按需创建任意多个环境,让开发和构建的拓扑与应用真实的生产运行方式对齐——比如一个应用同时跑在浏览器、Node 服务器和边缘运行时(Cloudflare Workers 的 workerd)上。
截至本文核对时间,Environment API 处于 release candidate(候选发布) 阶段:官方承诺在大版本之间保持 API 稳定,让生态可以放心基于它构建,但个别 API 仍标记为 experimental。
21.19.2 为什么需要
传统做法里,一个环境对应一份配置甚至一个进程。SSR 应用要维护两套模块图、两套解析行为;如果还有 edge/worker 环境,情况会进一步失控。Environment API 的目标是:
- 一个 Vite dev server 并发运行多个环境:共享同一个 HTTP server、中间件、解析后的配置和插件管线,但每个环境有自己独立的模块图、转换行为和 HMR。
- 消除开发/构建的差异:开发阶段各环境通过 module runner 拉取并执行转换后的代码,构建阶段每个环境独立产出产物(如
dist/client、dist/ssr)。 - 让运行时提供方介入:例如 Cloudflare 官方 Vite 插件用 Environment API 在开发时直接把代码跑在 workerd 运行时里,开发行为和生产一致。
21.19.3 怎么用
对普通 SPA/MPA 项目,不需要接触任何新概念:顶层配置会默认应用在 client 环境上,Vite 5 风格的配置原样工作:
ts
export default defineConfig({
build: { sourcemap: false },
optimizeDeps: { include: ['lib'] },
})需要多环境时才显式写 environments:
ts
export default defineConfig({
build: { sourcemap: false },
environments: {
server: {},
edge: {
resolve: { noExternal: true },
},
},
})继承规则:没有单独文档说明的选项,子环境继承顶层配置(上面 server、edge 都会继承 build.sourcemap: false)。少数顶层选项只作用于 client 环境(例如 optimizeDeps,配置参考中带有 non-inherit 标记),因为它们作为默认值套用到服务端环境上没有意义。
EnvironmentOptions 的核心形状:
ts
interface EnvironmentOptions {
define?: Record<string, any>
resolve?: EnvironmentResolveOptions
optimizeDeps: DepOptimizationOptions
consumer?: 'client' | 'server'
dev: DevOptions
build: BuildOptions
}UserConfig 扩展自 EnvironmentOptions,所以顶层配置既是在配置 client,也是在给其他环境提供默认值。
几个关于"哪些环境一定存在"的规则:
- 开发时
client环境始终存在,名为ssr的环境也始终存在(为了向后兼容server.ssrLoadModule()和server.moduleGraph)。 - 构建时
client始终存在,ssr只在显式配置(environments.ssr或兼容写法build.ssr)时才存在。 - 环境名不是必须叫
ssr,可以叫server、edge等。
运行时提供方可以用低层 API 创建自定义环境实例:
ts
import { customEnvironment } from 'vite-environment-provider'
export default defineConfig({
build: { outDir: 'dist/client' },
environments: {
ssr: customEnvironment({
build: { outDir: 'dist/ssr' },
}),
},
})💡 对插件作者的迁移方向(官方列出):钩子里通过 this.environment 拿到当前环境、使用新的 hotUpdate 钩子、转向 per-environment API、SSR 侧改用 ModuleRunner API。顶层的 ssr 配置属性会在 Environment API 稳定后被废弃。
21.20 Vite 8 架构:Rolldown 成为唯一打包器
21.20.1 从双打包器到单打包器
Vite 早期做了一个务实的赌注:开发阶段用 esbuild 换速度(依赖预构建、语法转换),生产阶段用 Rollup 换优化质量。这套组合用了多年,但两个打包器意味着两套行为、两套边界情况,以及"开发能跑、构建行为不同"的调试成本。
Vite 8 把它们统一成一个:Rolldown,由 VoidZero 团队用 Rust 编写的打包器。官方称这是自 Vite 2 以来最重要的架构变化,生产构建最高可获得 10–30 倍于 Rollup 的速度,同时达到与 esbuild 相当的性能水平(参考官方基准仓库)。
Rolldown 的三个设计目标:
| 目标 | 说明 |
|---|---|
| 性能 | Rust 原生速度,官方基准下比 Rollup 快 10–30 倍 |
| 兼容 | 支持与 Rollup 和 Vite 相同的插件 API,绝大多数现有 Vite 插件无需改动即可工作 |
| 高级能力 | 单一打包器解锁了双打包器时代难以实现的能力:full bundle mode、更灵活的 chunk 拆分、模块级持久化缓存、Module Federation 支持 |
21.20.2 rolldown-vite 的来历与迁移路径
迁移是刻意分阶段推进的:
- 技术预览:先发布独立的
rolldown-vite包,作为vite的 drop-in 替代,让早期采用者在不影响稳定版 Vite 的前提下验证 Rolldown 集成、反馈边界情况。 - 专项 CI:建立专门的 CI 套件,持续验证关键 Vite 插件和框架(如 SvelteKit、react-router、Storybook)在新打包器下的表现,尽早发现回归。
- 并入主线:2025 年 12 月发布 Vite 8 beta(Rolldown 完全集成),beta 期间 Rolldown 自身也从 beta 走到 release candidate;稳定后
rolldown-vite的能力并入主线vite,独立包的使命随之结束。
对使用者的两条迁移路线:
text
路线一(多数项目):直接把 package.json 里的 vite 升到 8,照常执行 dev/build。
Vite 8 内置兼容层,会自动把旧的 esbuild / rollupOptions 配置转换成
Rolldown / Oxc 的等价配置,许多项目无需改动配置。
路线二(大型或复杂项目,官方推荐):先在 Vite 7 上把 vite 换成 rolldown-vite 包,
隔离并解决 Rolldown 相关的兼容问题,再升级到 Vite 8。
这样容易区分问题来自打包器更换,还是 Vite 8 的其他变化。⚠️ 学习旧资料时要带版本意识:提到"开发用 esbuild、构建用 Rollup"的表述,描述的是 Vite 7 及更早版本;提到 rolldown-vite 独立包的表述,描述的是 Vite 8 稳定前的预览阶段。Vite 8 时代的准确表述是:开发预构建和生产构建都基于 Rolldown,语法转换与压缩更多依赖 Oxc,插件 API 保持 Rollup 风格兼容。
22. 学习资源
- Vite 官方文档:https://vite.dev/
- Vite 环境变量与模式:https://vite.dev/guide/env-and-mode
- Vite 配置参考:https://vite.dev/config/
- Vite CSS 功能:https://vite.dev/guide/features#css
- Vite 插件 API:https://vite.dev/guide/api-plugin
- Vite 依赖预构建:https://vite.dev/guide/dep-pre-bundling
- Vite 构建生产版本:https://vite.dev/guide/build
- Vite GitHub 源码:https://github.com/vitejs/vite
- @vitejs/plugin-vue 源码:https://github.com/vitejs/vite-plugin-vue/tree/main/packages/plugin-vue
- PostCSS 官方文档:https://postcss.org/
- postcss-load-config:https://github.com/postcss/postcss-load-config
- Vue 官方文档:https://vuejs.org/
- Vue Router 官方文档:https://router.vuejs.org/
- Rolldown 官方文档:https://rolldown.rs/
- Rollup 官方文档:https://rollupjs.org/
- Vite 8 发布公告(Rolldown 架构与迁移):https://vite.dev/blog/announcing-vite8
- Vite 迁移指南:https://vite.dev/guide/migration
- Vite Environment API:https://vite.dev/guide/api-environment
- Vite HMR API:https://vite.dev/guide/api-hmr
建议不要只读文档。最有效的学习方式是:
- 用 Vite 创建项目。
- 每学一个配置就写一个最小示例。
- 故意制造一次错误,再按报错排查。
- 把开发环境和生产环境都跑通。