前端开发··2 阅读·预计 17 分钟

Vite + React 工程化防坑指南:从路径别名到环境变量,6 个让你少走弯路的配置最佳实践

引言

Vite 已经成为 React 项目的标配构建工具。启动快、HMR 丝滑、配置简洁——这些优点让人很容易忽略一个事实:默认配置离生产级工程化还有很长的路

本文总结了 6 个在实际项目中反复踩过的坑,每一个都附正反例对比和自动化校验方案。


1. 路径别名:别让 ../../../ 毁了你的重构

❌ 常见错误

// components/Header.tsx — 深度嵌套时的灾难
import { Button } from '../../../ui/Button'
import { useAuth } from '../../../../hooks/useAuth'
import { formatDate } from '../../../../../utils/date'

重构时移动一个文件,所有相对路径全部崩坏。

✅ 正确做法

// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'

export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
      '@ui': path.resolve(__dirname, 'src/components/ui'),
      '@hooks': path.resolve(__dirname, 'src/hooks'),
      '@utils': path.resolve(__dirname, 'src/utils'),
    },
  },
  plugins: [react()],
})
// components/Header.tsx — 重构无忧
import { Button } from '@ui/Button'
import { useAuth } from '@hooks/useAuth'
import { formatDate } from '@utils/date'

🔧 配套 TS 配置

// tsconfig.json — Vite 不会自动同步给 TS Server
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@ui/*": ["src/components/ui/*"],
      "@hooks/*": ["src/hooks/*"],
      "@utils/*": ["src/utils/*"]
    }
  }
}

关键点vite.config.tsresolve.alias 只在构建时生效;IDE 的智能提示需要 tsconfig.jsonpaths。两者缺一不可。

🛡️ 自动化校验

// scripts/check-relative-imports.ts — 禁止深度相对引用
import { readFileSync } from 'fs'
import { globSync } from 'glob'

const files = globSync('src/**/*.{ts,tsx}')
const pattern = /\.\.\/(?:\.\.\/){2,}/ // 3 层及以上

for (const file of files) {
  const content = readFileSync(file, 'utf-8')
  const lines = content.split('\n')
  for (let i = 0; i < lines.length; i++) {
    if (pattern.test(lines[i])) {
      console.error(`❌ ${file}:${i + 1} — 禁止使用 3 层以上相对路径`)
      process.exit(1)
    }
  }
}

加入 lint-staged 或 CI,从源头杜绝。


2. 环境变量:VITE_ 前缀的隐蔽规则

❌ 常见错误

# .env
API_BASE_URL=https://api.prod.com
SECRET_KEY=abc123
// 运行时拿不到!
console.log(import.meta.env.API_BASE_URL) // undefined
console.log(import.meta.env.SECRET_KEY)   // undefined

✅ 正确做法

Vite 只暴露 VITE_ 前缀 的环境变量给客户端。

# .env
VITE_API_BASE_URL=https://api.prod.com
VITE_APP_TITLE=MyApp
// 类型安全的环境变量
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_API_BASE_URL: string
  readonly VITE_APP_TITLE: string
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}

🔐 敏感信息防护

// vite.config.ts — 构建时注入,而非运行时暴露
export default defineConfig({
  define: {
    __BUILD_TIME__: JSON.stringify(new Date().toISOString()),
    __COMMIT_HASH__: JSON.stringify(process.env.CI_COMMIT_SHA ?? 'dev'),
  },
})

铁律SECRET_KEYDATABASE_URL 等敏感信息绝不出现在客户端 bundle 中。如需运行时配置,走 /api/config 接口动态获取。

🛡️ 自动化校验

// scripts/check-env-leak.ts — 扫描 bundle 中的敏感模式
import { readFileSync } from 'fs'

const dist = readFileSync('dist/assets/index-*.js', 'utf-8')
  .match(/dist\/assets\/index-[^.]+\.[^.]+/g)
  ?.map(f => readFileSync(f, 'utf-8'))
  .join('')

const forbidden = ['SECRET', 'PASSWORD', 'PRIVATE_KEY', 'DATABASE_URL']
for (const key of forbidden) {
  if (dist?.includes(key)) {
    console.error(`❌ 敏感信息 ${key} 泄露到客户端 bundle!`)
    process.exit(1)
  }
}

3. 代理配置:开发环境与生产环境的同源裂痕

❌ 常见错误

// src/api/client.ts
const API_BASE = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:5173/api'

// vite.config.ts
server: { proxy: { '/api': 'http://localhost:3000' } }

生产环境 /api 走 Nginx 反向代理,但 URL 拼接逻辑散落在各个文件中,行为不可预测。

✅ 正确做法

