鸿蒙开发-版本管理

2 阅读4分钟

歌词改了好多版怎么管理?HarmonyOS版本记录与对比

如果你对歌词创作感兴趣,可以去鸿蒙应用市场搜一下**「词藻本」**,下载下来体验体验。写歌词时反复修改,App自动保存每个版本,可以回看历史版本和对比差异。体验完了再回来看这篇文章,你会更清楚版本管理功能是怎么实现的。


写在前面

大家好,我是一名写了十多年Web前端的老兵。从jQuery时代一路走到React/Vue,Git版本管理是每天都在用的工具。去年开始转战鸿蒙生态,用ArkTS开发App,发现App内的文档版本管理需要自己实现。

比如:

  • 版本存储:Git用.git目录存储;App里用preferences存储多个版本。
  • 版本对比:Git用diff命令;App里需要自己实现文本差异对比。
  • 版本回滚:Git用checkout;App里用状态恢复。

别担心,接下来这篇文章,我会用"词藻本"的歌词版本管理,带你看看HarmonyOS怎么实现文档版本控制。


这篇文章聊什么

词藻本的版本管理功能,核心要解决:

  1. 版本保存:每次修改自动保存版本
  2. 版本列表:展示所有历史版本
  3. 版本对比:对比两个版本的差异
  4. 版本回滚:恢复到某个历史版本

第一步:设计版本数据结构

// Web前端同学看这里:React里我们用state管理当前内容
// 鸿蒙里也需要设计版本数据结构来存储历史

// 歌词版本
interface LyricVersion {
  id: string;
  lyricId: string;
  content: string;
  version: number;
  changeDescription: string;
  createdAt: string;
}

// 歌词草稿(含版本信息)
interface LyricDraft {
  id: string;
  title: string;
  content: string;
  currentVersion: number;
  versions: LyricVersion[];
  rhymeGroup: string;
  mood: string;
  createdAt: string;
  updatedAt: string;
}

第二步:实现版本管理逻辑

// Web前端同学看这里:版本管理的核心是"快照"思想
// 每次保存时,把当前内容存为一个新版本

import { preferences } from '@kit.ArkData';

let prefInstance: preferences.Preferences | null = null;

async function getPreferences(context: Context): Promise<preferences.Preferences> {
  if (!prefInstance) {
    prefInstance = await preferences.getPreferences(context, 'cizaoben_data');
  }
  return prefInstance;
}

// 保存新版本
async function saveVersion(context: Context, lyric: LyricDraft, description: string): Promise<boolean> {
  try {
    const pref = await getPreferences(context);

    // 创建版本快照
    const newVersion: LyricVersion = {
      id: `ver_${Date.now()}`,
      lyricId: lyric.id,
      content: lyric.content,
      version: lyric.currentVersion + 1,
      changeDescription: description,
      createdAt: new Date().toISOString()
    };

    // 更新歌词的版本列表
    lyric.versions.push(newVersion);
    lyric.currentVersion = newVersion.version;
    lyric.updatedAt = new Date().toISOString().slice(0, 10);

    // 存储到preferences
    const drafts = await getItem<LyricDraft[]>(context, 'drafts', []);
    const index = drafts.findIndex(d => d.id === lyric.id);
    if (index !== -1) {
      drafts[index] = lyric;
    }
    await pref.put('drafts', JSON.stringify(drafts));
    await pref.flush();

    return true;
  } catch (err) {
    console.error(`保存版本失败: ${err}`);
    return false;
  }
}

// 获取版本历史
async function getVersionHistory(context: Context, lyricId: string): Promise<LyricVersion[]> {
  const drafts = await getItem<LyricDraft[]>(context, 'drafts', []);
  const lyric = drafts.find(d => d.id === lyricId);
  return lyric?.versions || [];
}

// 回滚到指定版本
async function rollbackToVersion(context: Context, lyricId: string, versionId: string): Promise<boolean> {
  try {
    const drafts = await getItem<LyricDraft[]>(context, 'drafts', []);
    const lyric = drafts.find(d => d.id === lyricId);
    if (!lyric) return false;

    const targetVersion = lyric.versions.find(v => v.id === versionId);
    if (!targetVersion) return false;

    // 恢复内容
    lyric.content = targetVersion.content;
    lyric.updatedAt = new Date().toISOString().slice(0, 10);

    // 保存当前状态为新版本
    await saveVersion(context, lyric, `回滚到版本 ${targetVersion.version}`);

    return true;
  } catch (err) {
    console.error(`回滚失败: ${err}`);
    return false;
  }
}

// 通用存储方法
async function getItem<T>(context: Context, key: string, defaultValue: T): Promise<T> {
  try {
    const pref = await getPreferences(context);
    const value = await pref.get(key, '');
    if (typeof value === 'string' && value.length > 0) {
      return JSON.parse(value) as T;
    }
    return defaultValue;
  } catch (err) {
    return defaultValue;
  }
}

