陌上人如玉
公子世无双

10.1 脚手架与构建

10.1 脚手架与构建

一句话: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)),
    },
  },
});

九、面试延伸

  1. 「React 项目怎么选脚手架?」
    按「需求」选:① 不需要 SEO 的 SPA(后台系统、内部工具)→ Vite + React Router(开发启动极快、配置简单);② 需要 SEO / SSR / 全栈能力(内容站、电商、营销页)→ Next.js(文件路由 + RSC + Server Actions + 内置 SSR/SSG/ISR);③ 组件库 → Vite 的库模式;④ 不要用 CRA(官方已停止维护、Webpack 配置繁琐、启动慢)。

  2. 「Vite 为什么比 Webpack 快?」
    核心是「开发环境不打包」:① Webpack 启动时会把整个应用打包成 bundle(时间与项目规模成正比),Vite 则让浏览器直接用原生 ESM 加载源文件,服务端按需转换单个模块(冷启动几乎瞬间);② 依赖用 esbuild 预构建(Go 编写,极快)并缓存到 node_modules/.vite;③ HMR 基于「原生 ESM 的模块图」,只让受影响的模块失效(毫秒级)。生产构建 Vite 用 Rollup(成熟、tree-shaking 好)。

  3. 「Vite 的『依赖预构建』是做什么的?」
    两件事:① 把 CJS 转成 ESM——很多 npm 包仍是 CommonJS 格式,而浏览器只认 ESM,预构建把它们转好;② 把「由很多小文件组成的包」合并——如 lodash 有上百个模块,如果不合并,浏览器要发上百个请求(即使 HTTP/2 也有开销)。它用 esbuild 执行(极快),产物缓存在 node_modules/.vite(依赖没变就复用缓存)。

  4. 「前端环境变量有什么安全风险?」
    关键在于「前端环境变量只是『构建时替换的常量』」——它们会被直接写进产物代码,任何访问者都能在 JS 里找到。所以:① 「密钥、数据库密码、管理 token」绝不能放前端环境变量,应该放服务端(由服务端代理请求,或只在服务端运行时读取);② 只有「本来就该公开的信息」(API base URL、应用标题、Sentry DSN 这类设计为公开的值)才用 VITE_ / NEXT_PUBLIC_ 前缀暴露。

  5. 「开发环境怎么处理跨域?」
    三种方式:① 代理(推荐)——Vite 的 server.proxy 把 /api 转发到后端,代码里用相对路径(fetch('/api/users'));② 后端开 CORS(需要处理预检、凭证、多个环境);③ 本地起一个「同域的网关」。推荐代理的原因是:开发与生产行为一致(都是同源)、不需要后端配合、换环境不用改代码(生产用 Nginx 反向代理实现同样效果)。

  6. 「生产环境的静态资源缓存怎么配?」
    核心是「按文件名是否有哈希来区分」:① 带内容哈希的资源(assets/index-a1b2c3.js)——设「长期强缓存」(Cache-Control: public, max-age=31536000, immutable),因为内容变了文件名就变,不存在「拿到旧内容」的问题;② index.html——不要强缓存(用 no-cache 或很短的 max-age),否则用户会一直用「引用旧资源」的 HTML;③ 「接口响应」按业务配(通常 no-store 或短缓存)。

  7. 「部署 SPA 为什么刷新会 404?」
    因为 SPA 用 History 路由(URL 形如 /dashboard/settings),而服务器上并没有对应文件——刷新时浏览器直接请求该路径,服务器找不到就返回 404。解法是「SPA fallback」:把所有「未匹配到静态文件的请求」返回 index.html(Nginx 的 try_files $uri $uri/ /index.html;),让前端路由接管。替代方案是改用 Hash 路由(不需要服务端配置,但 URL 不好看、SEO 更差)。

  8. 「怎么优化构建产物?」
    五个方向:① 代码分割(路由级懒加载,见 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 分析产物体积。

赞(0) 打赏
未经允许不得转载:陌上寒 » 10.1 脚手架与构建

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