Vite 用户可见功能
1. 开发服务器 (vite dev)
- 即时启动
- 热模块替换(HMR)
- 原生 ES 模块支持
- 自动依赖预构建
2. 生产构建 (vite build)
- 代码打包与优化
- 代码分割
- 资源优化
- Source Map 生成
- SSR 构建支持
3. 预览构建结果 (vite preview)
- 本地预览生产构建
- 验证构建结果
4. 依赖优化 (vite optimize)
- 依赖预构建(已废弃,自动执行)
总结
对用户来说,Vite 主要有 3 个核心功能:
- 开发服务器(开发时使用)
- 生产构建(构建时使用)
- 预览服务器(预览构建结果)
其他功能(如 HMR、依赖优化、插件等)是这些核心功能的组成部分,用户通过配置和插件系统使用它们。
Vite 的三个核心功能
1. 开发服务器 (vite dev / vite serve)
作用:启动本地开发服务器,用于开发阶段。
主要特性:
- 即时启动:基于原生 ES 模块,无需打包即可启动
- 热模块替换(HMR):修改代码后立即更新,无需刷新页面
- 自动依赖预构建:首次启动时预构建 CommonJS/UMD 依赖
- 文件监听:监听文件变化并触发更新
- WebSocket 通信:用于 HMR 和错误提示
- 中间件链:处理静态文件、代理、转换等
使用场景:日常开发
vite dev # 启动开发服务器
vite serve # 别名,功能相同
2. 生产构建 (vite build)
作用:将源代码打包并优化,生成生产环境可部署的静态资源。
主要特性:
- 代码打包:使用 Rolldown/Rollup 打包
- 代码压缩:使用 esbuild 或 terser 压缩
- 代码分割:自动分割代码块,优化加载
- 资源优化:处理图片、CSS 等静态资源
- Source Map:生成调试用的 source map
- SSR 构建:支持服务端渲染构建
- 清单文件:生成 manifest.json 用于资源映射
使用场景:部署到生产环境前
vite build # 构建生产版本
vite build --watch # 监听模式构建
3. 预览服务器 (vite preview)
作用:本地预览生产构建的结果,验证构建产物。
主要特性:
- 静态文件服务:提供构建后的静态文件
- 代理支持:支持 API 代理配置
- CORS 支持:可配置跨域
- 压缩支持:支持 gzip 压缩
- HTML 回退:支持 SPA 路由回退
使用场景:构建完成后,在本地验证生产构建结果
vite build # 先构建
vite preview # 预览构建结果
三者关系
开发阶段: vite dev
↓
开发、调试、测试
↓
构建阶段: vite build
↓
生成 dist/ 目录
↓
预览阶段: vite preview
↓
总结
- 开发服务器:开发时使用,提供 HMR 和即时反馈
- 生产构建:构建时使用,生成优化的生产代码
- 预览服务器:预览时使用,验证构建结果
这三个功能覆盖了从开发到部署的完整流程。
为什么生产要用npm run build不用npm run dev
npm run build 的优势
1. 性能优化
代码压缩和优化
开发模式 (dev):
main.js: 500KB (未压缩,包含注释和空格)
生产构建 (build):
main.js: 150KB (压缩后,去除注释、空格、变量名简化)
- 文件体积更小:代码压缩、去除注释和调试信息
- 加载更快:更小的文件下载更快
- 执行更快:优化后的代码执行效率更高
代码分割(Code Splitting)
开发模式: 所有代码在一个请求中
生产构建: 按需分割成多个 chunk
- main.js (核心代码)
- vendor.js (第三方库)
- component-a.js (按需加载)
- 按需加载:只加载当前需要的代码
- 并行加载:多个文件可以同时下载
- 缓存优化:修改代码时,未变的文件可以继续使用缓存
2. 生产环境特性
Tree Shaking(移除未使用代码)
// 你的代码
import { func1, func2 } from 'library'
// 只使用了 func1
开发模式: 可能包含 func1 和 func2
生产构建: 只包含 func1(移除未使用的 func2)
资源优化
- 图片压缩:自动优化图片大小
- CSS 压缩:去除空格和注释
- 小资源内联:小于 4KB 的资源转为 base64
3. 浏览器兼容性
代码转译
开发模式: 使用现代 JavaScript(ES2020+)
生产构建: 转译为兼容更多浏览器(ES2015+)
- 兼容更多浏览器:转译为旧版 JavaScript
- 自动添加 polyfill:为旧浏览器提供新特性支持
4. 安全性
代码混淆
开发模式:
function getUserData() { ... } // 函数名清晰可见
生产构建:
function a(){...} // 函数名被混淆,难以理解
- 代码混淆:增加逆向难度
- 隐藏实现细节:保护业务逻辑
5. 部署优势
静态文件部署
开发模式:
- 需要 Node.js 环境
- 需要运行 npm run dev
- 占用服务器资源(CPU、内存)
生产构建:
- 只需要静态文件服务器(Nginx)
- 不需要 Node.js
- 资源占用极低
6. 实际对比
文件大小对比
开发模式:
- main.js: 2.5MB
- 100+ 个文件请求
- 总加载时间: 5秒
生产构建:
- main.js: 200KB (压缩后)
- vendor.js: 150KB
- 10个文件请求
- 总加载时间: 0.8秒
加载速度对比
开发模式:
首次加载: 需要转换所有文件,较慢
后续加载: 有缓存,但仍有转换开销
生产构建:
首次加载: 直接加载优化后的文件,很快
后续加载: 利用浏览器缓存,极快
7. 用户体验
- 更快的页面加载:优化后的文件加载更快
- 更流畅的交互:代码优化后执行更快
- 更好的移动端体验:文件更小,节省流量
总结对比表
| 特性 | npm run dev | npm run build |
|---|---|---|
| 文件大小 | 大(未压缩) | 小(压缩优化) |
| 加载速度 | 慢(需要转换) | 快(直接加载) |
| 代码分割 | 无 | 有(按需加载) |
| 浏览器兼容 | 现代浏览器 | 更多浏览器 |
| 部署方式 | 需要 Node.js | 只需静态服务器 |
| 安全性 | 代码可见 | 代码混淆 |
| 适用场景 | 开发调试 | 生产部署 |
结论
npm run build 的优势:
- 性能:文件更小、加载更快
- 优化:代码压缩、分割、Tree Shaking
- 兼容性:支持更多浏览器
- 部署:简单、成本低、可扩展
- 用户体验:更快的加载和交互
简单说:dev 适合开发,build 适合生产。生产环境必须使用 build 才能获得这些优势。
Vite的缓存策略
Nginx 什么时候能帮忙缓存?
关键区别:开发环境 vs 生产环境
| 环境 | 缓存策略 | 原因 |
|---|---|---|
| 开发环境 (npm run dev) | ❌ 不缓存 | 代码频繁变化,需要实时更新 |
| 生产环境 (npm run build) | ✅ 可以缓存 | 代码稳定,文件名包含 hash |
生产构建的文件名机制
关键代码(build.ts 第727-730行):
entryFileNames: `[name]-[hash].${jsExt}`
// 例如: main-abc123def456.js
chunkFileNames: `[name]-[hash].${jsExt}`
// 例如: vendor-xyz789ghi012.js
构建后的文件名包含内容 hash:
dist/
├── index.html
├── assets/
│ ├── main-abc123def456.js ← 文件名包含 hash
│ ├── main-abc123def456.css
│ └── vendor-xyz789ghi012.js
为什么可以安全缓存?
1. 文件名包含内容 hash
文件内容变化前:
main-abc123def456.js (hash: abc123def456)
你修改了代码,重新构建:
main-xyz789ghi012.js (hash: xyz789ghi012) ← 文件名变了!
- 内容变化 → hash 变化 → 文件名变化
- 文件名变化 → 浏览器认为是新文件 → 自动请求新文件
2. HTML 文件引用会更新
<!-- 构建前 -->
<script src="/assets/main-abc123def456.js"></script>
<!-- 你修改代码,重新构建后 -->
<script src="/assets/main-xyz789ghi012.js"></script>
- HTML 会更新引用
- 旧文件不会被请求
- 新文件自动加载
Nginx 缓存配置
推荐配置
server {
listen 80;
root /path/to/dist;
index index.html;
# HTML 文件不缓存(因为会更新引用)
location ~* \.html$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
# 静态资源(JS/CSS/图片)长期缓存
# 因为文件名包含 hash,内容变化时文件名也会变化
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
# 31536000 秒 = 1 年
}
# SPA 路由回退
location / {
try_files $uri $uri/ /index.html;
}
}
缓存策略对比
开发环境(不能缓存)
请求: GET /assets/main-abc123def456.js
响应: Cache-Control: public, max-age=31536000, immutable
原因:
- 文件名包含 hash
- 内容变化时文件名也会变化
- 可以安全地长期缓存
实际例子
场景 1:首次部署
1. 构建: npm run build
生成: main-abc123def456.js
2. 用户访问网站
浏览器请求: GET /assets/main-abc123def456.js
Nginx 返回文件,设置缓存 1 年
3. 用户再次访问
浏览器使用缓存 ✅(因为文件名没变)
场景 2:更新代码(html不缓存更新引用)
1. 你修改了代码,重新构建
生成: main-xyz789ghi012.js ← 新文件名!
2. HTML 更新引用:
<script src="/assets/main-xyz789ghi012.js"></script>
3. 用户访问网站
浏览器请求: GET /assets/main-xyz789ghi012.js
← 这是新文件,浏览器会请求(不会用旧缓存)
4. 旧文件 main-abc123def456.js 不再被引用
可以安全删除或继续缓存(不会被使用)
缓存的好处
- 性能提升
- 减少服务器请求
- 减少带宽消耗
- 更快的页面加载
- 成本降低
- 减少服务器负载
- 减少 CDN 流量费用
- 用户体验
- 更快的响应速度
- 更好的离线体验
总结
Nginx 可以在生产环境缓存,因为:
- 文件名包含 hash:内容变化时文件名也会变化
- HTML 会更新引用:确保加载新文件
- 静态文件稳定:生产环境代码不会频繁变化
开发环境不能缓存(Vite 设置 no-cache),生产环境可以缓存(文件名包含 hash,可以安全地长期缓存)。
这就是为什么:
- 开发时:Cache-Control: no-cache(不缓存)
- 生产时:Cache-Control: max-age=31536000, immutable(长期缓存)
Vite 如何转换 Vue/React 代码
核心转换工具
Vite 使用两种转换器(按优先级):
- OXC(新,默认)
- 更快的 Rust 实现
- 支持 JS/TS/JSX/TSX
- ESBuild(旧,已废弃)
- 仍可用但会警告
- 逐步迁移到 OXC
转换流程
1. 浏览器请求文件(npm run dev 模式)
浏览器请求: GET /src/App.vue
↓
Vite 开发服务器接收请求
↓
进入 transformRequest 流程
最终返回处理后的结果给前端浏览器 前端浏览器看到的代码是未转化的TS、Vue、React代码
为什么要转换?
核心原因:浏览器无法直接执行这些代码
1. JSX/TSX 语法浏览器不支持
JSX 不是有效的 JavaScript
// ❌ 浏览器无法理解这个语法function App() { return <div>Hello</div>}
浏览器只能理解:
// ✅ 浏览器能理解的 JavaScriptfunction App() { return React.createElement('div', null, 'Hello')}
原因:
JSX 是语法糖,不是标准 JavaScript
浏览器只能执行标准 JavaScript
需要转换为浏览器能理解的代码
2. Vue 模板语法浏览器不支持
Vue 模板不是 JavaScript
<!-- ❌ 浏览器无法理解 --><template> <div>{{ message }}</div></template>
需要转换为:
// ✅ 浏览器能理解的 JavaScriptimport { createElementVNode } from 'vue'export default { render() { return createElementVNode('div', null, this.message) }}
构建后的代码是纯 JavaScript
构建前 vs 构建后
Vue 组件
<!-- 构建前:src/App.vue -->
<template>
<div class="container">
<h1>{{ title }}</h1>
<button @click="handleClick">Click me</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('Hello Vue')
const handleClick = () => {
console.log('clicked')
}
</script>
构建后
// dist/assets/main-abc123.js
import{a as e,b as r}from"vue";
const s=e("
验证方法
-
运行构建:
npm run build
-
查看 dist/ 目录:
dist/ ├── index.html └── assets/ ├── main-abc123.js ← 纯 JavaScript └── main-abc123.css
- 打开 dist/assets/main-abc123.js:
- 看到的是纯 JavaScript
- 没有 <template>、<script setup>
- 没有 JSX 语法
- 没有 TypeScript 类型
- 在浏览器中查看:
- 打开开发者工具 → Sources
- 查看加载的 JS 文件
- 都是标准 JavaScript
总结
- 构建后,浏览器看到的是纯 JavaScript 代码
- Vue/React 的特殊语法已被转换为标准 JavaScript 函数调用
- 浏览器可以直接执行,无需额外转换
这就是为什么构建后的代码可以直接部署到任何静态服务器,浏览器可以直接运行。
Vite 如何将代码更新推送到所有浏览器(只适用于 npm run dev)
核心机制:WebSocket 广播
Vite 使用 WebSocket 服务器管理所有浏览器连接,并通过广播将更新推送给所有客户端。
完整流程
1. 建立 WebSocket 连接
// 浏览器打开页面时
浏览器 → 连接 WebSocket (ws://localhost:5173)
↓
Vite WebSocket 服务器接收连接
↓
将客户端添加到 wss.clients 集合中
2.发生变更,广播到所有的客户端
文件变化 (src/App.vue)
↓
chokidar 文件监听器检测到变化
↓
触发 watcher.on('change')
↓
调用 handleHMRUpdate()
↓
生成 HMR 更新消息
↓
WebSocket 服务器.send()
↓
遍历所有客户端: wss.clients.forEach()
↓
┌─────────────────────────────────┐
│ 浏览器1 (Chrome) │ ← 收到更新
│ 浏览器2 (Firefox) │ ← 收到更新
│ 浏览器3 (Safari) │ ← 收到更新
│ 标签页1 (localhost:5173) │ ← 收到更新
│ 标签页2 (localhost:5173/page2) │ ← 收到更新
└─────────────────────────────────┘
Vite的HMR(热更新)只适用于 npm run dev
1. 你修改文件 (App.vue)
↓
2. chokidar 检测到变化
↓
3. handleHMRUpdate()
├─ 找到受影响的模块
├─ propagateUpdate() 分析更新边界
└─ 确定哪些模块可以热更新
↓
4. 生成更新消息
{
type: 'update',
updates: [{
type: 'js-update',
path: '/src/App.vue',
acceptedPath: '/src/App.vue',
timestamp: 1234567890
}]
}
↓
5. WebSocket 广播
wss.clients.forEach(client => {
client.send(updateMessage)
})
↓
6. 浏览器接收消息
↓
7. 客户端处理更新
├─ 重新导入模块: import('/src/App.vue?t=1234567890')
├─ 执行模块的 accept 回调
└─ 更新 DOM(如果模块有更新逻辑)
↓
8. 页面更新,无需刷新 ✅