HarmonyOS鸿蒙Next前端成功转开发者真实案例,教大家如何开发鸿蒙APP-- 卡片实践

HarmonyOS鸿蒙Next前端成功转开发者真实案例,教大家如何开发鸿蒙APP-- 卡片实践 https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtybbs/626/617/028/0030086000626617028.20260112052150.71070760222717298715407119674796:50001231000000:2800:E76F047F8A120D426E99EE750A5C6DB9B0A37CCDFF1D4DF0BBAFE01C63145A39.png

大家好,我是陈杨,一名有着8 年前端开发经验、6 年技术写作积淀的鸿蒙开发者,也是鸿蒙生态里的一名极客。

曾因前端行业的危机感居安思危,果断放弃饱和的 iOS、安卓赛道,在鸿蒙 API9 发布时,凭着前端技术底子,三天吃透 ArkTS 框架,快速上手鸿蒙开发。三年深耕,我不仅打造了鸿蒙开源图表组件库「莓创图表」,闯进过创新赛、极客挑战赛总决赛,更带着团队实实在在做出了成果 —— 目前已成功上架11 款鸿蒙应用,涵盖工具、效率、创意等多个品类,包括JLPT、REFLEX PRO、国潮纸刻、Wss 直连、ZenithDocs Pro、圣诞相册、CSS 特效等,靠这些自研产品赚到了转型后的第一桶金。

从前端转型到鸿蒙掘金,靠的不是运气,而是选对赛道的眼光和快速落地的执行力。今天这篇文章,我们不讲技术,我们已经学习了几篇卡片的知识了,相信大家也知道卡片怎么开发了,所以这篇我们来实践一下。

我们实践的 Demo 将创建一个 “音乐播放器” 卡片,它集成了我们讨论过的所有核心事件:

  • message 事件:用于模拟 “喜欢” 歌曲,不跳转应用,直接刷新卡片状态。
  • router 事件:用于 “打开应用播放列表”,跳转到应用内的具体页面。
  • call 事件:用于控制 “播放” 和 “暂停”,在后台拉起一个 UIAbility 执行任务,并同步刷新卡片状态。

这份代码包含了所有必要的文件,并与我们之前讨论的刷新机制(第三篇)紧密联动,形成了完整的交互闭环。所以到底有没有认真学习,就看这一篇的成果了,加油。

项目结构

为了让您能快速上手,我们将创建一个包含以下关键文件的项目:

  1. MusicWidget.ets: 卡片 UI 页面,包含所有交互按钮。
  2. EntryFormAbility.ets: 卡片的生命周期管理,处理message事件和卡片刷新。
  3. EntryAbility.ets: 应用的主 UIAbility,处理router事件的跳转。
  4. MusicBackgroundAbility.ets: 后台 UIAbility,处理call事件的播放 / 暂停逻辑。
  5. module.json5: 应用配置文件,注册所有 Ability 和权限。
  6. form_config.json: 卡片配置文件。

第 1 步:项目初始化

  1. 打开 DevEco Studio,创建一个新的Application项目,选择Stage 模型API 10
  2. 项目创建成功后,右键点击entry模块,选择 New > Service Widget
  3. 选择动态卡片,模板任选,语言选择ArkTS,卡片名称填写为MusicWidget

DevEco Studio 会自动生成EntryFormAbility.etsMusicWidget.etsform_config.json。我们接下来要做的就是修改这些文件,并添加新的文件。


第 2 步:代码实现

1. 卡片 UI 页面 (src/main/ets/widget/pages/MusicWidget.ets)

这是用户直接看到的界面,包含了触发所有事件的按钮。

import { postCardAction } from '@kit.FormKit';

let storage = new LocalStorage();
@Entry(storage)
@Component
struct MusicWidget {
  // 从EntryFormAbility接收的数据
  @LocalStorageProp('formId') formId: string = '';
  @LocalStorageProp('songName') songName: string = '未播放';
  @LocalStorageProp('isLiked') isLiked: boolean = false;
  @LocalStorageProp('playStatus') playStatus: string = '暂停';

  build() {
    Column({ space: 15 })
      .width('100%')
      .height('100%')
      .padding(15)
      .justifyContent(FlexAlign.Center) {

      Text('音乐卡片')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 10 });

      Text(`🎵 ${this.songName}`)
        .fontSize(16)
        .fontWeight(FontWeight.Normal);

      Text(`▶️ 状态: ${this.playStatus}`)
        .fontSize(14)
        .fontColor(Color.Grey)
        .margin({ bottom: 20 });

      // --- 事件按钮区域 ---

