基于Stage模型的鸿蒙5备忘录应用开发指南

一、项目概述

本文将基于HarmonyOS 5的Stage模型开发一个支持多窗口协同编辑的备忘录应用,借鉴《鸿蒙跨端U同步》中游戏多设备同步的技术原理,实现备忘录内容在多窗口间的实时同步编辑功能。该应用将展示Stage模型的核心特性,包括多窗口管理、跨窗口通信以及分布式数据同步。

二、技术架构

+---------------------+       +---------------------+
|  备忘录主窗口       |<----->|  协同编辑服务       |
| (MainAbilityStage)  |       | (CollabEditService) |
+----------+----------+       +----------+----------+
           |                             |
+----------v----------+       +----------v----------+
|  备忘录子窗口       |       |  分布式数据管理     |
| (SubAbilityStage)   |       | (DistributedData)   |
+----------+----------+       +----------+----------+
           |                             |
+----------v-----------------------------v----------+
|                Stage模型核心框架                  |
+---------------------------------------------------+

三、核心代码实现

1. 备忘录数据模型

// src/main/ets/model/MemoModel.ts
export class MemoItem {
  id: string;
  title: string;
  content: string;
  createTime: number;
  updateTime: number;
  author: string;
  collaborators: string[];

  constructor(title: string, content: string) {
    this.id = this.generateId();
    this.title = title;
    this.content = content;
    this.createTime = Date.now();
    this.updateTime = Date.now();
    this.author = AppStorage.get('currentUser') || 'anonymous';
    this.collaborators = [this.author];
  }

  private generateId(): string {
    return 'memo-' + Math.random().toString(36).substring(2, 9);
  }

  toJson(): string {
    return JSON.stringify({
      id: this.id,
      title: this.title,
      content: this.content,
      createTime: this.createTime,
      updateTime: this.updateTime,
      author: this.author,
      collaborators: this.collaborators
    });
  }

  static fromJson(jsonStr: string): MemoItem {
    const json = JSON.parse(jsonStr);
    const memo = new MemoItem(json.title, json.content);
    memo.id = json.id;
    memo.createTime = json.createTime;
    memo.updateTime = json.updateTime;
    memo.author = json.author;
    memo.collaborators = json.collaborators;
    return memo;
  }
}

2. 协同编辑服务

// src/main/ets/service/CollabEditService.ts
import { distributedData, DistributedData } from '@ohos.data.distributedData';
import { BusinessError } from '@ohos.base';
import { MemoItem } from '../model/MemoModel';

export class CollabEditService {
  private static instance: CollabEditService;
  private kvManager: distributedData.KVManager;
  private kvStore: distributedData.KVStore;
  private readonly STORE_ID = 'memo_store';
  private readonly SYNC_KEY = 'memo_sync';

  private constructor() {
    this.initDistributedData();
  }

  public static getInstance(): CollabEditService {
    if (!CollabEditService.instance) {
      CollabEditService.instance = new CollabEditService();
    }
    return CollabEditService.instance;
  }

  private initDistributedData(): void {
    const config: distributedData.KVManagerConfig = {
      bundleName: 'com.example.memo',
      userInfo: {
        userId: '0',
        userType: distributedData.UserType.SAME_USER_ID
      }
    };

    try {
      distributedData.createKVManager(config, (err: BusinessError, manager: distributedData.KVManager) => {
        if (err) {
          console.error(`Failed to create KVManager. Code: ${err.code}, message: ${err.message}`);
          return;
        }
        this.kvManager = manager;

        const options: distributedData.Options = {
          createIfMissing: true,
          encrypt: false,
          backup: false,
          autoSync: true,
          kvStoreType: distributedData.KVStoreType.SINGLE_VERSION,
          schema: '',
          securityLevel: distributedData.SecurityLevel.S1
        };

        this.kvManager.getKVStore(this.STORE_ID, options, (err: BusinessError, store: distributedData.KVStore) => {
          if (err) {
            console.error(`Failed to get KVStore. Code: ${err.code}, message: ${err.message}`);
            return;
          }
          this.kvStore = store;
          this.registerDataListener();
        });
      });
    } catch (e) {
      console.error(`An unexpected error occurred. Code: ${e.code}, message: ${e.message}`);
    }
  }

  private registerDataListener(): void {
    try {
      this.kvStore.on('dataChange', distributedData.SubscribeType.SUBSCRIBE_TYPE_ALL, (data: distributedData.ChangeData) => {
        if (data.key === this.SYNC_KEY) {
          const memo = MemoItem.fromJson(data.value.value as string);
          this.notifyMemoUpdate(memo);
        }
      });
    } catch (e) {
      console.error(`Failed to register data listener. Code: ${e.code}, message: ${e.message}`);
    }
  }

  private notifyMemoUpdate(memo: MemoItem): void {
    // 通知所有窗口更新备忘录
    WindowStageManager.getInstance().notifyAllStages(memo);
  }

