HarmonyOS ResourceStr 实战:中式美食菜谱封面怎么统一注册、兜底和缓存


中式美食的菜谱封面一开始很好处理:列表卡片里直接写一张图,详情页也能显示。问题是页面一多,图片很快就乱了。同一道菜在首页是一张图,详情页又是一张图;有的菜缺封面,列表里露出空白;推荐卡片尺寸和列表卡片不一样,滚动时高度还会抖。图片看起来只是 UI 细节,但它会直接影响列表稳定性和用户对应用的第一眼判断。
我这次不把图片问题放到每个页面里单独修,而是把封面来源、兜底图、尺寸比例和缓存 key 收到一个小的资源层。页面只问"这道菜应该显示哪张封面",不用关心图片来自本地资源、远程地址,还是默认占位。

环境和边界

项目 说明
应用 中式美食
技术栈 HarmonyOS / ArkTS / ArkUI
相关能力 ResourceStrImage、固定比例容器、默认图兜底
使用页面 首页列表、菜谱详情、收藏列表、推荐卡片
核心目标 同一道菜在不同页面使用同一套封面解析规则

这篇只处理菜谱封面,不展开讲图片上传、图片编辑和云端 CDN。先把本地资源和页面消费边界稳住,后面再接远程图会顺很多。

图片来源先分清楚

来源 例子 使用建议
本地资源 内置菜谱封面、默认封面 适合首批固定菜谱和兜底图
远程地址 后续后台或云端图 需要缓存 key 和失败兜底
生成图 运营封面、文章配图 不要直接散落在页面里
默认图 缺图、加载失败、数据异常 必须统一,不要每页各写一张

如果页面各自决定图片来源,后面肯定会出问题。首页卡片可能写 $r('app.media.mapo_tofu'),详情页写另一个资源名,推荐卡片忘记兜底。看着都能运行,但产品体验会越来越散。

先定义封面描述

页面不应该直接处理所有图片来源。可以先定义一个统一的描述对象,让服务层把业务字段转换成页面能消费的图片结果。

ts 复制代码
type RecipeCoverSource =
  | { type: 'local'; value: ResourceStr }
  | { type: 'remote'; value: string; cacheKey: string }
  | { type: 'fallback'; value: ResourceStr }

interface RecipeCoverMeta {
  recipeId: string
  name: string
  source: RecipeCoverSource
  aspectRatio: number
}

这里的 aspectRatio 我会放进元信息里。因为列表、详情和推荐卡片虽然尺寸不同,但比例最好稳定。比例不稳定,长列表滚动时卡片高度就容易跳。

ResourceStr 注册表负责本地图

本地封面不要散落在页面里。页面只拿菜谱 id,注册表返回对应资源;没有就返回默认图。

ts 复制代码
class RecipeImageRegistry {
  private readonly defaultCover: ResourceStr = $r('app.media.default_recipe_cover')

  private readonly localCovers = new Map<string, ResourceStr>([
    ['mapo_tofu', $r('app.media.cover_mapo_tofu')],
    ['tomato_egg', $r('app.media.cover_tomato_egg')],
    ['kung_pao_chicken', $r('app.media.cover_kung_pao_chicken')]
  ])

  getLocalCover(recipeId: string): ResourceStr {
    return this.localCovers.get(recipeId) ?? this.defaultCover
  }

  hasLocalCover(recipeId: string): boolean {
    return this.localCovers.has(recipeId)
  }
}

这段代码的价值不是 Map 本身,而是把"哪个菜用哪张图"从页面里拿出来。以后换图,只改注册表;页面不用一个个翻。

封面解析服务统一出口

注册表只管本地资源,真正给页面用的可以是一个 RecipeCoverResolver。它先看菜谱有没有远程封面,再看本地注册表,最后给默认图。

ts 复制代码
class RecipeCoverResolver {
  constructor(private imageRegistry: RecipeImageRegistry) {}

  resolve(recipe: RecipeCardModel): RecipeCoverMeta {
    if (recipe.remoteCoverUrl && recipe.remoteCoverUrl.startsWith('https://')) {
      return {
        recipeId: recipe.id,
        name: recipe.name,
        source: {
          type: 'remote',
          value: recipe.remoteCoverUrl,
          cacheKey: this.buildCacheKey(recipe)
        },
        aspectRatio: 16 / 10
      }
    }

    return {
      recipeId: recipe.id,
      name: recipe.name,
      source: {
        type: this.imageRegistry.hasLocalCover(recipe.id) ? 'local' : 'fallback',
        value: this.imageRegistry.getLocalCover(recipe.id)
      },
      aspectRatio: 16 / 10
    }
  }

  private buildCacheKey(recipe: RecipeCardModel): string {
    return `${recipe.id}_${recipe.coverVersion ?? 0}`
  }
}

cacheKey 里带 coverVersion 是为了后面换图时能刷新缓存。否则远程图地址不变、内容换了,页面可能还显示旧图。

页面只用一个图片组件

列表卡片、详情页头图、推荐卡片都可以共用一个封面组件,只是尺寸不同。组件内部处理本地图、远程图、兜底图和 objectFit。

ts 复制代码
@Component
struct RecipeCoverImage {
  cover: RecipeCoverMeta
  widthValue: Length = '100%'
  heightValue: Length = 120