      // 1. message事件:喜欢/取消喜欢
      Button(this.isLiked ? '❤️ 已喜欢' : '🤍 喜欢')
        .width('80%')
        .height(40)
        .backgroundColor(this.isLiked ? Color.Pink : Color.White)
        .fontColor(this.isLiked ? Color.White : Color.Black)
        .border({ width: 1, color: Color.Pink })
        .onClick(() => {
          this.triggerMessageEvent();
        });

      // 2. router事件:打开应用播放列表
      Button('📱 打开播放列表')
        .width('80%')
        .height(40)
        .backgroundColor(Color.Blue)
        .fontColor(Color.White)
        .onClick(() => {
          this.triggerRouterEvent();
        });

      // 3. call事件:播放/暂停
      Button(`${this.playStatus === '播放' ? '⏸️ 暂停' : '▶️ 播放'}`)
        .width('80%')
        .height(40)
        .backgroundColor(Color.Green)
        .fontColor(Color.White)
        .onClick(() => {
          this.triggerCallEvent(this.playStatus === '播放' ? 'pause' : 'play');
        });
    }
  }

  // 触发 message 事件
  private triggerMessageEvent(): void {
    postCardAction(this, {
      action: 'message',
      params: {
        formId: this.formId,
        command: 'toggleLike',
        isLiked: !this.isLiked
      }
    });
  }

  // 触发 router 事件
  private triggerRouterEvent(): void {
    postCardAction(this, {
      action: 'router',
      abilityName: 'EntryAbility', // 目标UIAbility名称
      params: {
        targetPage: 'PlayListPage',
        currentSong: this.songName
      }
    });
  }

  // 触发 call 事件
  private triggerCallEvent(method: string): void {
    postCardAction(this, {
      action: 'call',
      abilityName: 'MusicBackgroundAbility', // 后台UIAbility名称
      params: {
        formId: this.formId,
        method: method, // 要调用的后台方法名
        songId: 'song_001'
      }
    });
  }
}
2. 卡片生命周期管理 (src/main/ets/entryformability/EntryFormAbility.ets)

这个文件是卡片的 “大脑”,负责处理message事件并执行刷新。

import {
  formBindingData,
  FormExtensionAbility,
  formInfo,
  formProvider,
} from '@kit.FormKit';
import { Want, BusinessError } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG: string = 'EntryFormAbility';
const DOMAIN_NUMBER: number = 0xFF00;

export default class EntryFormAbility extends FormExtensionAbility {
  // 卡片创建时触发,提供初始数据
  onAddForm(want: Want): formBindingData.FormBindingData {
    hilog.info(DOMAIN_NUMBER, TAG, 'onAddForm');
    const formId = want.parameters?.[formInfo.FormParam.IDENTITY_KEY] as string;
    const initData = {
      formId: formId,
      songName: '七里香 - 周杰伦',
      isLiked: false,
      playStatus: '暂停'
    };
    return formBindingData.createFormBindingData(initData);
  }

  // 收到 message 事件时触发
  async onFormEvent(formId: string, message: string): Promise<void> {
    hilog.info(DOMAIN_NUMBER, TAG, `onFormEvent: ${message}`);
    const params = JSON.parse(message);

    if (params.command === 'toggleLike') {
      // 模拟更新喜欢状态并刷新卡片
      const newData = { isLiked: params.isLiked };
      try {
        await formProvider.updateForm(formId, formBindingData.createFormBindingData(newData));
        hilog.info(DOMAIN_NUMBER, TAG, `卡片刷新成功: ${formId}`);
      } catch (error) {
        const err = error as BusinessError;
        hilog.error(DOMAIN_NUMBER, TAG, `卡片刷新失败: ${err.message}`);
      }
    }
  }

  // 卡片删除时触发
  onRemoveForm(formId: string): void {
    hilog.info(DOMAIN_NUMBER, TAG, `onRemoveForm: ${formId}`);
  }
}
3. 应用主 UIAbility (src/main/ets/entryability/EntryAbility.ets)

处理router事件,负责跳转到应用内的页面。

import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG: string = 'EntryAbility';
const DOMAIN_NUMBER: number = 0xFF00;

export default class EntryAbility extends UIAbility {
  private currentWindowStage: window.WindowStage | null = null;

