1. Vite 原理简介:为什么 Vite 这么快
传统打包工具(如 webpack)在启动开发服务器时,需要先将整个应用打包成一个 bundle 再启动。项目越大,启动越慢。Vite 用了两个关键技术解决这个问题:
| 对比维度 | webpack | Vite |
|---|---|---|
| 开发启动速度 | 慢(需全量打包) | 极快(按需编译) |
| HMR 速度 | 较慢(需重新打包) | 极快(精准更新模块) |
| 生产构建 | Webpack(插件生态丰富) | Rollup(输出更优) |
| 配置复杂度 | 较高 | 低(开箱即用) |
| TypeScript | 需 ts-loader | 内置(esbuild 转译) |
npm run dev 和 npm run build 走的是完全不同的流程。2. vite.config.ts 详解
// vite.config.ts — 常用配置项完整示例
import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
// ── 路径别名:避免 '../../../' 地狱 ──
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@components': path.resolve(__dirname, './src/components'),
'@utils': path.resolve(__dirname, './src/utils'),
},
},
// ── 开发服务器配置 ──
server: {
port: 3000,
open: true, // 启动时自动打开浏览器
// 代理:解决开发环境跨域问题
proxy: {
'/api': {
target: 'http://localhost:8080', // 后端地址
changeOrigin: true,
// rewrite: (path) => path.replace(/^\/api/, '')
},
},
},
// ── 生产构建配置 ──
build: {
outDir: 'dist',
sourcemap: true, // 生成 source map(便于生产环境调试)
minify: 'esbuild', // 压缩方式
rollupOptions: {
output: {
// 手动控制代码分割:第三方库单独打包
manualChunks: {
react: ['react', 'react-dom'],
router: ['react-router-dom'],
query: ['@tanstack/react-query'],
},
},
},
},
});
配合 TypeScript,需在 tsconfig.json 中添加路径别名映射:
// tsconfig.json — 与 vite.config.ts 的 alias 保持一致
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"]
}
}
}
3. 环境变量
Vite 通过 .env 文件管理环境变量。只有以 VITE_ 开头的变量才会暴露给前端代码(安全机制,防止泄露服务器密钥)。
# .env — 所有环境都加载(通用变量)
VITE_APP_TITLE=我的应用
# .env.development — 开发环境(npm run dev)
VITE_API_URL=http://localhost:8080
# .env.production — 生产环境(npm run build)
VITE_API_URL=https://api.myapp.com
# .env.local — 本地覆盖(不提交到 Git!)
VITE_API_KEY=my-secret-key
// 在代码中访问环境变量
const apiUrl = import.meta.env.VITE_API_URL;
const isDev = import.meta.env.DEV; // Vite 内置:开发模式为 true
const isProd = import.meta.env.PROD; // Vite 内置:生产模式为 true
const mode = import.meta.env.MODE; // "development" 或 "production"
// 为环境变量添加 TypeScript 类型
// src/env.d.ts
interface ImportMetaEnv {
readonly VITE_API_URL: string;
readonly VITE_APP_TITLE: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
4. 代码分割(Code Splitting)
默认情况下,Vite 将所有代码打包进一个 JS 文件。用户打开任意页面都需要下载整个 bundle。代码分割(Code Splitting)将 bundle 拆分成多个 chunk,按需加载。
动态 import()
// 静态导入:打包时合并进主 bundle
import HeavyComponent from './HeavyComponent';
// 动态导入:生成单独的 chunk,点击时才下载
async function handleClick() {
const { default: HeavyModule } = await import('./HeavyModule');
HeavyModule.doSomething();
}
React.lazy + Suspense — 路由级懒加载
最常见的使用场景是对页面组件进行懒加载:用户访问哪个路由,才下载对应的 JS 文件。
import { lazy, Suspense } from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
// lazy() 包裹动态 import,每个页面生成独立 chunk
const HomePage = lazy(() => import('./pages/HomePage'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const SettingsPage = lazy(() => import('./pages/SettingsPage'));
function App() {
return (
// Suspense 是必须的:lazy 组件加载期间显示 fallback
<Suspense fallback={<div className="page-loading">页面加载中...</div>}>
<RouterProvider router={router} />
</Suspense>
);
}
5. 性能优化
React.memo — 防止不必要的重渲染
// 默认情况:父组件任何 state 变化都会重渲染所有子组件
// React.memo:只有 props 变化时才重渲染
import { memo } from 'react';
interface ListItemProps {
item: Item;
onDelete: (id: number) => void;
}
// 用 memo 包裹:item 和 onDelete 不变时跳过渲染
const ListItem = memo(function ListItem({ item, onDelete }: ListItemProps) {
return (
<li>
{item.name}
<button onClick={() => onDelete(item.id)}>删除</button>
</li>
);
});
// ⚠️ 注意:onDelete 要用 useCallback,否则每次父组件渲染都是新函数引用
function List({ items }: { items: Item[] }) {
const handleDelete = useCallback((id: number) => {
setItems((prev) => prev.filter((item) => item.id !== id));
}, []); // 空依赖 = 函数引用永远不变
return <ul>{items.map((item) => <ListItem key={item.id} item={item} onDelete={handleDelete} />)}</ul>;
}
useMemo — 缓存计算结果
import { useMemo } from 'react';
function DataTable({ data, filter }: Props) {
// 只有 data 或 filter 变化时才重新计算,避免每次渲染都遍历
const filteredData = useMemo(
() => data.filter((item) => item.name.includes(filter)),
[data, filter]
);
const total = useMemo(
() => filteredData.reduce((sum, item) => sum + item.price, 0),
[filteredData]
);
return <div>共 {filteredData.length} 条,总计 {total}</div>;
}
虚拟化长列表:react-window
渲染几千条列表项会导致 DOM 节点过多,页面卡顿。虚拟化(Virtualization)技术只渲染视口内可见的列表项,其余只在用户滚动到时才渲染。
# npm install react-window
import { FixedSizeList as List } from 'react-window';
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
<div style={style}>行 {index + 1}</div>
);
// 只有可见的行被渲染到 DOM,无论 itemCount 多大
<List
height={400} // 容器高度
itemCount={10000} // 总条目数
itemSize={50} // 每行高度
width="100%"
>
{Row}
</List>
6. Tree Shaking
Tree Shaking(摇树优化)是打包工具在构建时自动移除未被使用的代码(Dead Code)。就像摇动一棵树,将枯叶(无用代码)摇落。
生效条件:库必须使用 ES Modules(import/export),而不是 CommonJS(require/module.exports)。Rollup(Vite 生产构建)对 ESM 有完整的 Tree Shaking 支持。
// ✅ 具名导入:打包时只包含用到的函数
import { debounce } from 'lodash-es'; // 注意:用 lodash-es,不是 lodash
// ❌ 全量导入:即使只用 debounce,也会打包整个 lodash
import _ from 'lodash'; // lodash 是 CommonJS,无法 Tree Shaking
// ✅ 确认库是否支持 Tree Shaking:检查 package.json
// {
// "main": "dist/index.cjs", // CommonJS 入口
// "module": "dist/index.esm.js", // ESM 入口(Tree Shaking 用这个)
// "sideEffects": false // 告知打包工具无副作用,可安全 Tree Shake
// }
7. Bundle 体积分析
使用 rollup-plugin-visualizer 生成可视化的 bundle 分析图,直观看出哪个依赖占用体积最大。
# npm install -D rollup-plugin-visualizer
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
react(),
visualizer({
open: true, // 构建后自动打开分析页面
filename: 'stats.html',
gzipSize: true,
}),
],
});
# 运行后打开 stats.html 查看树状图
npm run build
8. 部署到 Vercel
Vercel 是前端部署的首选平台,与 GitHub 深度集成,每次 push 自动部署,无需配置服务器。
- 访问 vercel.com,用 GitHub 账号登录
- 点击 "Add New Project",选择你的 GitHub 仓库
- Vercel 自动检测 Vite 框架,构建命令已预填(
npm run build),输出目录为dist - 点击 "Deploy",几分钟后获得
*.vercel.app域名 - 此后每次 push 到 main 分支自动重新部署;每个 PR 生成预览链接
环境变量配置
# Vercel 面板:Settings → Environment Variables
# 添加 VITE_API_URL 等环境变量
# 可以为 Production / Preview / Development 分别设置不同值
# 或使用 Vercel CLI
npx vercel env add VITE_API_URL production
vercel.json — 高级配置
// vercel.json(可选,放在项目根目录)
{
"rewrites": [
// SPA fallback:所有路由都返回 index.html
{ "source": "/(.*)", "destination": "/index.html" }
],
"headers": [
{
"source": "/assets/(.*)",
"headers": [
// 静态资源永久缓存(Vite 的 hash 文件名保证内容变更时 URL 变化)
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
]
}
]
}
9. 部署到 Netlify
Netlify 与 Vercel 类似,但有独特的 Forms、Identity、Edge Functions 等功能。
关键文件:_redirects(SPA 路由 fallback)
静态托管服务器在用户直接访问 /dashboard 时会返回 404,因为服务器上没有这个文件。需要配置 fallback,让所有路由都返回 index.html,由前端路由接管。
# public/_redirects — 放在 public 目录(会被复制到 dist 根目录)
/* /index.html 200
# 格式:来源路径 目标路径 状态码
# 200 表示重写(rewrite),而不是重定向(redirect)
# API 请求代理到后端:
/api/* https://api.myapp.com/:splat 200
# netlify.toml(可选,比 _redirects 更强大)
[build]
command = "npm run build"
publish = "dist"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
[build.environment]
NODE_VERSION = "20"
10. CI/CD:GitHub Actions
CI(持续集成)是指每次代码提交时自动运行测试和构建;CD(持续部署)是指测试通过后自动部署。GitHub Actions 是 GitHub 内置的 CI/CD 工具,免费配额对大多数开源项目足够。
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test-and-build:
runs-on: ubuntu-latest
steps:
# 1. 检出代码
- uses: actions/checkout@v4
# 2. 安装 Node.js
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
# 3. 安装依赖
- name: Install dependencies
run: npm ci
# 4. 类型检查
- name: TypeScript check
run: npx tsc --noEmit
# 5. Lint 检查
- name: ESLint
run: npm run lint
# 6. 运行测试
- name: Run tests
run: npm run test:run
# 7. 生产构建
- name: Build
run: npm run build
env:
VITE_API_URL: ${{ secrets.VITE_API_URL }} # 从 Secrets 读取