  public syncMemo(memo: MemoItem): void {
    if (!this.kvStore) {
      console.error('KVStore is not initialized');
      return;
    }

    try {
      this.kvStore.put(this.SYNC_KEY, memo.toJson(), (err: BusinessError) => {
        if (err) {
          console.error(`Failed to put data. Code: ${err.code}, message: ${err.message}`);
        }
      });
    } catch (e) {
      console.error(`An unexpected error occurred. Code: ${e.code}, message: ${e.message}`);
    }
  }
}

3. 窗口阶段管理器

// src/main/ets/manager/WindowStageManager.ts
import { MemoItem } from '../model/MemoModel';
import { AbilityStage, Context } from '@ohos.app.ability.AbilityStage';
import { window } from '@ohos.window';

type StageCallback = (memo: MemoItem) => void;

export class WindowStageManager {
  private static instance: WindowStageManager;
  private stages: Map<string, StageCallback> = new Map();
  private context: Context;

  private constructor() {}

  public static getInstance(): WindowStageManager {
    if (!WindowStageManager.instance) {
      WindowStageManager.instance = new WindowStageManager();
    }
    return WindowStageManager.instance;
  }

  public setContext(context: Context): void {
    this.context = context;
  }

  public registerStage(stageId: string, callback: StageCallback): void {
    this.stages.set(stageId, callback);
  }

  public unregisterStage(stageId: string): void {
    this.stages.delete(stageId);
  }

  public notifyAllStages(memo: MemoItem): void {
    this.stages.forEach((callback, stageId) => {
      callback(memo);
    });
  }

  public async createSubWindow(memo: MemoItem): Promise<void> {
    try {
      const windowStage: AbilityStage = await this.context.createWindowStage('subWindow');
      const win = await windowStage.getMainWindow();
      
      // 设置子窗口属性
      await win.setWindowProperties({
        windowType: window.WindowType.TYPE_APP,
        width: 800,
        height: 1000,
        x: 100,
        y: 100,
        isFocusable: true,
        isTouchable: true
      });

      // 传递备忘录数据
      windowStage.loadContent('pages/SubWindowPage', (err, data) => {
        if (err) {
          console.error(`Failed to load content. Code: ${err.code}, message: ${err.message}`);
          return;
        }
        AppStorage.setOrCreate('currentMemo', memo);
      });

      await win.show();
    } catch (err) {
      console.error(`Failed to create sub window. Code: ${err.code}, message: ${err.message}`);
    }
  }
}

4. 主窗口AbilityStage实现

// src/main/ets/ability/MainAbilityStage.ts
import { AbilityStage, Context } from '@ohos.app.ability.AbilityStage';
import { WindowStageManager } from '../manager/WindowStageManager';
import { CollabEditService } from '../service/CollabEditService';

export default class MainAbilityStage extends AbilityStage {
  onCreate(): void {
    const windowStageManager = WindowStageManager.getInstance();
    windowStageManager.setContext(this.context);
    
    // 初始化协同编辑服务
    CollabEditService.getInstance();
    
    console.info('MainAbilityStage onCreate');
  }

  onDestroy(): void {
    console.info('MainAbilityStage onDestroy');
  }
}

5. 主窗口页面实现

// src/main/ets/pages/MainWindowPage.ets
import { MemoItem } from '../model/MemoModel';
import { WindowStageManager } from '../manager/WindowStageManager';
import { CollabEditService } from '../service/CollabEditService';

@Entry
@Component
struct MainWindowPage {
  @State memoList: MemoItem[] = [];
  @State currentMemo: MemoItem = new MemoItem('', '');
  @State showSubWindow: boolean = false;

  aboutToAppear(): void {
    // 注册窗口回调
    WindowStageManager.getInstance().registerStage('main', (memo: MemoItem) => {
      this.handleMemoUpdate(memo);
    });

    // 加载本地备忘录列表
    this.loadMemoList();
  }

  private loadMemoList(): void {
    // 模拟加载数据
    this.memoList = [
      new MemoItem('会议记录', '项目进度讨论...'),
      new MemoItem('购物清单', '牛奶、鸡蛋、面包...'),
      new MemoItem('读书笔记', '《设计模式》第一章...')
    ];
  }

  private handleMemoUpdate(memo: MemoItem): void {
    const index = this.memoList.findIndex(item => item.id === memo.id);
    if (index >= 0) {
      this.memoList[index] = memo;
    } else {
      this.memoList.push(memo);
    }
  }

  private createNewMemo(): void {
    this.currentMemo = new MemoItem('新备忘录', '');
    this.showSubWindow = true;
    WindowStageManager.getInstance().createSubWindow(this.currentMemo);
  }

  private editMemo(memo: MemoItem): void {
    this.currentMemo = memo;
    this.showSubWindow = true;
    WindowStageManager.getInstance().createSubWindow(memo);
  }

