Chapter 10

构建优化与部署

Vite 构建原理、代码分割、性能优化、Tree Shaking 与 Vercel/Netlify 部署

1. Vite 原理简介:为什么 Vite 这么快

传统打包工具(如 webpack)在启动开发服务器时,需要先将整个应用打包成一个 bundle 再启动。项目越大,启动越慢。Vite 用了两个关键技术解决这个问题:

开发时:ES Modules (ESM)浏览器原生支持 ESM,Vite 直接将文件以 ES 模块形式提供给浏览器,按需编译(只编译浏览器请求的文件),启动几乎瞬间完成。
🔧
预构建:esbuild第三方依赖(node_modules)用 esbuild 预打包,esbuild 用 Go 编写,比 JS 工具快 10-100 倍,只需运行一次。
对比维度webpackVite
开发启动速度慢(需全量打包)极快(按需编译)
HMR 速度较慢(需重新打包)极快(精准更新模块)
生产构建Webpack(插件生态丰富)Rollup(输出更优)
配置复杂度较高低(开箱即用)
TypeScript需 ts-loader内置(esbuild 转译)
💡
开发 vs 生产的区别:Vite 开发服务器使用 ESM + esbuild,不生成 bundle。生产构建使用 Rollup,会进行完整打包、压缩、代码分割。npm run devnpm 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 本身已经很快了,memo/useMemo/useCallback 有额外的比较开销。只在 Profiler 分析出真实性能问题时才使用。过度使用反而增加代码复杂度。

虚拟化长列表: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
🔍
常见优化方向:发现 moment.js(超大)时换成 date-fns;发现 lodash(CJS)时换成 lodash-es;发现所有页面都在一个 chunk 时配置路由懒加载。

8. 部署到 Vercel

Vercel 是前端部署的首选平台,与 GitHub 深度集成,每次 push 自动部署,无需配置服务器。

  1. 访问 vercel.com,用 GitHub 账号登录
  2. 点击 "Add New Project",选择你的 GitHub 仓库
  3. Vercel 自动检测 Vite 框架,构建命令已预填(npm run build),输出目录为 dist
  4. 点击 "Deploy",几分钟后获得 *.vercel.app 域名
  5. 此后每次 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 读取
🚀
部署与 CI 联动:Vercel 和 Netlify 都有官方 GitHub Actions 集成。PR 创建时自动生成预览链接并在 PR 页面显示,main 分支合并后自动部署到生产环境,整个流程无需人工干预。
🎉
恭喜完成全部10章!你已经掌握了现代 React / TypeScript 开发的核心知识体系——从 JavaScript 基础、TypeScript 类型系统、React Hooks、状态管理、路由、数据请求、样式方案、测试,到构建优化与部署。接下来,动手做一个真实项目,巩固所学知识吧!