前端成功转鸿蒙开发者真实案例,教大家如何开发鸿蒙APP-- 卡片实践
好了,本文的实践就已经结束了,看大家的收获成果吧,可以在评论区展示出来。还没学习的小伙伴快点跟上吧!有啥不懂的都可以私聊我,有交流群。希望能帮助你们更好地理解和应用这些技术!加油!

大家好,我是陈杨,一名有着8 年前端开发经验、6 年技术写作积淀的鸿蒙开发者,也是鸿蒙生态里的一名极客。
曾因前端行业的危机感居安思危,果断放弃饱和的 iOS、安卓赛道,在鸿蒙 API9 发布时,凭着前端技术底子,三天吃透 ArkTS 框架,快速上手鸿蒙开发。三年深耕,我不仅打造了鸿蒙开源图表组件库「莓创图表」,闯进过创新赛、极客挑战赛总决赛,更带着团队实实在在做出了成果 —— 目前已成功上架11 款鸿蒙应用,涵盖工具、效率、创意等多个品类,包括JLPT、REFLEX PRO、国潮纸刻、Wss 直连、ZenithDocs Pro、圣诞相册、CSS 特效等,靠这些自研产品赚到了转型后的第一桶金。
从前端转型到鸿蒙掘金,靠的不是运气,而是选对赛道的眼光和快速落地的执行力。今天这篇文章,我们不讲技术,我们已经学习了几篇卡片的知识了,相信大家也知道卡片怎么开发了,所以这篇我们来实践一下。
我们实践的 Demo 将创建一个 “音乐播放器” 卡片,它集成了我们讨论过的所有核心事件:
**message**** 事件**:用于模拟 “喜欢” 歌曲,不跳转应用,直接刷新卡片状态。**router**** 事件**:用于 “打开应用播放列表”,跳转到应用内的具体页面。**call**** 事件**:用于控制 “播放” 和 “暂停”,在后台拉起一个 UIAbility 执行任务,并同步刷新卡片状态。
这份代码包含了所有必要的文件,并与我们之前讨论的刷新机制(第三篇)紧密联动,形成了完整的交互闭环。所以到底有没有认真学习,就看这一篇的成果了,加油。
项目结构
为了让您能快速上手,我们将创建一个包含以下关键文件的项目:
**MusicWidget.ets**: 卡片 UI 页面,包含所有交互按钮。**EntryFormAbility.ets**: 卡片的生命周期管理,处理message事件和卡片刷新。**EntryAbility.ets**: 应用的主 UIAbility,处理router事件的跳转。**MusicBackgroundAbility.ets**: 后台 UIAbility,处理call事件的播放 / 暂停逻辑。**module.json5**: 应用配置文件,注册所有 Ability 和权限。**form_config.json**: 卡片配置文件。
第 1 步:项目初始化
- 打开 DevEco Studio,创建一个新的Application项目,选择Stage 模型和API 10。
- 项目创建成功后,右键点击
entry模块,选择 New > Service Widget。 - 选择动态卡片,模板任选,语言选择ArkTS,卡片名称填写为
MusicWidget。
DevEco Studio 会自动生成EntryFormAbility.ets、MusicWidget.ets和form_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": ["2*2"],
"formConfigAbility": "",
"dataProxyEnabled": false,
"isDynamic": true // 确保是动态卡片
}
]
}
第 3 步:运行指南
- 环境准备:确保您的 DevEco Studio 版本支持 API 10,并且已经配置好了 HarmonyOS SDK 和模拟器 / 真实设备。
- 创建项目:按照 “项目初始化” 的步骤创建一个新的 Application 项目。
- 替换代码:将上面提供的代码片段,分别复制并替换到项目中对应的文件里。特别注意手动创建
MusicBackgroundAbility.ets文件。 - 配置 HAP 包:
- 打开
entry > src > main > module.json5。 - 在编辑器下方的
HAP标签页中,点击Add,新增一个HAP,例如命名为entry_feature。 - 将
MusicBackgroundAbility从entry模块移动到entry_feature模块中。这是因为包含后台运行权限的Ability需要放在一个独立的featureHAP 中。 - 确保
module.json5中requestPermissions部分也在entry_feature模块中。
- 编译运行:将项目编译并运行到您的 HarmonyOS 设备或模拟器上。
- 添加卡片:
- 在设备的桌面上,长按空白处,选择 “添加卡片”。
- 找到您的应用,选择刚刚创建的 “MusicWidget” 并添加到桌面。
- 测试事件:
- 点击卡片上的 “喜欢” 按钮,按钮文字和颜色会立即变化(
message事件)。 - 点击 “打开播放列表” 按钮,应用会被启动并进入主界面(
router事件)。 - 点击 “播放 / 暂停” 按钮,卡片上的状态文字会切换,同时后台会有日志输出(
call事件)。
总结
好了,本文的实践就已经结束了,看大家的收获成果吧,可以在评论区展示出来。还没学习的小伙伴快点跟上吧!有啥不懂的都可以私聊我,有交流群。希望能帮助你们更好地理解和应用这些技术!加油!
更多推荐




所有评论(0)