476 lines
12 KiB
Markdown
476 lines
12 KiB
Markdown
# 版本管理方案
|
||
|
||
## 🎯 问题
|
||
|
||
前端每次更新版本,后端也需要手动同步版本信息,容易出错且效率低。
|
||
|
||
## ✅ 解决方案
|
||
|
||
### 方案一:自动生成 version.json(已实现)⭐⭐⭐
|
||
|
||
前端打包时自动生成 `version.json` 文件,后端可以直接读取。
|
||
|
||
#### 工作流程
|
||
|
||
```
|
||
1. 开发者修改 package.json 中的 version
|
||
↓
|
||
2. 运行 pnpm run build
|
||
↓
|
||
3. Vite 自动读取 package.json 版本
|
||
↓
|
||
4. 构建完成后生成 dist/version.json
|
||
↓
|
||
5. 部署到服务器
|
||
↓
|
||
6. 后端通过 HTTP 请求获取前端版本
|
||
```
|
||
|
||
#### 生成的 version.json 格式
|
||
|
||
```json
|
||
{
|
||
"version": "0.0.1",
|
||
"buildTime": "2025-12-30T10:30:00.000Z",
|
||
"gitCommit": "abc123",
|
||
"environment": "production"
|
||
}
|
||
```
|
||
|
||
#### 后端获取版本的方式
|
||
|
||
**方式 1:直接读取静态文件(推荐)**
|
||
|
||
如果前端部署在 Nginx/CDN,后端可以直接访问:
|
||
|
||
```bash
|
||
curl https://your-domain.com/version.json
|
||
```
|
||
|
||
后端 API 实现示例:
|
||
|
||
```typescript
|
||
// Node.js / Elysia 示例
|
||
app.get("/api/app/version", async (ctx) => {
|
||
// 从前端静态资源读取版本
|
||
const response = await fetch("https://your-frontend-domain.com/version.json");
|
||
const frontendVersion = await response.json();
|
||
|
||
const { platform, currentVersion } = ctx.query;
|
||
|
||
return {
|
||
version: frontendVersion.version,
|
||
forceUpdate: compareVersion(currentVersion, frontendVersion.version) < 0,
|
||
updateMessage: "修复了一些问题",
|
||
updateUrl: platform === "ios"
|
||
? "https://apps.apple.com/app/xxx"
|
||
: "https://play.google.com/store/apps/details?id=xxx",
|
||
minSupportVersion: "0.0.1",
|
||
};
|
||
});
|
||
```
|
||
|
||
**方式 2:部署时同步到后端**
|
||
|
||
在 CI/CD 中,部署前端时自动同步 version.json 到后端:
|
||
|
||
```yaml
|
||
# GitHub Actions / GitLab CI 示例
|
||
- name: Deploy Frontend
|
||
run: |
|
||
pnpm run build
|
||
# 上传到前端服务器
|
||
rsync -avz dist/ user@frontend-server:/var/www/html/
|
||
# 同时将 version.json 复制到后端
|
||
scp dist/version.json user@backend-server:/app/frontend-version.json
|
||
```
|
||
|
||
后端直接读取本地文件:
|
||
|
||
```typescript
|
||
import fs from "node:fs";
|
||
|
||
app.get("/api/app/version", async (ctx) => {
|
||
const versionFile = fs.readFileSync("/app/frontend-version.json", "utf-8");
|
||
const { version } = JSON.parse(versionFile);
|
||
|
||
return {
|
||
version,
|
||
forceUpdate: false,
|
||
// ...
|
||
};
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## 📝 使用步骤
|
||
|
||
### 1. 修改版本号
|
||
|
||
只需修改 `package.json` 中的 `version` 字段:
|
||
|
||
```json
|
||
{
|
||
"version": "1.2.3" // 只改这里
|
||
}
|
||
```
|
||
|
||
### 2. 构建项目
|
||
|
||
```bash
|
||
pnpm run build
|
||
```
|
||
|
||
构建完成后会在 `dist/` 目录生成 `version.json`:
|
||
|
||
```
|
||
dist/
|
||
├── index.html
|
||
├── assets/
|
||
└── version.json ← 自动生成
|
||
```
|
||
|
||
### 3. 部署
|
||
|
||
将整个 `dist/` 目录部署到服务器,`version.json` 会一起部署。
|
||
|
||
---
|
||
|
||
## 🔧 原生应用版本同步
|
||
|
||
### iOS (Xcode)
|
||
|
||
修改 `ios/App/App/Info.plist`:
|
||
|
||
```xml
|
||
<key>CFBundleShortVersionString</key>
|
||
<string>1.2.3</string> <!-- 与 package.json 保持一致 -->
|
||
```
|
||
|
||
### Android (build.gradle)
|
||
|
||
修改 `android/app/build.gradle`:
|
||
|
||
```gradle
|
||
versionName "1.2.3" // 与 package.json 保持一致
|
||
```
|
||
|
||
### 自动化脚本(可选)
|
||
|
||
创建 `scripts/sync-version.js`:
|
||
|
||
```javascript
|
||
#!/usr/bin/env node
|
||
import { execSync } from "node:child_process";
|
||
import fs from "node:fs";
|
||
|
||
const packageJson = JSON.parse(fs.readFileSync("./package.json", "utf-8"));
|
||
const version = packageJson.version;
|
||
|
||
console.log(`Syncing version ${version} to native projects...`);
|
||
|
||
// iOS
|
||
const infoPlist = "./ios/App/App/Info.plist";
|
||
let plistContent = fs.readFileSync(infoPlist, "utf-8");
|
||
plistContent = plistContent.replace(
|
||
/<key>CFBundleShortVersionString<\/key>\s*<string>.*?<\/string>/,
|
||
`<key>CFBundleShortVersionString</key>\n\t<string>${version}</string>`
|
||
);
|
||
fs.writeFileSync(infoPlist, plistContent);
|
||
|
||
// Android (需要安装 gradle 解析库或手动更新)
|
||
console.log("Please manually update android/app/build.gradle versionName");
|
||
|
||
console.log("✓ Version synced!");
|
||
```
|
||
|
||
在 `package.json` 中添加脚本:
|
||
|
||
```json
|
||
{
|
||
"scripts": {
|
||
"version:sync": "node scripts/sync-version.js",
|
||
"build:ios": "npm run version:sync && ionic cap sync ios",
|
||
"build:android": "npm run version:sync && ionic cap sync android"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🎨 版本号规范
|
||
|
||
遵循**语义化版本 (Semantic Versioning)**:
|
||
|
||
```
|
||
主版本号.次版本号.修订号
|
||
↓ ↓ ↓
|
||
1 . 2 . 3
|
||
|
||
- 主版本号:不兼容的 API 修改
|
||
- 次版本号:向下兼容的功能新增
|
||
- 修订号:向下兼容的问题修正
|
||
```
|
||
|
||
### 版本更新示例
|
||
|
||
- `0.0.1` → `0.0.2` - 修复 bug
|
||
- `0.0.2` → `0.1.0` - 新增功能
|
||
- `0.1.0` → `1.0.0` - 重大更新
|
||
|
||
---
|
||
|
||
## 🚀 CI/CD 集成
|
||
|
||
### GitHub Actions 示例
|
||
|
||
```yaml
|
||
name: Build and Deploy
|
||
|
||
on:
|
||
push:
|
||
branches: [main]
|
||
|
||
jobs:
|
||
deploy:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v3
|
||
|
||
- name: Setup Node.js
|
||
uses: actions/setup-node@v3
|
||
with:
|
||
node-version: "18"
|
||
|
||
- name: Install dependencies
|
||
run: pnpm install
|
||
|
||
- name: Build
|
||
run: pnpm run build
|
||
env:
|
||
NODE_ENV: production
|
||
|
||
- name: Deploy to server
|
||
run: |
|
||
rsync -avz dist/ user@server:/var/www/html/
|
||
|
||
- name: Notify backend
|
||
run: |
|
||
curl -X POST https://your-backend.com/api/webhook/frontend-deployed \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"version": "'$(node -p "require('./package.json').version")'"}'
|
||
```
|
||
|
||
---
|
||
|
||
## 📊 方案对比
|
||
|
||
| 方案 | 优点 | 缺点 | 推荐度 |
|
||
|-----|------|------|--------|
|
||
| **自动生成 version.json** | 完全自动化、前后端解耦 | 需要后端额外请求 | ⭐⭐⭐⭐⭐ |
|
||
| 手动同步 | 简单直接 | 容易忘记、出错 | ⭐ |
|
||
| 共享配置文件 | 统一管理 | 需要 Monorepo | ⭐⭐⭐ |
|
||
| 环境变量 | 灵活 | 部署时需手动设置 | ⭐⭐ |
|
||
|
||
---
|
||
|
||
## ✅ 总结
|
||
|
||
1. **前端**:只需修改 `package.json` 的 `version` 字段
|
||
2. **构建**:运行 `pnpm run build` 自动生成 `version.json`
|
||
3. **后端**:通过 HTTP 请求或读取文件获取版本信息
|
||
4. **原生应用**:可选使用脚本自动同步版本号
|
||
|
||
**一次配置,永久受益!** 🎉
|
||
|
||
## 后端 API 实现示例 - 版本检查接口
|
||
|
||
```ts
|
||
// 方案二:从本地文件读取(适用于前后端部署在同一服务器)
|
||
import fs from "node:fs";
|
||
|
||
import { Elysia, t } from "elysia";
|
||
|
||
// 版本比较工具函数
|
||
function compareVersion(version1: string, version2: string): number {
|
||
const v1Parts = version1.split(".").map(Number);
|
||
const v2Parts = version2.split(".").map(Number);
|
||
const maxLength = Math.max(v1Parts.length, v2Parts.length);
|
||
|
||
for (let i = 0; i < maxLength; i++) {
|
||
const v1Part = v1Parts[i] || 0;
|
||
const v2Part = v2Parts[i] || 0;
|
||
|
||
if (v1Part < v2Part)
|
||
return -1;
|
||
if (v1Part > v2Part)
|
||
return 1;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
// 方案一:从前端静态资源读取版本(推荐)
|
||
async function getFrontendVersionFromURL(): Promise<{
|
||
version: string;
|
||
buildTime: string;
|
||
gitCommit: string;
|
||
environment: string;
|
||
}> {
|
||
const response = await fetch("https://your-frontend-domain.com/version.json");
|
||
return await response.json();
|
||
}
|
||
|
||
function getFrontendVersionFromFile(): {
|
||
version: string;
|
||
buildTime: string;
|
||
gitCommit: string;
|
||
environment: string;
|
||
} {
|
||
const content = fs.readFileSync("/app/frontend-version.json", "utf-8");
|
||
return JSON.parse(content);
|
||
}
|
||
|
||
// 应用商店链接配置
|
||
const APP_STORE_URLS = {
|
||
ios: "https://apps.apple.com/app/id123456789",
|
||
android: "https://play.google.com/store/apps/details?id=riwa.ionic.app",
|
||
};
|
||
|
||
// 版本策略配置(可存储在数据库)
|
||
const VERSION_POLICIES = {
|
||
minSupportVersion: "0.0.1", // 最低支持版本
|
||
forceUpdateVersions: ["0.0.1"], // 需要强制更新的版本列表
|
||
updateMessages: {
|
||
"zh-CN": "修复了一些问题并优化了性能",
|
||
"en-US": "Bug fixes and performance improvements",
|
||
},
|
||
};
|
||
|
||
// Elysia 路由定义
|
||
const app = new Elysia()
|
||
.get(
|
||
"/api/app/version",
|
||
async ({ query, headers }) => {
|
||
const { platform, currentVersion } = query;
|
||
const lang = headers["accept-language"]?.split(",")[0] || "en-US";
|
||
|
||
try {
|
||
// 获取前端版本信息
|
||
const frontendVersion = await getFrontendVersionFromURL();
|
||
// 或使用本地文件: const frontendVersion = getFrontendVersionFromFile()
|
||
|
||
// 判断是否需要更新
|
||
const hasUpdate = compareVersion(currentVersion, frontendVersion.version) < 0;
|
||
|
||
// 判断是否强制更新
|
||
let forceUpdate = VERSION_POLICIES.forceUpdateVersions.includes(currentVersion);
|
||
|
||
// 检查是否低于最低支持版本
|
||
if (compareVersion(currentVersion, VERSION_POLICIES.minSupportVersion) < 0) {
|
||
forceUpdate = true;
|
||
}
|
||
|
||
// 获取更新链接
|
||
const updateUrl = platform === "ios"
|
||
? APP_STORE_URLS.ios
|
||
: platform === "android"
|
||
? APP_STORE_URLS.android
|
||
: "";
|
||
|
||
// 获取更新说明(多语言)
|
||
const updateMessage = VERSION_POLICIES.updateMessages[lang]
|
||
|| VERSION_POLICIES.updateMessages["en-US"];
|
||
|
||
return {
|
||
version: frontendVersion.version,
|
||
buildNumber: Number.parseInt(frontendVersion.version.replace(/\./g, "")),
|
||
buildTime: frontendVersion.buildTime,
|
||
gitCommit: frontendVersion.gitCommit,
|
||
forceUpdate,
|
||
updateMessage,
|
||
updateUrl,
|
||
minSupportVersion: VERSION_POLICIES.minSupportVersion,
|
||
releaseNotes: [
|
||
"修复了已知问题",
|
||
"优化了应用性能",
|
||
"改进了用户界面",
|
||
],
|
||
};
|
||
}
|
||
catch (error) {
|
||
console.error("Failed to get frontend version:", error);
|
||
|
||
// 降级处理:返回当前版本,不强制更新
|
||
return {
|
||
version: currentVersion,
|
||
forceUpdate: false,
|
||
updateMessage: "",
|
||
updateUrl: "",
|
||
};
|
||
}
|
||
},
|
||
{
|
||
query: t.Object({
|
||
platform: t.Union([t.Literal("ios"), t.Literal("android"), t.Literal("web")]),
|
||
currentVersion: t.String(),
|
||
}),
|
||
response: t.Object({
|
||
version: t.String(),
|
||
buildNumber: t.Optional(t.Number()),
|
||
buildTime: t.Optional(t.String()),
|
||
gitCommit: t.Optional(t.String()),
|
||
forceUpdate: t.Boolean(),
|
||
updateMessage: t.Optional(t.String()),
|
||
updateUrl: t.Optional(t.String()),
|
||
minSupportVersion: t.Optional(t.String()),
|
||
releaseNotes: t.Optional(t.Array(t.String())),
|
||
}),
|
||
}
|
||
);
|
||
|
||
export default app;
|
||
|
||
/**
|
||
* 使用示例:
|
||
*
|
||
* 1. 启动后端服务
|
||
* 2. 前端请求:GET /api/app/version?platform=ios¤tVersion=0.0.1
|
||
* 3. 后端响应:
|
||
* {
|
||
* "version": "0.0.2",
|
||
* "buildTime": "2025-12-30T11:42:43.425Z",
|
||
* "forceUpdate": false,
|
||
* "updateMessage": "修复了一些问题并优化了性能",
|
||
* "updateUrl": "https://apps.apple.com/app/id123456789",
|
||
* "minSupportVersion": "0.0.1"
|
||
* }
|
||
*/
|
||
|
||
/**
|
||
* 数据库存储方案(可选)
|
||
*
|
||
* 如果需要更灵活的版本策略管理,可以将配置存储在数据库:
|
||
*
|
||
* CREATE TABLE app_versions (
|
||
* id SERIAL PRIMARY KEY,
|
||
* platform VARCHAR(20) NOT NULL,
|
||
* version VARCHAR(20) NOT NULL,
|
||
* min_support_version VARCHAR(20),
|
||
* force_update BOOLEAN DEFAULT FALSE,
|
||
* update_message_zh TEXT,
|
||
* update_message_en TEXT,
|
||
* update_url TEXT,
|
||
* release_notes JSONB,
|
||
* created_at TIMESTAMP DEFAULT NOW()
|
||
* );
|
||
*
|
||
* 然后从数据库查询版本策略:
|
||
* const policy = await db.query(
|
||
* 'SELECT * FROM app_versions WHERE platform = $1 ORDER BY created_at DESC LIMIT 1',
|
||
* [platform]
|
||
* )
|
||
*/
|
||
```
|