  build() {
    Stack() {
      if (this.cover.source.type === 'remote') {
        Image(this.cover.source.value)
          .alt($r('app.media.default_recipe_cover'))
          .objectFit(ImageFit.Cover)
          .width(this.widthValue)
          .height(this.heightValue)
      } else {
        Image(this.cover.source.value)
          .objectFit(ImageFit.Cover)
          .width(this.widthValue)
          .height(this.heightValue)
      }
    }
    .clip(true)
    .borderRadius(10)
    .backgroundColor('#F6F1EA')
  }
}

页面里不要再直接 Image(recipe.cover)。这样写虽然快,但一旦有远程图、缺图和默认图,所有页面都要重复一遍判断。

列表卡片要固定比例

图片加载慢时,如果容器高度跟着图片走,列表会抖。对菜谱列表来说,我宁愿先固定卡片尺寸,再等图片填进去。

ts 复制代码
@Component
struct RecipeCard {
  recipe: RecipeCardModel
  coverResolver: RecipeCoverResolver

  build() {
    Column() {
      RecipeCoverImage({
        cover: this.coverResolver.resolve(this.recipe),
        widthValue: '100%',
        heightValue: 132
      })

      Text(this.recipe.name)
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })

      Text(`${this.recipe.cookMinutes} 分钟 · ${this.recipe.difficultyText}`)
        .fontSize(12)
        .fontColor('#8A7562')
    }
    .width('100%')
  }
}

固定高度不是为了偷懒,而是为了滚动稳定。菜谱封面质量参差不齐时,统一比例能让列表看起来更像一个产品,而不是一堆随手拼起来的图片。

缓存 key 怎么设计

如果后面封面来自远程,缓存 key 不能只用 URL。换图时如果 URL 不变,缓存可能不刷新。我会把菜谱 id 和封面版本一起放进去。

字段 作用
recipeId 标记是哪道菜
coverVersion 封面更新时主动变化
sourceType 区分本地资源和远程资源
fallback 加载失败时统一兜底
ts 复制代码
interface CoverCacheRecord {
  recipeId: string
  sourceType: 'local' | 'remote'
  cacheKey: string
  updatedAt: number
}

function buildCoverCacheKey(recipe: RecipeCardModel): string {
  const version = recipe.coverVersion ?? 0
  const source = recipe.remoteCoverUrl ? 'remote' : 'local'
  return `${source}_${recipe.id}_${version}`
}

这套 key 不复杂,但能避免两个常见问题:不同菜用了同一个缓存名,或者封面更新后页面还拿旧图。

验收时我会看这些点

场景 操作 期望结果
列表正常封面 打开首页菜谱列表 每张卡片比例稳定,不跳动
详情页同一道菜 从列表进入详情 详情页和列表使用同一张封面
缺图菜谱 构造没有封面的菜谱 显示统一默认图,不出现空白
推荐卡片 打开推荐区域 推荐图和列表图来源一致
更换封面版本 修改 coverVersion 缓存 key 变化,旧图不会继续占用

图片问题不能只靠"看起来有图"。我会同时看列表、详情和推荐三个入口,因为封面不一致通常就是多入口暴露出来的。

失败用例也要提前兜住

失败用例 可能后果 处理方式
本地资源没有注册 页面空白或报错 注册表返回默认图
远程图地址为空 Image 加载失败 Resolver 转成本地兜底
同一道菜多处写死封面 列表和详情不一致 页面只消费 Resolver
图片尺寸不固定 列表滚动时卡片跳动 图片容器固定高度和比例
封面更新后缓存不刷新 用户仍看到旧图 cacheKey 带 coverVersion

这些失败用例看着小,但它们会直接影响用户对应用的判断。做菜应用图片很多,封面一乱,内容再好也会显得不稳。

这次拆完后的判断

图片资源管理不是单纯为了好看,而是为了让中式美食的列表、详情、收藏和推荐看起来像同一个应用。封面来源统一以后,页面代码会轻一点,缺图兜底也不会到处复制。
我的经验是:图片越多,越不能让页面各自发挥。先把资源注册、封面解析、默认图和缓存 key 收到一处,后面无论换成本地图、远程图还是生成图,页面都不用大改。这个边界稳住以后,再去打磨视觉,才不会边做边漏。

相关推荐
Helen_cai1 小时前
OpenHarmony http 网络请求封装与全局拦截实战(API Version23 + 适配版)
网络·网络协议·http·华为·harmonyos
Helen_cai5 小时前
OpenHarmony 完整项目工程整合规范 + 模块化分层架构(API23+ 标准企业级结构)
开发语言·华为·php·harmonyos
木木子229 小时前
# 密码强度检测深度解析:正则表达式实时分析、多维度评分算法与可视化反馈
mysql·算法·华为·正则表达式·harmonyos
星释13 小时前
鸿蒙智能体开发实战:6.A2A 模式开发接口
华为·harmonyos·鸿蒙·智能体
澹然疏离14 小时前
HarmonyOS应用《民族图鉴》开发第34篇:TTSEngineService——语音引擎封装与降级策略
华为·harmonyos
花开彼岸天~14 小时前
03 module.json5 配置文件全解:能力声明、权限申请与设备支持
华为·harmonyos·鸿蒙系统
ZZZMMM.zip15 小时前
基于鸿蒙HarmonyOS NEXT开发AI股票分析应用:智能投资新体验与鸿蒙Flutter框架跨端实践
人工智能·flutter·华为·harmonyos·鸿蒙
花开彼岸天~17 小时前
04 oh-package.json5 与依赖管理:ohpm 包管理器全解析
华为·harmonyos·鸿蒙系统
爸爸61918 小时前
06 项目目录结构与编码规范:三层架构与 DAO 模式实践
华为·架构·harmonyos·鸿蒙系统