  build() {
    Column() {
      // 标题栏
      Row() {
        Text('协同备忘录')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
        
        Button('新建')
          .margin({left: 20})
          .onClick(() => this.createNewMemo())
      }
      .width('100%')
      .padding(10)
      .justifyContent(FlexAlign.Start)

      // 备忘录列表
      List({ space: 10 }) {
        ForEach(this.memoList, (item: MemoItem) => {
          ListItem() {
            Column() {
              Text(item.title)
                .fontSize(18)
                .fontWeight(FontWeight.Bold)
              
              Text(item.content.length > 30 ? item.content.substring(0, 30) + '...' : item.content)
                .fontSize(14)
                .margin({top: 5})
              
              Text(`最后更新: ${new Date(item.updateTime).toLocaleString()}`)
                .fontSize(12)
                .margin({top: 5})
            }
            .width('100%')
            .padding(10)
            .borderRadius(10)
            .backgroundColor('#f5f5f5')
          }
          .onClick(() => this.editMemo(item))
        })
      }
      .width('100%')
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .padding(10)
  }
}

6. 子窗口页面实现

// src/main/ets/pages/SubWindowPage.ets
import { MemoItem } from '../model/MemoModel';
import { WindowStageManager } from '../manager/WindowStageManager';
import { CollabEditService } from '../service/CollabEditService';

@Entry
@Component
struct SubWindowPage {
  @LocalStorageLink('currentMemo') memo: MemoItem = new MemoItem('', '');
  private timer: number = 0;
  private lastSyncTime: number = 0;

  aboutToAppear(): void {
    // 注册窗口回调
    WindowStageManager.getInstance().registerStage('sub_' + this.memo.id, (memo: MemoItem) => {
      if (memo.id === this.memo.id) {
        this.memo = memo;
      }
    });

    // 设置自动同步定时器
    this.timer = setInterval(() => {
      if (Date.now() - this.lastSyncTime > 1000) {
        this.syncMemo();
      }
    }, 1000);
  }

  onPageHide(): void {
    // 取消注册窗口回调
    WindowStageManager.getInstance().unregisterStage('sub_' + this.memo.id);
    
    // 清除定时器
    if (this.timer) {
      clearInterval(this.timer);
    }
    
    // 最后一次同步
    this.syncMemo();
  }

  private syncMemo(): void {
    this.memo.updateTime = Date.now();
    CollabEditService.getInstance().syncMemo(this.memo);
    this.lastSyncTime = Date.now();
  }

  build() {
    Column() {
      // 标题栏
      Row() {
        Text(this.memo.title || '无标题')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
        
        Button('关闭')
          .margin({left: 20})
          .onClick(() => {
            this.syncMemo();
            window.getLastWindow(this.context).then(win => win.hide());
          })
      }
      .width('100%')
      .padding(10)
      .justifyContent(FlexAlign.SpaceBetween)

      // 标题输入
      TextInput({ placeholder: '输入标题' })
        .width('100%')
        .height(50)
        .fontSize(18)
        .onChange((value: string) => {
          this.memo.title = value;
        })

      // 内容编辑区
      TextArea({ placeholder: '输入内容' })
        .width('100%')
        .height('100%')
        .fontSize(16)
        .onChange((value: string) => {
          this.memo.content = value;
        })
    }
    .width('100%')
    .height('100%')
    .padding(10)
  }
}

四、与游戏同步技术的结合点

  1. ​状态同步机制​​:借鉴游戏中玩家状态同步方式,实现备忘录内容的实时同步
  2. ​冲突解决策略​​:基于时间戳的最近更新策略,确保数据一致性
  3. ​分布式数据管理​​:利用HarmonyOS分布式能力实现跨设备数据同步
  4. ​窗口协同​​:类似游戏多视角展示,实现多窗口协同编辑
  5. ​性能优化​​:采用节流策略减少同步频率,平衡实时性和性能

五、Stage模型关键特性应用

  1. ​多窗口管理​​:通过WindowStageManager实现主窗口和子窗口的创建与通信
  2. ​生命周期管理​​:合理利用AbilityStage的生命周期方法管理资源
  3. ​上下文共享​​:通过AppStorage和LocalStorage实现窗口间数据共享
  4. ​窗口属性控制​​:精确控制窗口大小、位置和交互属性
  5. ​跨设备能力​​:基于分布式数据服务实现跨设备协同编辑

六、项目扩展方向

  1. ​富文本编辑​​:支持Markdown或富文本格式
  2. ​版本历史​​:实现备忘录的版本控制功能
  3. ​云同步​​:结合云服务实现多端数据同步
  4. ​权限管理​​:精细化控制协同编辑权限
  5. ​离线编辑​​:支持离线编辑和冲突解决

七、总结

本备忘录应用实现了以下核心功能:

  1. 基于Stage模型的多窗口协同编辑
  2. 备忘录内容的实时同步与冲突解决
  3. 直观的用户界面和流畅的交互体验
  4. 高效的分布式数据同步机制
  5. 完善的窗口生命周期管理

通过借鉴游戏中的同步技术,我们构建了一个高性能的跨窗口协同编辑应用。该项目不仅展示了HarmonyOS 5 Stage模型的核心能力,也为开发者提供了实现复杂协同应用的参考方案。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