// vite.config.ts — 集中配置,消除环境差异
export default defineConfig(({ mode }) => ({
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },
}))
// src/api/client.ts — 永远用相对路径
const fetcher = async <T>(url: string): Promise<T> => {
  const res = await fetch(url) // /api/users — 开发走 proxy,生产走 Nginx
  if (!res.ok) throw new Error(`HTTP ${res.status}`)
  return res.json()
}

核心原则:客户端不应该知道 API 的完整 URL。相对路径让开发/生产环境的行为完全一致。


4. 构建产物分析:你的 bundle 里藏着什么?

❌ 直觉陷阱

vite build  # 构建完成
ls -lh dist/assets/
# index-abc123.js  1.2MB  ← "还行吧"

✅ 正确做法

npm i -D rollup-plugin-visualizer
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    react(),
    visualizer({
      open: true,        // 构建后自动打开
      gzipSize: true,    // 显示 gzip 尺寸
      brotliSize: true,  // 显示 brotli 尺寸
      filename: 'dist/stats.html',
    }),
  ],
})

可视化分析让你一眼看清:

  • moment.js 占了 200KB——换成 dayjs(2KB)
  • lodash 整包引入——改成 import debounce from 'lodash/debounce'
  • antd 图标全量加载——启用 @ant-design/icons 按需引入

🔎 告警门禁

// scripts/check-bundle-size.ts — CI 中拦截体积膨胀
import { readFileSync, readdirSync } from 'fs'
import path from 'path'

const distDir = 'dist/assets'
const MAX_SIZE = 300 * 1024 // 300KB

for (const file of readdirSync(distDir)) {
  if (!file.endsWith('.js')) continue
  const size = readFileSync(path.join(distDir, file)).length
  if (size > MAX_SIZE) {
    console.error(`❌ ${file}: ${(size / 1024).toFixed(1)}KB 超出限制 ${MAX_SIZE / 1024}KB`)
    process.exit(1)
  }
}

5. HMR 失效:何时需要手动 import.meta.hot

❌ 隐蔽问题

// stores/counter.ts — Zustand store 修改后页面不更新?
import { create } from 'zustand'

export const useCounter = create<CounterState>((set) => ({
  count: 0,
  increment: () => set((s) => ({ count: s.count + 1 })),
}))

修改 store 后页面不刷新——因为 Vite 的 HMR 边界在组件层面,非组件模块的变更不会触发更新。

✅ 正确做法

// stores/counter.ts — 手动声明 HMR 边界
import { create } from 'zustand'

export const useCounter = create<CounterState>((set) => ({
  count: 0,
  increment: () => set((s) => ({ count: s.count + 1 })),
}))

if (import.meta.hot) {
  import.meta.hot.accept(() => {
    // Store 变更触发全量 HMR
    console.log('[HMR] counter store updated')
  })
}

更优雅的方案——在 store 文件顶层统一处理:

// stores/setup-hmr.ts
const stores = import.meta.glob('./*.ts', { eager: true })

if (import.meta.hot) {
  import.meta.hot.accept(Object.keys(stores), () => {
    console.log('[HMR] Stores updated, components will re-render')
  })
}

6. CSS 模块:TypeScript 中的类型安全样式

❌ 运行时才发现拼写错误

import styles from './Button.module.css'

<button className={styles.primay}>{/* 拼写错误,值为 undefined */}
  Click
</button>

✅ 自动生成类型声明

npm i -D typed-css-modules
// package.json
{
  "scripts": {
    "css-types": "tcm src -w",
    "dev": "concurrently \"vite\" \"npm run css-types\""
  }
}

运行后自动生成 *.css.d.ts

// Button.module.css.d.ts — 自动生成
declare const styles: {
  readonly primary: string
  readonly secondary: string
  readonly disabled: string
}
export default styles

编译时就能发现拼写错误:

<button className={styles.primary}>  // ✅ 类型检查通过
<button className={styles.primay}>   // ❌ TS2322: 'primay' does not exist

总结

维度最小方案推荐方案
路径别名@/ 单别名多别名 + 相对路径 Lint 规则
环境变量手动 .env类型声明 + CI 敏感信息扫描
API 代理硬编码 URL相对路径 + Vite proxy
产物分析ls -lh dist/rollup-plugin-visualizer + 体积门禁
HMR 边界手动刷新import.meta.hot.accept + glob 批量注册
CSS 类型运行时调试typed-css-modules 自动生成 .d.ts

工程化的本质不是堆砌工具,而是让正确的事变得容易,让错误的事变得不可能

这些配置不需要每次从零搭起——建议沉淀为团队内部的 Vite preset 或脚手架模板,新项目开箱即用,老项目渐进迁移。

0 评论

评论区

登录 后参与评论