鸿蒙 H5 动态加桌面卡片踩坑记录

完成时间:2026-04-21
场景:H5 活动页点击按钮 → 弹 Dialog → 加一张带 H5 图片+标题的卡片到桌面
项目:Grab 元服务(atomic service,API 12,ArkTS)

最终方案概览【实际】

架构

单 widget 条目 + LocalStorage 数据分支:

form_config.json(只保留 "widget" 一条)
    ↓
WidgetCard.ets(@Entry,共用一张卡)
    ├─ 无 H5 数据 → dimension1x2/2x2 原默认 Grab 卡
    └─ 有 H5 数据 → h5Dimension1x2/2x2(图 + logo + 标题)

H5 数据流:

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 会被系统过滤【实际】

现象ActivityAddCardDialogAddFormMenuItem({ parameters: { grab_card_title: 'xxx' } }),但 EntryFormAbility.onAddForm 里读 want.parameters 拿不到 grab_card_title,只有 ohos.extra.param.key.* 系统字段。

原因:鸿蒙 form extension 的 parameters IPC 有白名单过滤。

方案

  • H5 数据通过 AddFormMenuItemformBindingData 选项直接注入 LocalStorage
  • 同步用 callback 写 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 有生命周期【实际】

现象:加卡 25 秒后卡片图片变空白。

原因

  • 卡片进程 com.ohos.formrenderservice 是共享进程,约 25 秒空闲回收
  • formImages: {imgName: fd} 的 fd 进程重启后失效

方案

  • SPUtils 持久化 localImagePath
  • onUpdateForm 触发时,FormExtension 用 localImagePath 重新 openSync 拿新 fd → updateForm

4. SPUtils 跨进程读取缓存失效【实际】

现象:主进程写 SPUtils 后立即 flush,但 FormExtension(独立进程)读到旧值/空值。

原因:Preferences 在各进程有独立内存缓存,不会跨进程同步。

方案:FormExtension 读之前调 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})✅ 完全按坐标定位✅ 推荐

经验:复杂布局不要玩花的,用 Stack + 绝对 position 最稳。坐标从 onSizeChange 拿到的 cardHeight/cardWidth 按比例算。

6. form_config.json 没有"隐藏选择器"字段【实际】

需求:动态卡片没数据来源时不应该让用户从系统小组件选择器裸加。

尝试过

  • isDefault: false:只是不作默认尺寸,仍会显示在选择器
  • formProvider.deleteForm:provider 端不存在此 API(只有 host 能删)
  • 两个独立 widget 条目:widget_h5 照样显示在选择器

最终方案:合并回单 widget 条目,卡片组件里按 LocalStorage 是否有 H5 数据分支:

  • 选择器裸加 → 原默认 Grab 卡
  • H5 加卡 → H5 样式

7. 卡片底部的 app 名字标签是系统行为【实际】

现象:卡片下方会强制显示"Grab境外打车"字样。

原因:鸿蒙桌面系统自动在卡片下方展示 EntryAbility_labelentry/.../string.json 里的),不受 form_config 的 displayName 控制。

结论:无法干预,displayName 只在系统选择器里用。

8. @Entry 文件顶层 const 名不能重复【实际】

现象:两个 @Entry 文件同时声明 const OPEN = 'open',编译报 Cannot redeclare block-scoped variable 'OPEN'

原因:hvigor 把所有 @Entry 文件打进同一个作用域。

方案:不同 @Entry 文件用不同常量前缀(或合并成一个 @Entry)。

9. ImageFit 策略【实际】

模式效果适用
Cover保比例 + 裁剪图片比例接近卡片
Fill拉伸铺满比例不一致时首选(轻微变形换完整铺满)
Contain保比例 + 留白不能裁剪任何内容

H5 传的图片比例不可控 → 选 Fill

10. 原 WidgetCard 的 onSizeChange 守卫不能删【实际】

原代码:

if (this.cardHeight > 0 && this.cardWidth > 0) {
  // 渲染子元素
}

原因cardHeight - 24 这类表达式在 onSizeChange 触发前是负数,会导致布局异常。

方案:照抄原结构,onSizeChange + cardHeight/cardWidth 守卫保留。

布局最佳实践(卡片进程专用)

能用就用 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"

用途:HAP 更新后,已存在的卡片进程还在跑旧代码。force-stop 后系统会重启进程,加载新 HAP 代码。

抓日志(过滤 UTF-16 编码问题)

PowerShell 重定向写入的 hilog 是 UTF-16 编码,Python 读要指定:

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 测试页

标签: none

添加新评论