# 前端构建 :::tip 说明 前端使用 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 dev` | `development` | `.env` + `.env.development` | 本地开发(默认) | --- ## 环境变量体系 ### env 文件加载规则 Vite 按 mode 加载环境变量,后面的文件覆盖前面的: | 文件 | 加载时机 | 说明 | |------|----------|------| | `.env` | 所有模式 | 通用配置(端口、标题、短名) | | `.env.development` | `pnpm dev` | 开发配置(代理、mock) | | `.env.production` | `pnpm build` | 生产配置 | | `.env.test` | `pnpm build:test` | 测试环境 | | `.env.uat` | `pnpm build:uat` | UAT 环境 | | `.env.dev` | `pnpm build:dev` | 开发联调环境 | ### 环境变量完整参考 | 变量 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `VITE_PORT` | `number` | `8001` | 开发服务器端口 | | `VITE_PUBLIC_PATH` | `string` | `/` | 部署基础路径(子目录部署时修改) | | `VITE_GLOB_APP_TITLE` | `string` | `Tornado+ElementPlus版` | 应用标题(注入 HTML) | | `VITE_GLOB_APP_SHORT_NAME` | `string` | `Tornado+ElementPlus版` | 应用短名(用于生成运行时配置变量名) | | `VITE_GLOB_API_URL` | `string` | 空 | API 基础路径(留空使用相对路径) | | `VITE_GLOB_API_URL_PREFIX` | `string` | `/api` | API 前缀 | | `VITE_GLOB_FILE_URL` | `string` | - | 文件访问域名(拼接上传文件 URL) | | `VITE_GLOB_UPLOAD_URL` | `string` | - | 图片上传地址 | | `VITE_GLOB_IMG_URL` | `string` | - | 图片前缀地址 | | `VITE_GLOB_PROD_MOCK` | `boolean` | `false` | 生产环境是否开启 mock | | `VITE_USE_MOCK` | `boolean` | `false` | 开发环境是否开启 mock | | `VITE_DROP_CONSOLE` | `boolean` | `true` | 构建时删除 console | | `VITE_PROXY` | `JSON` | `[["/api","http://127.0.0.1:8041/api/v1"]]` | 跨域代理配置(JSON 数组) | | `VITE_BUILD_COMPRESS` | `string` | `'none'` | 压缩方式:`gzip` / `brotli` / `none` | | `VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE` | `boolean` | `false` | 压缩后是否删除原文件 | ### 代理配置格式 `VITE_PROXY` 是 JSON 格式的二维数组,每个元素 `[前缀, 目标地址]`: ```bash # 单个代理 VITE_PROXY=[["/api","http://127.0.0.1:8041/api/v1"]] # 多个代理(注意不要换行) VITE_PROXY=[["/api","http://127.0.0.1:8041/api/v1"],["/ws","ws://127.0.0.1:8041/ws"]] ``` 代理生成逻辑(`build/vite/proxy.ts`): ```typescript // 输入: [["/api", "http://127.0.0.1:8041/api/v1"]] // 输出: { '/api': { target: 'http://127.0.0.1:8041/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-vue` | Vue3 SFC 支持 | 始终 | | `@vitejs/plugin-vue-jsx` | JSX/TSX 支持 | 始终 | | `vite-plugin-vue-setup-extend` | ` ``` ### 运行时配置(app.config.js) 构建后自动生成 `dist/app.config.js`,包含 `VITE_GLOB_` 开头的环境变量: ```javascript // dist/app.config.js(构建后自动生成,不可手动修改) window.__PRODUCTION_TORNADO_ELEMENTPLUS___CONF__ = { "VITE_GLOB_API_URL": "", "VITE_GLOB_API_URL_PREFIX": "/api", "VITE_GLOB_FILE_URL": "http://file.tornado.elevue2", "VITE_GLOB_APP_TITLE": "Tornado+ElementPlus版", // ... }; Object.freeze(window.__PRODUCTION_TORNADO_ELEMENTPLUS___CONF__); ``` :::warning 为什么需要运行时配置? `import.meta.env` 的值在构建时被静态替换,构建后无法修改。而 `app.config.js` 是运行时加载的,部署时只需修改这个文件即可切换 API 地址等配置,无需重新构建。 ::: ### 打包体积分析 ```bash # 生成打包体积报告(自动打开浏览器) pnpm report ``` 生成 `node_modules/.cache/visualizer/stats.html`,可视化展示各模块占比。 --- ## 生产构建 ### 构建命令 | 命令 | mode | 说明 | |------|------|------| | `pnpm build` | `production` | 生产构建(默认) | | `pnpm build:prod` | `production` | 等效于 `pnpm build` | | `pnpm build:test` | `test` | 测试环境构建 | | `pnpm build:uat` | `uat` | UAT 环境构建 | | `pnpm build:dev` | `dev` | 开发联调环境构建 | | `pnpm build:no-cache` | `production` | 清除缓存后构建 | ### 构建流程 ``` 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.js` | vue + vue-router + pinia | | `vendor-element.js` | element-plus | | `vendor-echarts.js` | echarts(如果使用) | | `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/`): | 注册项 | 文件 | 说明 | |--------|------|------| | ElementPlus | `element.ts` | 全局引入 + 中文包 | | 自定义组件 | `customComponents.ts` | BasicTable、BasicForm、PageWrapper 等 | | 自定义指令 | `directives.ts` | `v-perm`、`v-perms`、`v-scrollBar` | **自定义指令**: | 指令 | 语法 | 说明 | |------|------|------| | `v-perm` | `v-perm="['sys:user:add']"` | 包含其中任一权限即显示 | | `v-perms` | `v-perms="['sys:user:add','sys:user:edit']"` | 包含所有权限才显示 | | `v-scrollBar` | `v-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:8041/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.yml` 和 `Dockerfile` 支持前后端一体部署,详见 [Docker 部署](/zh/volume6/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.development` 中 `VITE_PROXY` 格式是否正确 2. 检查后端是否运行在 `http://127.0.0.1:8041` 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 压缩。