鸿蒙H5动态加桌面卡片踩坑记录
单 widget 条目 + LocalStorage 数据分支: H5 数据流: 现象: 原因:鸿蒙 form extension 的 parameters IPC 有白名单过滤。 方案: 现象:用 方案:全部改成下划线 现象:加卡 25 秒后卡片图片变空白。 原因: 方案: 现象:主进程写 SPUtils 后立即 flush,但 FormExtension(独立进程)读到旧值/空值。 原因:Preferences 在各进程有独立内存缓存,不会跨进程同步。 方案:FormExtension 读之前调 卡片进程( 经验:复杂布局不要玩花的,用 Stack + 绝对 position 最稳。坐标从 需求:动态卡片没数据来源时不应该让用户从系统小组件选择器裸加。 尝试过: 最终方案:合并回单 widget 条目,卡片组件里按 LocalStorage 是否有 H5 数据分支: 现象:卡片下方会强制显示"Grab境外打车"字样。 原因:鸿蒙桌面系统自动在卡片下方展示 结论:无法干预, 现象:两个 原因:hvigor 把所有 @Entry 文件打进同一个作用域。 方案:不同 @Entry 文件用不同常量前缀(或合并成一个 @Entry)。 H5 传的图片比例不可控 → 选 原代码: 原因: 方案:照抄原结构,onSizeChange + cardHeight/cardWidth 守卫保留。 用途:HAP 更新后,已存在的卡片进程还在跑旧代码。force-stop 后系统会重启进程,加载新 HAP 代码。 PowerShell 重定向写入的 hilog 是 UTF-16 编码,Python 读要指定:鸿蒙 H5 动态加桌面卡片踩坑记录
完成时间:2026-04-21
场景:H5 活动页点击按钮 → 弹 Dialog → 加一张带 H5 图片+标题的卡片到桌面
项目:Grab 元服务(atomic service,API 12,ArkTS)最终方案概览【实际】
架构
form_config.json(只保留 "widget" 一条)
↓
WidgetCard.ets(@Entry,共用一张卡)
├─ 无 H5 数据 → dimension1x2/2x2 原默认 Grab 卡
└─ 有 H5 数据 → h5Dimension1x2/2x2(图 + logo + 标题)H5 页面
↓ JsBridge.addDesktopCard
ActivityWebPage.handleAddDesktopCard
↓ 下载图到沙盒 /data/.../cache/image_cache/xxx
ActivityAddCardDialog
↓ AddFormMenuItem + formBindingData(fd via formImages)
↓ callback 回调内联写 SPUtils(跨进程持久化)
系统创建 form
↓
EntryFormAbility.onAddForm
↓ WidgetH5Biz.safeRefresh(读 SPUtils → 重新 openSync fd → updateForm)
WidgetCard 卡片进程(formrenderservice)
└─ 读 LocalStorage(grab_card_title / imgName 等)→ H5 样式渲染关键文件清单
文件 作用 entry/src/main/resources/base/profile/form_config.json只保留一个 widget 条目 entry/src/main/ets/widget/pages/WidgetCard.ets共用卡片组件,内部按 LocalStorage 是否有 H5 数据分支 entry/src/main/ets/entryformability/EntryFormAbility.ets统一入口,onAddForm/onUpdateForm 都尝试 H5 刷新 entry/src/main/ets/module/form/biz/WidgetH5Biz.etsSPUtils 配置管理 + fd 重新注入 features/home/src/main/ets/dialogs/ActivityAddCardDialog.etsH5 加卡弹窗 + AddFormMenuItem features/home/src/main/ets/pages/ActivityWebPage.etsH5 页面 + 图片下载 核心陷阱(踩过的坑)
1. AddFormMenuItem.parameters 的自定义 key 会被系统过滤【实际】
ActivityAddCardDialog 给 AddFormMenuItem({ parameters: { grab_card_title: 'xxx' } }),但 EntryFormAbility.onAddForm 里读 want.parameters 拿不到 grab_card_title,只有 ohos.extra.param.key.* 系统字段。AddFormMenuItem 的 formBindingData 选项直接注入 LocalStoragecallback 写 SPUtils 给 FormExtension 跨进程读取2. @LocalStorageProp key 只能用字母/数字/下划线【实际】
grab.card.title 作 key,编译期报 Cannot use the key! The value of key can only consist of letters, digits and underscores。grab_card_title。3. memory:// 的 fd 有生命周期【实际】
com.ohos.formrenderservice 是共享进程,约 25 秒空闲回收formImages: {imgName: fd} 的 fd 进程重启后失效localImagePathonUpdateForm 触发时,FormExtension 用 localImagePath 重新 openSync 拿新 fd → updateForm4. SPUtils 跨进程读取缓存失效【实际】
SPUtils.removePreferencesFromCacheSync() 强制从磁盘重读。5. 卡片渲染进程组件支持有限【实际】
com.ohos.formrenderservice)跑的是裁剪版 ArkTS 运行时,以下踩过坑:组件/用法 表现 结论 Blank().layoutWeight(1)整个渲染进程崩溃 → 所有卡片变白 ❌ 禁用 Flex({ justifyContent: FlexAlign.End })同上,进程崩溃 ❌ 禁用 Stack.alignContent(Alignment.Bottom)不按预期放置子元素 ⚠️ 不稳定 Column + layoutWeight(1)可以渲染但尺寸计算有偏差 ⚠️ 谨慎 Stack() + child.position({x, y})✅ 完全按坐标定位 ✅ 推荐 onSizeChange 拿到的 cardHeight/cardWidth 按比例算。6. form_config.json 没有"隐藏选择器"字段【实际】
isDefault: false:只是不作默认尺寸,仍会显示在选择器formProvider.deleteForm:provider 端不存在此 API(只有 host 能删)7. 卡片底部的 app 名字标签是系统行为【实际】
EntryAbility_label(entry/.../string.json 里的),不受 form_config 的 displayName 控制。displayName 只在系统选择器里用。8. @Entry 文件顶层 const 名不能重复【实际】
@Entry 文件同时声明 const OPEN = 'open',编译报 Cannot redeclare block-scoped variable 'OPEN'。9. ImageFit 策略【实际】
模式 效果 适用 Cover保比例 + 裁剪 图片比例接近卡片 Fill拉伸铺满 比例不一致时首选(轻微变形换完整铺满) Contain保比例 + 留白 不能裁剪任何内容 Fill。10. 原 WidgetCard 的 onSizeChange 守卫不能删【实际】
if (this.cardHeight > 0 && this.cardWidth > 0) {
// 渲染子元素
}cardHeight - 24 这类表达式在 onSizeChange 触发前是负数,会导致布局异常。布局最佳实践(卡片进程专用)
能用就用 Stack + position
Stack() {
// 背景图(最底层)
Image(src)
.width('100%').height('100%')
.objectFit(ImageFit.Fill)
// 左上角 logo(绝对定位)
Image($r('app.media.logo'))
.width(14).height(14)
.position({ x: 10, y: 6 })
// 中下方文字块(按 cardHeight 比例放)
Column() {
Text(title)
Text(subtitle)
}
.width('100%')
.padding({ left: 10, right: 10 })
.alignItems(HorizontalAlign.Start)
.position({ x: 0, y: this.cardHeight * 0.4 })
}
.width('100%').height('100%')
.borderRadius(12)
.clip(true)cardHeight 从 onSizeChange 拿
@State cardHeight: number = 0
@State cardWidth: number = 0
build() {
FormLink(...) {
Row() {
if (this.cardHeight > 0 && this.cardWidth > 0) {
// 用 cardHeight/cardWidth 做尺寸计算
}
}
.width('100%').height('100%')
}
.onSizeChange((o, n) => {
this.cardHeight = n.height as number
this.cardWidth = n.width as number
})
}调试命令速查
构建 + 安装
cd "D:/Documents/Codes/ai/Project/Grab/GrabMetaServices"
export DEVECO_SDK_HOME="D:/Documents/Codes/ai/Sdk/openHarmony"
export HOS_SDK_HOME="D:/Documents/Codes/ai/Sdk/openHarmony/sdk/default"
export PATH="D:/Documents/Codes/ai/Sdk/openHarmony/tools/node:D:/Documents/Codes/ai/Sdk/openHarmony/tools/hvigor/bin:D:/Documents/Codes/ai/Sdk/openHarmony/tools/ohpm/bin:D:/Documents/Codes/ai/Sdk/openHarmony/sdk/default/openharmony/toolchains:$PATH"
# 构建
node "D:/Documents/Codes/ai/Sdk/openHarmony/tools/hvigor/bin/hvigorw.js" assembleHap --mode module -p product=default
# 安装(必须 cd 到项目根目录,用相对路径)
hdc install -r "entry/build/default/outputs/default/entry-default-signed.hap"强制卡片进程重启(加载新代码)
hdc shell "aa force-stop com.ohos.formrenderservice"抓日志(过滤 UTF-16 编码问题)
with open('D:/log_v19.txt', 'rb') as f:
data = f.read()
try:
text = data.decode('utf-16')
except:
text = data.decode('utf-8', errors='ignore')LogUtil 坑【实际】
LogUtil.i 在 default build 被 Logger.isPrint=false 过滤,筛不到日志LogUtil.infoForce(绕过 isPrint)console.log 输出到 com.ohos.formrenderservice tag,不在 app bundleName 下参考
HANDOVER_h5addDeskCard_20260420.md — 前期调试记录(会话间交接)features/home/src/main/resources/rawfile/bridge_jsbridge_test.html — H5 测试页