  // 首次启动或在后台被router事件唤醒时触发
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    hilog.info(DOMAIN_NUMBER, TAG, `onCreate: ${JSON.stringify(want.parameters)}`);
    // 处理router事件传递的参数
    if (want.parameters.params) {
      const params = JSON.parse(want.parameters.params as string);
      if (params.targetPage === 'PlayListPage') {
        hilog.info(DOMAIN_NUMBER, TAG, `准备跳转到播放列表页,当前歌曲: ${params.currentSong}`);
        // 在这里可以设置全局状态,让目标页面获取数据
      }
    }
  }

  // 应用已在前台,再次收到router事件时触发
  onNewWant(want: Want): void {
    hilog.info(DOMAIN_NUMBER, TAG, `onNewWant: ${JSON.stringify(want.parameters)}`);
    // 逻辑与onCreate类似
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    this.currentWindowStage = windowStage;
    // 加载主页面
    this.loadPage('pages/Index');
  }

  private loadPage(pageName: string): void {
    this.currentWindowStage?.loadContent(pageName, (err) => {
      if (err.code) {
        hilog.error(DOMAIN_NUMBER, TAG, `页面加载失败: ${err.message}`);
      }
    });
  }
}
4. 后台 UIAbility (src/main/ets/ability/MusicBackgroundAbility.ets)

注意: 这个文件需要您手动创建。在ets目录下新建一个ability文件夹,然后创建此文件。

它在后台运行,处理call事件。

import { UIAbility, Want, AbilityConstant } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { formBindingData, formProvider } from '@kit.FormKit';
import { rpc } from '@kit.IPCKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG: string = 'MusicBackgroundAbility';
const DOMAIN_NUMBER: number = 0xFF00;

// 用于RPC通信的数据序列化
class CallResult implements rpc.Parcelable {
  result: string;
  constructor(result: string) {
    this.result = result;
  }
  marshalling(messageSequence: rpc.MessageSequence): boolean {
    messageSequence.writeString(this.result);
    return true;
  }
  unmarshalling(messageSequence: rpc.MessageSequence): boolean {
    this.result = messageSequence.readString();
    return true;
  }
}

export default class MusicBackgroundAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    hilog.info(DOMAIN_NUMBER, TAG, 'onCreate');
    // 监听'play'和'pause'方法调用
    try {
      this.callee.on('play', this.handlePlay.bind(this));
      this.callee.on('pause', this.handlePause.bind(this));
    } catch (error) {
      const err = error as BusinessError;
      hilog.error(DOMAIN_NUMBER, TAG, `方法监听失败: ${err.message}`);
    }
  }

  // 处理播放逻辑
  private handlePlay(data: rpc.MessageSequence): CallResult {
    const params = JSON.parse(data.readString());
    const formId = params.formId;
    hilog.info(DOMAIN_NUMBER, TAG, `后台执行播放: ${params.songId}`);
    
    // 模拟播放并刷新卡片状态
    this.updateCardStatus(formId, '播放');
    return new CallResult('播放成功');
  }

  // 处理暂停逻辑
  private handlePause(data: rpc.MessageSequence): CallResult {
    const params = JSON.parse(data.readString());
    const formId = params.formId;
    hilog.info(DOMAIN_NUMBER, TAG, `后台执行暂停: ${params.songId}`);

    // 模拟暂停并刷新卡片状态
    this.updateCardStatus(formId, '暂停');
    return new CallResult('暂停成功');
  }

  // 调用刷新接口更新卡片
  private async updateCardStatus(formId: string, status: string): Promise<void> {
    try {
      await formProvider.updateForm(formId, formBindingData.createFormBindingData({ playStatus: status }));
      hilog.info(DOMAIN_NUMBER, TAG, `卡片状态刷新成功: ${status}`);
    } catch (error) {
      const err = error as BusinessError;
      hilog.error(DOMAIN_NUMBER, TAG, `卡片状态刷新失败: ${err.message}`);
    }
  }

  onDestroy(): void {
    hilog.info(DOMAIN_NUMBER, TAG, 'onDestroy');
    this.callee.off('play');
    this.callee.off('pause');
  }
}
5. 应用配置文件 (src/main/module.json5)

这是最重要的配置文件,必须正确注册所有组件和权限。

{
  "module": {
    "name": "entry",
    "type": "entry",
    "description": "$string:module_desc",
    "mainElement": "EntryAbility",
    "deviceTypes": ["phone", "tablet"],
    "deliveryWithInstall": true,
    "installationFree": false,
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:icon",
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:icon",
        "startWindowBackground": "$color:start_window_background"
      },
      {
        "name": "MusicBackgroundAbility", // 注册后台UIAbility
        "srcEntry": "./ets/ability/MusicBackgroundAbility.ets",
        "description": "处理卡片call事件的后台服务"
      }
    ],
    "extensionAbilities": [
      {
        "name": "EntryFormAbility",
        "srcEntry": "./ets/entryformability/EntryFormAbility.ets",
        "label": "$string:EntryFormAbility_label",
        "description": "$string:EntryFormAbility_desc",
        "type": "form",
        "metadata": [
          {
            "name": "ohos.extension.form",
            "resource": "$profile:form_config"
          }
        ]
      }
    ],
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
      },
      {
        // 为call事件申请后台运行权限
        "name": "ohos.permission.KEEP_BACKGROUND_RUNNING",
        "reason": "$string:keep_background_running_reason",
        "usedScene": {
          "abilities": ["MusicBackgroundAbility"],
          "when": "always"
        }
      }
    ]
  }
}

