Skip to content

前端构建

说明

前端使用 Vite 构建,支持开发服务器和生产构建。vite.config.ts 通过环境变量和插件系统管理构建配置,生产环境推荐使用 Nginx 部署。

构建架构

vite.config.ts

   ├─ loadEnv(mode)          ← 按 mode 加载 .env / .env.{mode}
   ├─ wrapperEnv()           ← 类型转换(string→number/boolean/JSON)
   ├─ createVitePlugins()    ← 组装插件链
   ├─ createProxy()          ← 生成代理配置


┌──────────────────────────────────────────────────────┐
│  Vite 构建                                           │
│  ├─ 开发:vite dev(HMR + proxy)                    │
│  └─ 生产:vite build → postBuild → app.config.js     │
└──────────────────────────────────────────────────────┘

开发环境

bash
# 安装依赖
pnpm install

# 启动开发服务器(HMR 热更新)
pnpm dev

# 等效命令
pnpm serve

开发服务器默认运行在 http://localhost:8001(端口由 VITE_PORT 控制),通过 Vite 代理将 /api 请求转发到后端。

多环境开发命令

命令mode加载的 env 文件说明
pnpm devdevelopment.env + .env.development本地开发(默认)

环境变量体系

env 文件加载规则

Vite 按 mode 加载环境变量,后面的文件覆盖前面的:

文件加载时机说明
.env所有模式通用配置(端口、标题、短名)
.env.developmentpnpm dev开发配置(代理、mock)
.env.productionpnpm build生产配置
.env.testpnpm build:test测试环境
.env.uatpnpm build:uatUAT 环境
.env.devpnpm build:dev开发联调环境

环境变量完整参考

变量类型默认值说明
VITE_PORTnumber8001开发服务器端口
VITE_PUBLIC_PATHstring/部署基础路径(子目录部署时修改)
VITE_GLOB_APP_TITLEstringFastAPI+ElementPlus版应用标题(注入 HTML)
VITE_GLOB_APP_SHORT_NAMEstringFastAPI+ElementPlus版应用短名(用于生成运行时配置变量名)
VITE_GLOB_API_URLstringAPI 基础路径(留空使用相对路径)
VITE_GLOB_API_URL_PREFIXstring/apiAPI 前缀
VITE_GLOB_FILE_URLstring-文件访问域名(拼接上传文件 URL)
VITE_GLOB_UPLOAD_URLstring-图片上传地址
VITE_GLOB_IMG_URLstring-图片前缀地址
VITE_GLOB_PROD_MOCKbooleanfalse生产环境是否开启 mock
VITE_USE_MOCKbooleanfalse开发环境是否开启 mock
VITE_DROP_CONSOLEbooleantrue构建时删除 console
VITE_PROXYJSON[["/api","http://127.0.0.1:8031/api/v1"]]跨域代理配置(JSON 数组)
VITE_BUILD_COMPRESSstring'none'压缩方式:gzip / brotli / none
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILEbooleanfalse压缩后是否删除原文件

代理配置格式

VITE_PROXY 是 JSON 格式的二维数组,每个元素 [前缀, 目标地址]

bash
# 单个代理
VITE_PROXY=[["/api","http://127.0.0.1:8031/api/v1"]]

# 多个代理(注意不要换行)
VITE_PROXY=[["/api","http://127.0.0.1:8031/api/v1"],["/ws","ws://127.0.0.1:8031/ws"]]

代理生成逻辑(build/vite/proxy.ts):

typescript
// 输入: [["/api", "http://127.0.0.1:8031/api/v1"]]
// 输出:
{
  '/api': {
    target: 'http://127.0.0.1:8031/api/v1',
    changeOrigin: true,    // 修改 Origin 头
    ws: true,              // 支持 WebSocket
    rewrite: (path) => path.replace(/^\/api/, ''),  // 去掉前缀
  }
}

代码中使用环境变量

typescript
// import.meta.env 包含所有 VITE_ 开头的环境变量
const apiBaseUrl = import.meta.env.VITE_GLOB_API_URL;
const fileUrl = import.meta.env.VITE_GLOB_FILE_URL;

// 类型声明在 types/global.d.ts
interface ImportMetaEnv {
  readonly VITE_GLOB_API_URL: string;
  readonly VITE_GLOB_FILE_URL: string;
  // ...
}

wrapperEnv 类型转换

build/utils.ts 中的 wrapperEnv 函数自动转换环境变量类型:

原始值转换结果
"true"true(布尔)
"false"false(布尔)
"8001"(VITE_PORT)8001(数字)
'[["/api","http://..."]]'(VITE_PROXY)[["/api","http://..."]](JSON 解析)
其他字符串保持原样

插件系统

build/vite/plugin/index.ts 组装所有 Vite 插件:

插件列表

