
一句话:SPA 项目用 Vite(开发极快、原生 ESM、生态成熟),需要 SEO/SSR/全栈能力用 Next.js,CRA 已停止维护不要再用;构建阶段关心的是「依赖预构建、HMR、产物优化、环境变量」。
一、三种脚手架对比
| 维度 | Vite | Next.js | CRA(已废弃) |
|---|---|---|---|
| 定位 | 通用构建工具(SPA/库) | 全栈框架(SSR/RSC) | React 官方脚手架(历史) |
| 开发启动 | 极快(原生 ESM + esbuild 预构建) | 较快(按需编译) | 慢(Webpack 全量打包) |
| 构建工具 | Rollup(生产) | Turbopack / Webpack | Webpack |
| SSR/SSG | ❌(要自己搭或用 SSR 插件) | ✅ 内置 | ❌ |
| 路由 | 自己选(React Router) | ✅ 文件路由 | ❌ |
| 数据获取 | 自己做 | ✅ loader / RSC / Server Actions | ❌ |
| 环境变量 | VITE_ 前缀 |
NEXT_PUBLIC_ 前缀 |
REACT_APP_ 前缀 |
| 现状 | SPA 首选 | 全栈首选 | ⚠️ 已废弃(2025 年停止维护) |
选型建议:
① 后台系统 / 内部工具 / 不需要 SEO → Vite + React Router
② 需要 SEO / SSR / 全栈 → Next.js
③ 组件库 → Vite(库模式)
④ 不要再新起 CRA 项目(Webpack 配置繁琐、启动慢、官方已弃用)
# 创建项目
npm create vite@latest my-app -- --template react-ts
npm create next-app@latest my-app
二、Vite 的核心原理
Vite 快的两个原因:
① 开发环境:【不打包】+ 原生 ESM
· 浏览器直接请求模块(<script type="module">)
· Vite 只做「按需转换」(请求哪个文件就转哪个)
→ 启动时不需要「打包整个应用」(冷启动几乎瞬间)
② 依赖预构建(Pre-bundling):
· 用 esbuild(Go 编写,极快)把 node_modules 里的依赖预打包成 ESM
· 目的 1:把 CJS 转成 ESM(浏览器只认 ESM)
· 目的 2:把「很多小文件的包」(如 lodash 的上百个模块)合并成一个
→ 减少浏览器的 HTTP 请求数
· 产物缓存在 node_modules/.vite
③ HMR(热更新):
· 基于原生 ESM 的「模块图」,只让「受影响的模块」失效
· 不需要「重新打包 + 整页刷新」→ 毫秒级更新
生产构建:Vite 用 Rollup(成熟、tree-shaking 好、插件生态)
→ 也可以选用 Rolldown(Vite Team 用 Rust 重写的 Rollup,正在推进中)
⚠️ 所以 Vite 是「开发用 esbuild + 生产用 Rollup」的双引擎:
开发快、产物质量有保障。
三、Vite 项目的常用配置
// vite.config.ts
import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'node:path';
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '');
return {
plugins: [react()], // 或用 @vitejs/plugin-react-swc(更快)
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'), // ⭐ 路径别名
},
},
server: {
port: 5173,
open: true,
proxy: {
// ⭐ 开发环境代理(解决跨域,且与生产同源行为一致)
'/api': {
target: env.VITE_API_TARGET || 'http://localhost:3000',
changeOrigin: true,
rewrite: (p) => p.replace(/^\/api/, ''),
},
},
},
build: {
outDir: 'dist',
sourcemap: false, // ⚠️ 生产默认关闭(但要留给监控平台)
chunkSizeWarningLimit: 1000,
rollupOptions: {
output: {
// ⭐ 手动分包(把「变动少的大依赖」单独抽出,利用缓存)
manualChunks: {
react: ['react', 'react-dom', 'react-router-dom'],
vendor: ['axios', 'dayjs'],
},
// 或按「模块路径」自动分包
// manualChunks(id) {
// if (id.includes('node_modules')) return 'vendor';
// },
},
},
},
define: {
__APP_VERSION__: JSON.stringify(env.npm_package_version ?? '0.0.0'),
},
};
});
// 路径别名的 TS 支持(tsconfig.json)
{
"compilerOptions": {
"baseUrl": ".",
"paths": { "@/*": ["src/*"] }
}
}
// vite.config.ts 里的 define + TS 声明(让类型也能识别全局常量)
// src/vite-env.d.ts
/// <reference types="vite/client" />
declare const __APP_VERSION__: string;
| 配置项 | 作用 |
|---|---|
plugins |
React 插件(plugin-react 用 Babel,plugin-react-swc 用 SWC 更快) |
resolve.alias |
路径别名(@/ → src/) |
server.proxy |
开发代理(解决跨域) |
build.rollupOptions.output.manualChunks |
手动分包(缓存优化) |
build.sourcemap |
生产是否生成 sourcemap |
define |
注入全局常量(构建时替换) |
optimizeDeps.include/exclude |
控制依赖预构建 |
四、环境变量
# .env (所有环境都会加载)
# .env.local (本地覆盖,不进 git)
# .env.development (仅 development)
# .env.production (仅 production)
VITE_API_BASE_URL=/api
VITE_APP_TITLE=我的应用
# ⚠️ 只有 VITE_ 前缀的变量才会暴露给客户端代码
DB_PASSWORD=xxx # ✅ 不会被打进客户端(但也读不到)
// 使用
const baseURL = import.meta.env.VITE_API_BASE_URL;
const isDev = import.meta.env.DEV; // 内置
const isProd = import.meta.env.PROD; // 内置
// ⚠️ 注意:
// ① import.meta.env 的值是「构建时替换」的(不是运行时读取)
// → 改了 .env 要重启 dev server / 重新构建
// ② 所有 VITE_ 前缀的变量都会被打进产物 → 不要放密钥!
⚠️ 安全提醒:
「前端环境变量」只是「构建时替换的常量」——
任何 VITE_ 前缀的变量都能在产物里被找到。
所以「密钥、数据库密码、管理后台 token」绝不能放前端环境变量。
正确的做法是「放服务端」(由服务端代理请求,如 BFF)。
五、开发代理
// ✅ 开发环境用 Vite 代理,避免跨域与「本地不同源」的问题
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
},
},
}
// 代码里直接用相对路径(开发走代理、生产走同源/Nginx)
fetch('/api/users');
⚠️ 为什么推荐「代理 + 相对路径」而不是「配置完整的后端域名 + CORS」?
① 开发与生产行为一致(都是同源请求)
② 不需要后端开 CORS(也不需要处理预检)
③ 不需要把「后端域名」写死在代码里(换环境不用改)
生产环境的对应做法:Nginx 反向代理
location /api/ { proxy_pass http://backend:3000/; }
六、构建产物与部署
Vite 的产物结构:
dist/
├── index.html 入口 HTML
├── assets/
│ ├── index-a1b2c3d4.js 入口 JS(含 hash)
│ ├── vendor-e5f6g7h8.js 分包
│ ├── index-i9j0k1l2.css
│ └── logo-m3n4o5p6.svg 小资源(内联或用 hash 命名)
⭐ 三个要点:
① 文件名带「内容哈希」→ 内容变了文件名就变 → 可以「长期强缓存」
② index.html 【不要强缓存】(否则用户会一直用旧版本引用旧资源)
→ 通常设 Cache-Control: no-cache(每次都校验)或很短的 max-age
③ 静态资源(assets/*)设「长期强缓存」:Cache-Control: public, max-age=31536000, immutable
# Nginx 部署 SPA 的典型配置
server {
listen 80;
root /usr/share/nginx/html;
# ① 静态资源:长期强缓存
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# ② index.html:不缓存(保证拿到最新的资源引用)
location = /index.html {
add_header Cache-Control "no-cache";
}
# ③ SPA fallback:所有未匹配路径返回 index.html(否则刷新 404)
location / {
try_files $uri $uri/ /index.html;
}
# ④ 接口反向代理(可选)
location /api/ {
proxy_pass http://backend:3000/;
}
}
# 本地预览构建产物(验证生产构建是否正确)
npm run build && npm run preview
七、与 Webpack 的差异(Vite 为什么快)
| 维度 | Webpack | Vite(开发) |
|---|---|---|
| 开发启动 | 打包整个应用(冷启动慢,项目越大越慢) | 不打包,按需转换(几乎瞬间) |
| 依赖处理 | 每次都参与打包 | esbuild 预构建 + 缓存 |
| HMR | 依赖打包产物,更新可能较慢 | 基于原生 ESM,毫秒级 |
| 生产构建 | Webpack(成熟) | Rollup(tree-shaking 好) |
| 配置复杂度 | 高(loader/plugin 一堆) | 低(约定 + 少量配置) |
| 生态 | 最丰富(老项目多) | 丰富(现代项目主流) |
| 兼容老浏览器 | ✅(可配置) | ⚠️ 需要 @vitejs/plugin-legacy |
⭐ 一句话解释 Vite 快的原因:
Webpack:「先把整个应用打包成 bundle,再让浏览器加载」
Vite :「让浏览器直接加载源文件(原生 ESM),服务端按需转换」
→ 冷启动时间从「与项目规模成正比」变成「几乎常数」
八、常见坑
// ① 用 CRA 新起项目(已废弃、启动慢)
// ② 以为「VITE_ 环境变量能放密钥」(会被打进产物)
// ③ 改了 .env 但没重启 dev server(值是「构建时替换」的)
// ④ 直接把后端域名写进代码(换环境要改代码)
const API = 'http://localhost:3000/api'; // ⚠️
const API2 = import.meta.env.VITE_API_BASE_URL; // ✅
// ⑤ 生产环境的 index.html 被强缓存(用户一直用旧版本)
// ⑥ 生产环境静态资源「没设强缓存」(每次重新下载)
// ⑦ 部署到「子路径」但 base 没配(资源 404)
export default defineConfig({ base: '/my-app/' }); // ⭐ 部署在子路径时要配
// ⑧ 没有 SPA fallback(刷新非根路径 404)
// ⑨ 生产构建没开 sourcemap 也没上传(线上错误无法定位,见 8.6)
// ⑩ manualChunks 分得太细(请求数暴增)或太粗(缓存粒度差)
// ⑪ 忘记「路径别名在 tsconfig 里也要配」(编辑器报错但能构建)
// ⑫ 用 `__dirname`(ESM 里不可用)→ 用 `import.meta.url` 或 `fileURLToPath`
// ⑬ 把「大依赖」打进首屏 bundle(应该按需引入或懒加载)
// ⑭ 「依赖预构建」出错时反复重启(可以 `--force` 清缓存)
rm -rf node_modules/.vite
// ⑮ 生产环境用 dev server 提供服务(性能与安全都不行)
// ⑯ 忽略「构建产物体积分析」(不知道哪个包最大)
npm i -D rollup-plugin-visualizer
// ⑰ 用「绝对路径」引用 public 里的资源(应该用 /xxx.png)
import logo from '/logo.png'; // ⚠️ 会被当成「绝对 URL」
import logo2 from '@/assets/logo.png'; // ✅ 走构建处理(带 hash)
// ⑫ 的正确写法(ESM 下没有 __dirname)
import { fileURLToPath, URL } from 'node:url';
export default defineConfig({
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
});
九、面试延伸
- 「React 项目怎么选脚手架?」
按「需求」选:① 不需要 SEO 的 SPA(后台系统、内部工具)→ Vite + React Router(开发启动极快、配置简单);② 需要 SEO / SSR / 全栈能力(内容站、电商、营销页)→ Next.js(文件路由 + RSC + Server Actions + 内置 SSR/SSG/ISR);③ 组件库 → Vite 的库模式;④ 不要用 CRA(官方已停止维护、Webpack 配置繁琐、启动慢)。 -
「Vite 为什么比 Webpack 快?」
核心是「开发环境不打包」:① Webpack 启动时会把整个应用打包成 bundle(时间与项目规模成正比),Vite 则让浏览器直接用原生 ESM 加载源文件,服务端按需转换单个模块(冷启动几乎瞬间);② 依赖用 esbuild 预构建(Go 编写,极快)并缓存到node_modules/.vite;③ HMR 基于「原生 ESM 的模块图」,只让受影响的模块失效(毫秒级)。生产构建 Vite 用 Rollup(成熟、tree-shaking 好)。 -
「Vite 的『依赖预构建』是做什么的?」
两件事:① 把 CJS 转成 ESM——很多 npm 包仍是 CommonJS 格式,而浏览器只认 ESM,预构建把它们转好;② 把「由很多小文件组成的包」合并——如lodash有上百个模块,如果不合并,浏览器要发上百个请求(即使 HTTP/2 也有开销)。它用 esbuild 执行(极快),产物缓存在node_modules/.vite(依赖没变就复用缓存)。 -
「前端环境变量有什么安全风险?」
关键在于「前端环境变量只是『构建时替换的常量』」——它们会被直接写进产物代码,任何访问者都能在 JS 里找到。所以:① 「密钥、数据库密码、管理 token」绝不能放前端环境变量,应该放服务端(由服务端代理请求,或只在服务端运行时读取);② 只有「本来就该公开的信息」(API base URL、应用标题、Sentry DSN 这类设计为公开的值)才用VITE_/NEXT_PUBLIC_前缀暴露。 -
「开发环境怎么处理跨域?」
三种方式:① 代理(推荐)——Vite 的server.proxy把/api转发到后端,代码里用相对路径(fetch('/api/users'));② 后端开 CORS(需要处理预检、凭证、多个环境);③ 本地起一个「同域的网关」。推荐代理的原因是:开发与生产行为一致(都是同源)、不需要后端配合、换环境不用改代码(生产用 Nginx 反向代理实现同样效果)。 -
「生产环境的静态资源缓存怎么配?」
核心是「按文件名是否有哈希来区分」:① 带内容哈希的资源(assets/index-a1b2c3.js)——设「长期强缓存」(Cache-Control: public, max-age=31536000, immutable),因为内容变了文件名就变,不存在「拿到旧内容」的问题;②index.html——不要强缓存(用no-cache或很短的max-age),否则用户会一直用「引用旧资源」的 HTML;③ 「接口响应」按业务配(通常no-store或短缓存)。 -
「部署 SPA 为什么刷新会 404?」
因为 SPA 用 History 路由(URL 形如/dashboard/settings),而服务器上并没有对应文件——刷新时浏览器直接请求该路径,服务器找不到就返回 404。解法是「SPA fallback」:把所有「未匹配到静态文件的请求」返回index.html(Nginx 的try_files $uri $uri/ /index.html;),让前端路由接管。替代方案是改用 Hash 路由(不需要服务端配置,但 URL 不好看、SEO 更差)。 -
「怎么优化构建产物?」
五个方向:① 代码分割(路由级懒加载,见 5.6);②manualChunks分包——把「变动少的大依赖」(React、UI 库)单独抽成一个 chunk,利用浏览器缓存(业务代码变了不影响它);③ 按需引入——避免import _ from 'lodash'这类全量引入(换成lodash/debounce或用 tree-shakable 的替代库);④ 分析产物——用rollup-plugin-visualizer找出「最大的几个包」;⑤ 压缩与优化——gzip/brotli(服务端配置)、图片优化、去掉未使用的 polyfill。
一句话速记
SPA 用 Vite(开发不打包 + 原生 ESM + esbuild 预构建 + 毫秒级 HMR,生产用 Rollup)、需要 SEO/SSR 用 Next.js、CRA 已废弃;Vite 常用配置是「
resolve.alias路径别名(tsconfig也要配)、server.proxy开发代理(配相对路径,开发生产一致)、build.rollupOptions.output.manualChunks手动分包、define注入常量」;环境变量只有VITE_前缀会暴露(默认是「构建时常量」,改了要重启,绝不能放密钥);部署要点是「带 hash 的资源长期强缓存、index.html不缓存、配 SPA fallback(try_files ... /index.html)、子路径部署配base」;生产 sourcemap 要留给监控平台(别公开);用rollup-plugin-visualizer分析产物体积。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!