注意: 您需要在src/main/resources/base/element/string.json中添加keep_background_running_reason的定义。

{
  "string": [
    // ... 其他字符串
    {
      "name": "keep_background_running_reason",
      "value": "用于在后台处理卡片的播放/暂停指令"
    }
  ]
}
6. 卡片配置文件 (src/main/resources/base/profile/form_config.json)

确保卡片是动态的。

{
  "forms": [
    {
      "name": "MusicWidget",
      "description": "$string:MusicWidget_desc",
      "src": "./ets/widget/pages/MusicWidget.ets",
      "uiSyntax": "arkts",
      "window": {
        "designWidth": 720,
        "autoDesignWidth": true
      },
      "colorMode": "auto",
      "isDefault": true,
      "updateEnabled": true,
      "scheduledUpdateTime": "10:30",
      "updateDuration": 0,
      "defaultDimension": "2*2",
      "supportDimensions": ["

更多关于HarmonyOS鸿蒙Next前端成功转开发者真实案例,教大家如何开发鸿蒙APP-- 卡片实践的实战教程也可以访问 https://www.itying.com/category-93-b0.html

3 回复

更多关于HarmonyOS鸿蒙Next前端成功转开发者真实案例,教大家如何开发鸿蒙APP-- 卡片实践的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


鸿蒙Next卡片开发

鸿蒙Next卡片开发基于ArkTS语言,使用声明式UI范式。卡片通过FormExtensionAbility承载,需在module.json5中配置formConfig。

开发步骤:

  1. 创建FormAbility;
  2. 设计卡片布局(使用ArkUI组件);
  3. 实现数据更新逻辑;
  4. 配置卡片尺寸与动态更新策略。

卡片支持静态/动态数据绑定,可通过FormProvider实现跨应用交互。

感谢陈杨分享的宝贵实战经验。这个“音乐播放器”卡片Demo非常出色,清晰地展示了HarmonyOS Next中卡片开发的三大核心交互事件(message、router、call)的完整闭环实现,对开发者有很高的参考价值。

从技术实现来看,这个案例有几个关键点值得肯定:

  1. 架构清晰:严格遵循了Stage模型,将卡片UI(MusicWidget)、卡片生命周期管理(EntryFormAbility)、前台应用(EntryAbility)和后台服务(MusicBackgroundAbility)职责分离,符合鸿蒙应用的最佳实践。

  2. 事件机制运用准确

    • message事件:用于处理卡片自身状态的轻量级更新(如“喜欢”),通过formProvider.updateForm实现无跳转刷新,效率高。
    • router事件:用于导航到应用内具体页面(如播放列表),通过Want参数传递数据,是标准的页面跳转方式。
    • call事件:这是最体现鸿蒙分布式能力的一环。通过RPC(callee.on)调用后台Ability执行耗时或持续任务(如播放控制),并回调更新卡片状态,实现了前端交互与后台逻辑的解耦。
  3. 配置完整module.json5的配置是关键,尤其是正确声明MusicBackgroundAbility以及为其申请ohos.permission.KEEP_BACKGROUND_RUNNING权限,并注意将其部署在独立的Feature HAP中,这是保证后台Ability正常工作的必要条件。

对于想复现或学习的开发者,可以重点关注以下几点:

  • RPC序列化MusicBackgroundAbility中的CallResult类实现了rpc.Parcelable接口,这是跨Ability通信(call事件)时数据序列化的标准做法,务必掌握。
  • 状态管理:卡片UI通过@LocalStorageProp与EntryFormAbility中的数据进行绑定,这是卡片数据更新的推荐方式。
  • 动态卡片form_config.json"isDynamic": true的配置是卡片能够实时更新的前提。

这个案例成功地将理论知识点(卡片刷新、事件通信)串联成了一个可运行、可交互的完整应用,是前端开发者转型鸿蒙应用开发的一个优秀练手项目。遵循此架构,开发者可以扩展出更复杂的卡片功能,如实时天气、日程提醒等。

回到顶部