插件说明生效时机
@vitejs/plugin-vueVue3 SFC 支持始终
@vitejs/plugin-vue-jsxJSX/TSX 支持始终
vite-plugin-vue-setup-extend<script setup> 支持 name 属性始终
unplugin-auto-importVue/Router/Pinia API 自动导入始终
vite-plugin-htmlHTML 模板注入(标题、运行时配置)始终
vite-plugin-mockMock 数据服务VITE_USE_MOCK=true
rollup-plugin-visualizer打包体积分析REPORT=true
vite-plugin-compressiongzip/brotli 压缩仅构建时

自动导入

unplugin-auto-import 自动导入以下 API,无需手动 import:

typescript
// 以下 API 可直接使用,无需 import
import { ref, computed, watch, onMounted } from 'vue';     // 自动
import { useRouter, useRoute } from 'vue-router';           // 自动
import { defineStore } from 'pinia';                         // 自动

自动生成类型声明文件 auto-imports.d.ts

HTML 模板注入

vite-plugin-html 在构建时:

  1. VITE_GLOB_APP_TITLE 注入到 index.html<title> 标签
  2. 注入 app.config.js<script> 标签(运行时配置)
html
<!-- 构建后的 index.html -->
<title>FastAPI+ElementPlus版</title>
<script src="/app.config.js?v=1.0.0-1725000000000"></script>

运行时配置(app.config.js)

构建后自动生成 dist/app.config.js,包含 VITE_GLOB_ 开头的环境变量:

javascript
// dist/app.config.js(构建后自动生成,不可手动修改)
window.__PRODUCTION_FASTAPI_ELEMENTPLUS___CONF__ = {
  "VITE_GLOB_API_URL": "",
  "VITE_GLOB_API_URL_PREFIX": "/api",
  "VITE_GLOB_FILE_URL": "http://file.fastapi.elevue",
  "VITE_GLOB_APP_TITLE": "FastAPI+ElementPlus版",
  // ...
};
Object.freeze(window.__PRODUCTION_FASTAPI_ELEMENTPLUS___CONF__);

为什么需要运行时配置?

import.meta.env 的值在构建时被静态替换,构建后无法修改。而 app.config.js 是运行时加载的,部署时只需修改这个文件即可切换 API 地址等配置,无需重新构建。

打包体积分析

bash
# 生成打包体积报告(自动打开浏览器)
pnpm report

生成 node_modules/.cache/visualizer/stats.html,可视化展示各模块占比。


生产构建

构建命令

命令mode说明
pnpm buildproduction生产构建(默认)
pnpm build:prodproduction等效于 pnpm build
pnpm build:testtest测试环境构建
pnpm build:uatuatUAT 环境构建
pnpm build:devdev开发联调环境构建
pnpm build:no-cacheproduction清除缓存后构建

构建流程

pnpm build

   ├─ 1. cross-env NODE_ENV=production
   │     设置 Node 环境变量

   ├─ 2. vite build --mode production
   │     加载 .env + .env.production
   │     执行 Vite 构建(代码编译、Tree-shaking、代码分割)
   │     输出到 dist/

   └─ 3. esno ./build/script/postBuild.ts
         运行 postBuild 脚本
         生成 dist/app.config.js(运行时配置)

构建产物

dist/
├── index.html                    # 入口 HTML(注入标题和 app.config.js)
├── app.config.js                 # 运行时配置(VITE_GLOB_* 变量)
├── favicon.ico                   # 网站图标
└── assets/
    ├── index-[hash].js           # 主包(Vue、ElementPlus、Pinia 等)
    ├── index-[hash].css          # 全局样式
    ├── [name]-[hash].js          # 路由懒加载 chunk(按视图分割)
    └── ...                       # 字体、图片等静态资源

代码分割策略

Vite 自动进行代码分割,主要手段:

1. 路由懒加载(最大收益)

typescript
// router 中使用 import.meta.glob 自动分割
const viewsModules = import.meta.glob('../views/**/*.{vue,tsx}');

// 每个视图组件被打包为独立 chunk
// 首屏只加载当前路由的 chunk,其他按需加载

2. 第三方库分割

Vite 自动将 node_modules 中的库分割为独立 chunk:

chunk内容
vendor-vue.jsvue + vue-router + pinia
vendor-element.jselement-plus
vendor-echarts.jsecharts(如果使用)
vendor-*.js其他大型依赖

3. 预构建依赖

optimizeDeps.include 预构建以下依赖(开发时加速冷启动):

typescript
optimizeDeps: {
  include: ['dayjs', '@vicons/ionicons5', '@vicons/antd', '@element-plus/icons-vue'],
}

应用初始化

src/main.ts 是前端入口,初始化顺序:

