Files
riwa-ionic/VERSION_MANAGEMENT.md

476 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 版本管理方案
## 🎯 问题
前端每次更新版本,后端也需要手动同步版本信息,容易出错且效率低。
## ✅ 解决方案
### 方案一:自动生成 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&currentVersion=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]
* )
*/
```