527 lines
15 KiB
Markdown
527 lines
15 KiB
Markdown
# 前端构建
|
||
|
||
:::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` | `FastAPI+AntDesign版` | 应用标题(注入 HTML) |
|
||
| `VITE_GLOB_APP_SHORT_NAME` | `string` | `FastAPI+AntDesign版` | 应用短名(用于生成运行时配置变量名) |
|
||
| `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:8031/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: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-vue` | Vue3 SFC 支持 | 始终 |
|
||
| `@vitejs/plugin-vue-jsx` | JSX/TSX 支持 | 始终 |
|
||
| `vite-plugin-vue-setup-extend` | `<script setup>` 支持 `name` 属性 | 始终 |
|
||
| `unplugin-auto-import` | Vue/Router/Pinia API 自动导入 | 始终 |
|
||
| `vite-plugin-html` | HTML 模板注入(标题、运行时配置) | 始终 |
|
||
| `vite-plugin-mock` | Mock 数据服务 | `VITE_USE_MOCK=true` |
|
||
| `rollup-plugin-visualizer` | 打包体积分析 | `REPORT=true` |
|
||
| `vite-plugin-compression` | gzip/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+AntDesign版</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_ANTDESIGN___CONF__ = {
|
||
"VITE_GLOB_API_URL": "",
|
||
"VITE_GLOB_API_URL_PREFIX": "/api",
|
||
"VITE_GLOB_FILE_URL": "http://file.fastapi.antdvue",
|
||
"VITE_GLOB_APP_TITLE": "FastAPI+AntDesign版",
|
||
// ...
|
||
};
|
||
Object.freeze(window.__PRODUCTION_FASTAPI_ANTDESIGN___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、AntDesign Vue、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-antd.js` | ant-design-vue |
|
||
| `vendor-echarts.js` | echarts(如果使用) |
|
||
| `vendor-*.js` | 其他大型依赖 |
|
||
|
||
**3. 预构建依赖**
|
||
|
||
`optimizeDeps.include` 预构建以下依赖(开发时加速冷启动):
|
||
|
||
```typescript
|
||
optimizeDeps: {
|
||
include: ['dayjs', '@vicons/ionicons5', '@vicons/antd', '@ant-design/icons-vue'],
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 应用初始化
|
||
|
||
`src/main.ts` 是前端入口,初始化顺序:
|
||
|
||
```typescript
|
||
async function bootstrap() {
|
||
const app = createApp(App);
|
||
|
||
// 1. 全局注册 AntDesign Vue 图标
|
||
for (const [key, component] of Object.entries(AntdIconsVue)) {
|
||
app.component(key, component);
|
||
}
|
||
|
||
// 2. 全局引入 AntDesign Vue(中文包)
|
||
setupAntd(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/`):
|
||
|
||
| 注册项 | 文件 | 说明 |
|
||
|--------|------|------|
|
||
| AntDesign Vue | `antd.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: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.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: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. 按需引入 AntDesign Vue 组件(当前为全量引入)
|
||
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 压缩。
|