Skip to content

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 架构。版本细节请以官方文档为准,本文重点讲学习路线和工程化思路。


目录 ​

  1. Vite 到底是什么
  2. 学习 Vite 前需要具备什么基础
  3. 快速开始:创建第一个 Vite 项目
  4. Vite 项目结构详解
  5. Vite 为什么开发时很快
  6. vite.config 配置文件系统讲解
  7. 开发服务器 server 配置
  8. 路径别名与模块解析
  9. 静态资源、CSS 与预处理器
  10. 环境变量与模式
  11. TypeScript 在 Vite 中的正确用法
  12. 插件机制与常见插件
  13. 生产构建 build 配置
  14. 代码分割、缓存与性能优化
  15. 代理、跨域与本地联调
  16. 部署上线与 base 路径
  17. 进阶主题:库模式、SSR、Monorepo
  18. Vite 与 Webpack 的关系
  19. 常见问题与排查思路
  20. 推荐学习顺序
  21. 核心机制深度补全
  22. 学习资源

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-ts
bash
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.ts

4.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.ts

4.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 在开发时通常会这样做:

  1. 找到入口文件。
  2. 分析完整依赖图。
  3. 对所有模块执行 loader / plugin。
  4. 生成一个或多个 bundle。
  5. 启动开发服务器。

项目越大,这个启动过程越重。

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/.vite

Windows PowerShell:

powershell
Remove-Item -Recurse -Force node_modules/.vite

5.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 -D

Vue 组件中使用:

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.local

10.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.local

10.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.html

13.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:3000

vite.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。

学习顺序建议:

  1. 先掌握普通 SPA。
  2. 再理解构建和路由。
  3. 最后学习 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 应该怎么学 ​

推荐顺序:

  1. 先用 Vite 把项目做起来。
  2. 再学 Webpack 理解构建工具底层概念。
  3. 最后比较两者在开发、构建、插件、优化上的差异。

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 报错。

排查顺序:

  1. 打开浏览器控制台。
  2. 看 Network 是否有 404。
  3. 看资源路径是否带错前缀。
  4. 检查 base 和路由 base。
  5. 检查服务器 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 依赖预构建 ​

依赖预构建主要解决两个问题:

  1. 把 CommonJS/UMD 依赖转换为浏览器可直接加载的 ESM。
  2. 把有大量内部模块的依赖合并,减少浏览器请求数量。

缓存位置通常在:

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。
  • 某些包预构建后行为异常,需要交给源码转换链路处理。

排查方式:

  1. 删除 node_modules/.vite。
  2. 使用 vite --force 强制重新预构建。
  3. 看终端中的 dependency optimized 提示。
  4. 检查 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配置解析后读取最终配置
configureServerdev server添加中间件、读取 server
resolveId解析模块改写模块路径
load加载模块返回虚拟模块内容
transform转换模块编译源码
handleHotUpdateHMR自定义热更新行为
generateBundlebuild修改构建产物

虚拟模块示例:

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.ico

src/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 排查清单 ​

  1. 启动慢:检查依赖预构建、插件数量、Monorepo 本地包。
  2. HMR 不生效:看是否整页 reload、模块是否有副作用、插件是否支持 HMR。
  3. 页面白屏:检查控制台错误、资源 404、base 配置。
  4. 环境变量 undefined:确认变量以 VITE_ 开头,重启 dev server。
  5. 代理不生效:确认请求路径命中 server.proxy,生产环境另配 Nginx/网关。
  6. 构建后路径错误:检查 base、部署子路径、路由 history base。
  7. 包体积过大:用 visualizer 分析,再拆分大依赖。
  8. sourcemap 泄露:生产环境按安全策略关闭或上传到错误监控平台。

21.12 版本基线:Vite 8 之后要先修正旧认知 ​

本文本次补全按 2026-07-09 核对的 npm 版本书写:

包核对版本说明
vite8.1.4Vite 主包
@vitejs/plugin-vue6.0.7Vue SFC 官方 Vite 插件
postcss8.5.16CSS 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.js
  • postcss.config.cjs
  • postcss.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 处理选项对象。实际项目中最常用的是对象写法。

配置作用常见使用场景
pluginsPostCSS 插件列表,按顺序处理 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 ModuleslocalsConvention、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

这里有三个容易混淆的点:

  1. lang="scss" 不是 PostCSS 做的,是 Sass 预处理器做的。
  2. autoprefixer、postcss-preset-env 这类才是 PostCSS 插件做的。
  3. <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 排查清单 ​

  1. 插件是否安装到当前项目依赖?
  2. 配置文件是 CommonJS 还是 ESM,和 package.json 的 "type" 是否冲突?
  3. 是否同时写了 postcss.config.* 和 css.postcss,导致配置来源混乱?
  4. 插件顺序是否正确?
  5. 是否把 Sass/Less 语法误以为 PostCSS 能直接识别?
  6. source map 不准时,先打开 css.devSourcemap,再检查 PostCSS map。
  7. 生产结果和开发结果不同,检查 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

插件源码定位:

源码级简化示例,非逐字复制:

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 解析
@/utilsalias先命中 resolve.alias,再继续解析
vuebare 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.conditionspackage exports 条件解析
resolve.mainFieldspackage 入口字段优先级
resolve.extensions省略扩展名时尝试的扩展
resolve.preserveSymlinks是否保留符号链接真实路径
resolve.tsconfigPathsVite 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、处理入口 scripttransformIndexHtml
模块解析浏览器请求 JS/CSS/Vue 等模块把 import 字符串解析为模块 idresolveId
模块加载解析出 id 后从文件系统、虚拟模块或插件返回源码load
模块转换加载源码后编译 TS/JSX/Vue/CSS,重写 import,生成 sourcemaptransform
依赖预构建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
previewvite 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 的 CSSOptionscss.postcss、modules、preprocessorOptions、preprocessCSS
dev server 怎么创建vite/packages/vite/src/node/server/index.tsHTTP server、中间件、watcher、WebSocket、environment
一次请求怎么转换vite/packages/vite/src/node/server/transformRequest.ts缓存、load、transform、module graph
路径怎么解析vite/packages/vite/src/node/plugins/resolve.tsalias、bare import、exports、dedupe
import 怎么重写vite/packages/vite/src/node/plugins/importAnalysis.tsimport 分析、URL 重写、HMR 接入
Vue SFC 怎么拆vite-plugin-vue/packages/plugin-vue/src/*descriptor、script/template/style 子请求
插件类型定义vite/dist/node/index.d.tsPlugin、CSSOptions、ResolveOptions

源码阅读顺序建议:

  1. 先读类型定义,知道配置和 hook 名称。
  2. 再读插件入口,理解各 hook 做什么。
  3. 最后读具体函数,不要一开始陷入每个兼容分支。

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 的来历与迁移路径 ​

迁移是刻意分阶段推进的:

  1. 技术预览:先发布独立的 rolldown-vite 包,作为 vite 的 drop-in 替代,让早期采用者在不影响稳定版 Vite 的前提下验证 Rolldown 集成、反馈边界情况。
  2. 专项 CI:建立专门的 CI 套件,持续验证关键 Vite 插件和框架(如 SvelteKit、react-router、Storybook)在新打包器下的表现,尽早发现回归。
  3. 并入主线: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. 学习资源 ​

建议不要只读文档。最有效的学习方式是:

  1. 用 Vite 创建项目。
  2. 每学一个配置就写一个最小示例。
  3. 故意制造一次错误,再按报错排查。
  4. 把开发环境和生产环境都跑通。

基于 VitePress 构建的个人学习文档知识库