typescript
async function bootstrap() {
  const app = createApp(App);

  // 1. 全局注册 ElementPlus 图标
  for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
    app.component(key, component);
  }

  // 2. 全局引入 ElementPlus(中文包)
  setupElement(app);

  // 3. 全局注册自定义组件(BasicTable、BasicForm 等)
  setupCustomComponents(app);

  // 4. 注册全局指令(v-perm、v-perms、v-scrollBar)
  setupDirectives(app);

  // 5. 挂载 Pinia 状态管理
  setupStore(app);

  // 6. 挂载路由
  await setupRouter(app);

  // 7. 路由就绪后挂载应用
  await router.isReady();
  app.mount('#app', true);
}

全局注册清单

插件注册src/plugins/):

注册项文件说明
ElementPluselement.ts全局引入 + 中文包
自定义组件customComponents.tsBasicTable、BasicForm、PageWrapper 等
自定义指令directives.tsv-permv-permsv-scrollBar

自定义指令

指令语法说明
v-permv-perm="['sys:user:add']"包含其中任一权限即显示
v-permsv-perms="['sys:user:add','sys:user:edit']"包含所有权限才显示
v-scrollBarv-scrollBar自定义滚动条样式

Nginx 部署

基础配置

nginx
server {
    listen 80;
    server_name your-domain.com;
    charset utf-8;

    # Gzip 压缩
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
    gzip_min_length 1000;
    gzip_comp_level 6;

    # 前端静态文件
    location / {
        root /path/to/dist;
        index index.html;
        try_files $uri $uri/ /index.html;  # SPA 路由回退

        # 静态资源缓存(带 hash 的文件长期缓存)
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
            expires 30d;
            add_header Cache-Control "public, immutable";
        }
    }

    # API 代理
    location /api/ {
        proxy_pass http://127.0.0.1:8031/api/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # 超时配置
        proxy_connect_timeout 60s;
        proxy_read_timeout 120s;
        proxy_send_timeout 60s;
    }

    # 文件访问代理(上传文件)
    location /api/file/ {
        alias /path/to/uploads/;
        expires 7d;
        add_header Cache-Control "public";
    }
}

子目录部署

如果部署在子目录(如 https://example.com/admin/):

bash
# .env.production
VITE_PUBLIC_PATH=/admin/
nginx
location /admin/ {
    alias /path/to/dist/;
    index index.html;
    try_files $uri $uri/ /admin/index.html;
}

Docker 部署

前端独立部署

dockerfile
FROM nginx:alpine

# 复制构建产物
COPY dist/ /usr/share/nginx/html/

# 复制 Nginx 配置
COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

前后端一体部署

项目根目录的 docker-compose.ymlDockerfile 支持前后端一体部署,详见 Docker 部署 章节。


构建信息注入

vite.config.ts 通过 define 注入构建信息:

typescript
define: {
  __APP_INFO__: JSON.stringify({
    pkg: { dependencies, devDependencies, name, version },
    lastBuildTime: formatToDateTime(new Date()),
  }),
},

代码中访问:

typescript
const appInfo = __APP_INFO__;
console.log(appInfo.pkg.version);       // package.json 版本
console.log(appInfo.lastBuildTime);     // 最后构建时间

常见问题

开发环境 API 请求 404

原因:代理配置错误或后端未启动。

排查

  1. 检查 .env.developmentVITE_PROXY 格式是否正确
  2. 检查后端是否运行在 http://127.0.0.1:8031
  3. 检查浏览器 Network 面板中请求的实际 URL

构建后页面空白

可能原因

  1. VITE_PUBLIC_PATH 配置与实际部署路径不一致
  2. Nginx 缺少 try_files $uri $uri/ /index.html 导致刷新 404
  3. app.config.js 未正确加载(检查 HTML 中的 script 标签)

解决:检查 dist/index.html 中的资源路径是否正确。

构建体积过大

排查

bash
# 生成打包体积分析报告
pnpm report

优化手段

  1. 路由懒加载(已默认启用)
  2. 按需引入 ElementPlus 组件(当前为全量引入)
  3. 大型依赖(echarts、wangeditor)按需引入
  4. 开启 gzip 压缩(VITE_BUILD_COMPRESS=gzip

环境变量不生效

原因:Vite 只识别 VITE_ 开头的环境变量。

排查

  1. 变量名必须以 VITE_ 开头
  2. 修改 .env 文件后需要重启开发服务器
  3. 确认加载的 env 文件与当前 mode 匹配

热更新不工作

可能原因

  1. 文件路径大小写不一致(Windows 不敏感,但 Vite 敏感)
  2. 修改了 vite.config.ts(需要重启)
  3. 修改了 tsconfig.json(需要重启)

总结

前端使用 Vite 构建,通过 .env 文件体系管理多环境配置,wrapperEnv 自动类型转换。插件链包括 Vue3 / JSX / 自动导入 / HTML 注入 / Mock / 压缩 / 体积分析。生产构建自动生成 app.config.js 运行时配置,支持不重新构建切换 API 地址。代码分割通过路由懒加载和第三方库自动分割实现。部署推荐 Nginx,需配置 try_files 回退和 gzip 压缩。

小蚂蚁云团队 · 提供技术支持