Files
riwa-ionic/VERSION_MANAGEMENT.md

12 KiB
Raw Permalink Blame History

版本管理方案

🎯 问题

前端每次更新版本,后端也需要手动同步版本信息,容易出错且效率低。

解决方案

方案一:自动生成 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 格式

{
  "version": "0.0.1",
  "buildTime": "2025-12-30T10:30:00.000Z",
  "gitCommit": "abc123",
  "environment": "production"
}

后端获取版本的方式

方式 1直接读取静态文件推荐

如果前端部署在 Nginx/CDN后端可以直接访问

curl https://your-domain.com/version.json

后端 API 实现示例:

// 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 到后端:

# 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

后端直接读取本地文件:

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 字段:

{
  "version": "1.2.3" // 只改这里
}

2. 构建项目

pnpm run build

构建完成后会在 dist/ 目录生成 version.json

dist/
├── index.html
├── assets/
└── version.json  ← 自动生成

3. 部署

将整个 dist/ 目录部署到服务器,version.json 会一起部署。


🔧 原生应用版本同步

iOS (Xcode)

修改 ios/App/App/Info.plist

<key>CFBundleShortVersionString</key>
<string>1.2.3</string>  <!-- 与 package.json 保持一致 -->

Android (build.gradle)

修改 android/app/build.gradle

versionName "1.2.3"  // 与 package.json 保持一致

自动化脚本(可选)

创建 scripts/sync-version.js

#!/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 中添加脚本:

{
  "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.10.0.2 - 修复 bug
  • 0.0.20.1.0 - 新增功能
  • 0.1.01.0.0 - 重大更新

🚀 CI/CD 集成

GitHub Actions 示例

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.jsonversion 字段
  2. 构建:运行 pnpm run build 自动生成 version.json
  3. 后端:通过 HTTP 请求或读取文件获取版本信息
  4. 原生应用:可选使用脚本自动同步版本号

一次配置,永久受益! 🎉

后端 API 实现示例 - 版本检查接口

// 方案二:从本地文件读取(适用于前后端部署在同一服务器)
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]
 * )
 */