第三步:实现版本对比

// Web前端同学看这里:文本diff算法
// 简单实现:按行对比,标记新增和删除

interface DiffLine {
  type: 'same' | 'added' | 'removed';
  content: string;
}

function diffText(oldText: string, newText: string): DiffLine[] {
  const oldLines = oldText.split('\n');
  const newLines = newText.split('\n');
  const result: DiffLine[] = [];

  const maxLen = Math.max(oldLines.length, newLines.length);

  for (let i = 0; i < maxLen; i++) {
    const oldLine = i < oldLines.length ? oldLines[i] : undefined;
    const newLine = i < newLines.length ? newLines[i] : undefined;

    if (oldLine === undefined) {
      result.push({ type: 'added', content: newLine! });
    } else if (newLine === undefined) {
      result.push({ type: 'removed', content: oldLine });
    } else if (oldLine === newLine) {
      result.push({ type: 'same', content: oldLine });
    } else {
      result.push({ type: 'removed', content: oldLine });
      result.push({ type: 'added', content: newLine });
    }
  }

  return result;
}

// 版本对比页面
@Entry
@Component
struct VersionComparePage {
  @State diffResult: DiffLine[] = []
  @State oldVersion: LyricVersion | null = null
  @State newVersion: LyricVersion | null = null

  compareVersions(oldVer: LyricVersion, newVer: LyricVersion) {
    this.oldVersion = oldVer;
    this.newVersion = newVer;
    this.diffResult = diffText(oldVer.content, newVer.content);
  }

  build() {
    Column() {
      Text('版本对比')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 16 })

      if (this.oldVersion && this.newVersion) {
        Row() {
          Text(`版本 ${this.oldVersion.version}`)
            .fontSize(14)
            .fontColor('#6b7280')
          Text(' → ')
            .fontSize(14)
          Text(`版本 ${this.newVersion.version}`)
            .fontSize(14)
            .fontColor('#6b7280')
        }
        .margin({ bottom: 12 })

        // 差异显示
        ForEach(this.diffResult, (line: DiffLine, index: number) => {
          Text(line.content)
            .fontSize(14)
            .fontFamily('monospace')
            .fontColor(this.getLineFontColor(line.type))
            .backgroundColor(this.getLineBgColor(line.type))
            .width('100%')
            .padding({ left: 8, right: 8, top: 4, bottom: 4 })
        })
      }
    }
    .padding(16)
  }

  private getLineFontColor(type: string): string {
    if (type === 'added') return '#166534';
    if (type === 'removed') return '#991b1b';
    return '#374151';
  }

  private getLineBgColor(type: string): string {
    if (type === 'added') return '#dcfce7';
    if (type === 'removed') return '#fee2e2';
    return 'transparent';
  }
}

第四步:实现版本列表页面

@Entry
@Component
struct VersionListPage {
  @State versions: LyricVersion[] = []
  @State lyricId: string = ''

  async aboutToAppear() {
    this.versions = await getVersionHistory(getContext(this) as Context, this.lyricId);
    // 按版本号倒序
    this.versions.sort((a, b) => b.version - a.version);
  }

  build() {
    Column() {
      Text('版本历史')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 16 })

      ForEach(this.versions, (version: LyricVersion) => {
        Row() {
          Column() {
            Text(`版本 ${version.version}`)
              .fontSize(16)
              .fontWeight(FontWeight.Medium)

            Text(version.changeDescription)
              .fontSize(12)
              .fontColor('#6b7280')

            Text(version.createdAt)
              .fontSize(10)
              .fontColor('#9ca3af')
              .margin({ top: 4 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)

          Button('恢复')
            .fontSize(12)
            .height(32)
            .backgroundColor('#3b82f6')
            .borderRadius(8)
            .onClick(() => {
              rollbackToVersion(getContext(this) as Context, this.lyricId, version.id);
            })
        }
        .width('100%')
        .padding(12)
        .backgroundColor('#ffffff')
        .borderRadius(12)
        .margin({ bottom: 8 })
      })
    }
    .padding(16)
  }
}

第五步:常见问题

5.1 版本数量过多

问题:长期使用后版本数量很大。

解决:设置最大版本数(如50个),超出时删除最早的版本。

5.2 存储空间

问题:每个版本都存储完整内容,占用空间大。

解决:可以只存储差异(delta),但实现复杂度高;简单方案是限制版本数量。


总结

这篇文章围绕"词藻本"的歌词版本管理,讲解了:

版本存储

  • 版本快照的创建和存储
  • preferences数据持久化
  • 版本列表管理

版本对比

  • 文本差异对比算法
  • 差异可视化展示

版本回滚

  • 恢复历史版本内容
  • 回滚操作的版本记录

如果你对"词藻本"感兴趣,欢迎去鸿蒙应用市场搜索下载体验。