适合谁看
想写可维护鸿蒙卡片数据层的人
正在做每日推荐、排行榜、轮播类鸿蒙卡片的人
不想把数据硬写进鸿蒙 Ability 文件的人
问题背景
鸿蒙卡片代码很容易越写越乱的一个原因是:
Ability 管生命周期
Ability 还管数据
Ability 还管兜底
Ability 还管资源校验
最后所有逻辑都挤在同一个文件里,改一个数据要动 Ability,改一个兜底要动 Ability,改一个资源名也要动 Ability。
项目中的真实场景
食界探味当前把鸿蒙卡片数据层单独放在:
app/ohos/entry/src/main/ets/formability/RecommendData.ets
对应的 Ability 只负责消费它:
// DailyRecommendFormAbility.ets import { getRecommendOfToday, resolveImageResName } from './RecommendData'; onAddForm(want: Want): formBindingData.FormBindingData { const item = getRecommendOfToday(); return formBindingData.createFormBindingData({ dishName: item.name, dishImage: resolveImageResName(item.imageResName), // ... }); }Ability 不关心数据从哪来、怎么选、怎么校验,只关心"拿数据 → 绑定到卡片"。
核心实现
一、RecommendData.ets 的完整结构
// 1. 数据结构定义 export interface RecommendItem { id: string; // 菜品 ID(用于点击跳转) name: string; // 菜名 region: string; // 地区 imageResName: string; // 鸿蒙图片资源名 highlight: string; // 口味亮点 summary: string; // 一句话介绍 } // 2. 推荐列表(10 道菜品) const RECOMMEND_LIST: RecommendItem[] = [ { id: 'beef-curry', name: '牛肉咖喱', region: '印度 · 亚洲', imageResName: 'dish_beef_curry', highlight: '浓郁香料', summary: '椰香与香料层层叠起,入口热烈又厚实。' }, { id: 'sukiyaki', name: '寿喜锅', region: '日本 · 亚洲', imageResName: 'dish_sukiyaki', highlight: '鲜甜酱香', summary: '牛肉、蔬菜与寿喜烧汁一起慢慢煮到刚好。' }, // ... 共 10 道菜品 ]; // 3. 兜底数据 const FALLBACK_ITEM: RecommendItem = { id: 'fallback', name: '环球美食', region: '世界', imageResName: 'dish_fallback', highlight: '今天吃什么', summary: '打开食界探味,挑一道想去认识的新菜。', }; // 4. 图片资源白名单 const VALID_IMAGE_RES_NAMES: Set<string> = new Set( RECOMMEND_LIST.map((item) => item.imageResName).concat(FALLBACK_ITEM.imageResName) ); // 5. 今日选择算法 export function getRecommendOfToday(): RecommendItem { ... } // 6. 兜底获取 export function getFallbackItem(): RecommendItem { ... } // 7. 图片资源校验 export function resolveImageResName(imageResName: string): string { ... }这个文件承担了 7 类职责,每一类都值得单独分析。
二、职责 1:定义卡片数据结构
export interface RecommendItem { id: string; name: string; region: string; imageResName: string; highlight: string; summary: string; }通过RecommendItem明确了卡片需要的 6 个字段。这让数据项结构先稳定下来。
字段设计的考量:
字段 | 类型 | 用途 | 为什么需要 |
|---|---|---|---|
| string | 点击跳转到菜品详情页 | 卡片点击需要传参数 |
| string | 卡片上显示菜名 | 核心展示信息 |
| string | 卡片上显示地区 | 帮助用户判断兴趣 |
| string | 鸿蒙图片资源名 | 卡片需要显示图片 |
| string | 口味亮点标签 | 吸引用户点击 |
| string | 一句话介绍 | 补充信息,帮助决策 |
这个结构既不过于复杂(6 个字段),也不过于简单(包含了点击跳转需要的id)。
三、职责 2:维护推荐列表和兜底项
const RECOMMEND_LIST: RecommendItem[] = [ // 10 道菜品... ]; const FALLBACK_ITEM: RecommendItem = { id: 'fallback', name: '环球美食', region: '世界', imageResName: 'dish_fallback', highlight: '今天吃什么', summary: '打开食界探味,挑一道想去认识的新菜。', };RECOMMEND_LIST是正常推荐数据,FALLBACK_ITEM是兜底数据。
兜底项的价值:
场景 | 没有兜底 | 有兜底 |
|---|---|---|
列表为空 | 卡片显示空白 | 显示"环球美食" |
图片资源不存在 | 卡片渲染崩溃 | 显示兜底图片 |
数据异常 | 用户看到异常 | 用户看到正常兜底 |
鸿蒙卡片比应用页面更怕显示异常——因为卡片在桌面上,异常会一直显示,用户无法刷新。
四、职责 3:图片资源校验
const VALID_IMAGE_RES_NAMES: Set<string> = new Set( RECOMMEND_LIST.map((item) => item.imageResName).concat(FALLBACK_ITEM.imageResName) ); export function resolveImageResName(imageResName: string): string { if (!imageResName || !VALID_IMAGE_RES_NAMES.has(imageResName)) { return FALLBACK_ITEM.imageResName; } return imageResName; }VALID_IMAGE_RES_NAMES是鸿蒙图片资源的白名单。resolveImageResName会检查传入的资源名是否在白名单中,如果不在就返回兜底图片。
这个设计的好处:
防止资源不存在导致崩溃— 鸿蒙卡片如果引用了不存在的
$r()资源,会直接崩溃新增菜品时只需加白名单— 不需要改校验逻辑
兜底行为明确— 用户永远看不到空白图片
五、职责 4:今日选择算法
export function getRecommendOfToday(): RecommendItem { if (RECOMMEND_LIST.length === 0) return FALLBACK_ITEM; const now = new Date(); const dateNum = now.getFullYear() * 10000 + (now.getMonth() + 1) * 100 + now.getDate(); const index = dateNum % RECOMMEND_LIST.length; return RECOMMEND_LIST[index]; }日期轮询算法:用年月日生成数字,对列表长度取模。
日期 | dateNum | index (mod 10) | 菜品 |
|---|---|---|---|
2025-01-15 | 20250115 | 5 | 牛肉塔可 |
2025-01-16 | 20250116 | 6 | 石锅拌饭 |
2025-01-17 | 20250117 | 7 | 班尼迪克蛋 |
这个算法的特点:
同一天内结果一致— 所有卡片展示同一道菜
不同天自动切换— 不需要手动更新
列表为空时兜底— 返回
FALLBACK_ITEM不依赖网络— 纯本地计算
六、职责 5:兜底获取
export function getFallbackItem(): RecommendItem { return FALLBACK_ITEM; }单独导出兜底项,方便其他地方使用(比如卡片 UI 的默认值)。
七、数据层和 Ability 的分工
RecommendData.ets(数据层) │ ├─ RecommendItem ← 数据结构 ├─ RECOMMEND_LIST[] ← 推荐列表 ├─ FALLBACK_ITEM ← 兜底数据 ├─ VALID_IMAGE_RES_NAMES ← 资源白名单 ├─ getRecommendOfToday() ← 选择算法 ├─ getFallbackItem() ← 兜底获取 └─ resolveImageResName() ← 资源校验 DailyRecommendFormAbility.ets(Ability 层) │ ├─ onAddForm() ← 消费数据层 ├─ onUpdateForm() ← 消费数据层 └─ onRemoveForm() ← 清理资源Ability 只关心"拿数据 → 绑定到卡片",不关心数据从哪来、怎么选、怎么校验。
八、为什么这种组织方式值得复用
好处 | 说明 |
|---|---|
职责清晰 | 数据层管内容,Ability 管生命周期 |
易于测试 | 数据层可以独立测试,不需要启动 Ability |
易于扩展 | 新增菜品只需加 RECOMMEND_LIST |
易于维护 | 改数据不影响 Ability,改 Ability 不影响数据 |
兜底完善 | 资源异常时不会崩溃 |
关键代码位置
文件 | 作用 |
|---|---|
| 鸿蒙卡片数据层(本文核心) |
| 消费数据层 |
数据层职责全景图
RecommendData.ets │ ├─ 接口定义 │ └─ RecommendItem { id, name, region, imageResName, highlight, summary } │ ├─ 数据存储 │ ├─ RECOMMEND_LIST[] ← 10 道菜品 │ ├─ FALLBACK_ITEM ← 兜底数据 │ └─ VALID_IMAGE_RES_NAMES ← 图片资源白名单 │ ├─ 选择算法 │ └─ getRecommendOfToday() ← 日期轮询 │ ├─ 兜底获取 │ └─ getFallbackItem() ← 返回 FALLBACK_ITEM │ └─ 资源校验 └─ resolveImageResName() ← 白名单校验 + 兜底常见坑
把推荐列表直接写在 Ability 里— 改数据要动 Ability,职责混乱
没有兜底数据项— 鸿蒙卡片异常时显示空白
图片资源名不做校验— 鸿蒙
$r()引用不存在的资源会崩溃每次更新时间策略都散落在多个地方— 应该集中管理
数据结构不稳定— 新增字段时要改多个文件
没有导出数据结构— 其他文件无法复用
RecommendItem
可复用模板
鸿蒙卡片数据层模板
// 1. 数据结构 export interface CardItem { id: string; title: string; subtitle: string; imageRes: string; highlight: string; summary: string; } // 2. 数据列表 const ITEM_LIST: CardItem[] = [ { id: '1', title: '标题1', subtitle: '副标题1', imageRes: 'img_1', highlight: '亮点1', summary: '简介1' }, // ... ]; // 3. 兜底数据 const FALLBACK: CardItem = { id: 'fallback', title: '默认标题', subtitle: '默认副标题', imageRes: 'img_fallback', highlight: '默认亮点', summary: '默认简介', }; // 4. 资源白名单 const VALID_RES: Set<string> = new Set(ITEM_LIST.map(i => i.imageRes).concat(FALLBACK.imageRes)); // 5. 选择算法 export function getTodayItem(): CardItem { if (ITEM_LIST.length === 0) return FALLBACK; const now = new Date(); const dateNum = now.getFullYear() * 10000 + (now.getMonth() + 1) * 100 + now.getDate(); return ITEM_LIST[dateNum % ITEM_LIST.length]; } // 6. 资源校验 export function safeImage(name: string): string { if (!name || !VALID_RES.has(name)) return FALLBACK.imageRes; return name; }数据层职责清单
鸿蒙卡片数据层应该包含: □ 数据结构定义(export interface) □ 数据列表(const LIST) □ 兜底数据(const FALLBACK) □ 资源白名单(const VALID_RES) □ 选择算法(getTodayItem) □ 资源校验(safeImage) □ 兜底获取(getFallback)本篇总结
鸿蒙卡片数据层应该独立于 Ability 文件。RecommendData.ets这种组织方式很适合"每日推荐"类卡片:
数据结构稳定—
RecommendItem接口定义了 6 个字段数据和兜底并存—
RECOMMEND_LIST+FALLBACK_ITEM资源校验完善—
VALID_IMAGE_RES_NAMES白名单 +resolveImageResName兜底选择算法轻量— 日期轮询,不依赖网络
职责分离— 数据层管内容,Ability 管生命周期
先把数据结构、兜底和轮换规则单独收好,后面维护会轻很多。这份代码很适合当鸿蒙卡片数据层组织的第一块样板。