HarmonyOS 互动卡片刷新太频繁怎么办:FormExtensionAbility、用户触发和缓存兜底怎么拆

互动卡片很容易被写成“迷你页面”。页面里能做的事,卡片里也想做;页面可以请求接口,卡片也跟着请求;页面可以频繁刷新,卡片也跟着频繁刷新。这样写一开始看着功能丰富,后面问题会集中爆出来。
这篇按 HarmonyOS 5.0.0 及以上版本的卡片入口来拆。重点不是把所有接口都堆上去,而是把卡片刷新、用户触发和缓存兜底三件事分清楚。
我遇到这类问题时,通常先看三个现象:
- 卡片一刷新,接口就被打很多次;
- 用户点了卡片按钮,但页面打开后状态对不上;
- 网络失败时,卡片直接空白,看起来像应用坏了。
这些问题的根源基本一致:卡片没有自己的“轻量状态模型”,而是直接复用了页面的请求链路。
卡片要解决的是快速查看和快速触达。它不适合承载完整页面逻辑,更不适合在每一次显示、每一次点击后都做重请求。
我会把卡片拆成四层:
| 层级 | 负责什么 | 不负责什么 |
|---|---|---|
| FormExtensionAbility | 接收卡片生命周期、提供初始数据、处理卡片更新 | 不承载页面大流程 |
| 卡片数据快照 | 保存可直接渲染的小数据 | 不保存大对象和页面状态 |
| 刷新调度器 | 控制刷新间隔、合并重复刷新 | 不直接渲染 UI |
| 页面入口 | 接住用户点击后的完整流程 | 不让卡片自己做复杂跳转判断 |
这样拆以后,卡片就很清楚:能显示旧数据就先显示旧数据,需要刷新再按规则刷新;用户点击时记录意图,完整业务交给页面。
下面这种写法最容易造成刷新风暴:
async function onFormVisible(formId: string): Promise<FormSnapshot> {
const data = await requestLatestData()
return {
formId,
title: data.title,
countText: `${data.count} 条待处理`,
updatedAt: Date.now()
}
}
问题在于,它没有判断上次什么时候刷新过,也没有旧快照兜底。卡片如果被系统调度、用户多次触发、页面反复打开,就可能把一个轻量入口变成高频请求源。
更稳的写法是:先读缓存,再判断是否需要刷新,最后决定要不要请求。
先定义一个卡片快照:
interface FormSnapshot {
formId: string
title: string
countText: string
updatedAt: number
fromCache: boolean
}
再做一个刷新调度器:
class FormRefreshGuard {
private lastRefreshAt: Map<string, number> = new Map()
canRefresh(formId: string, now: number, minIntervalMs: number): boolean {
const last = this.lastRefreshAt.get(formId) ?? 0
return now - last >= minIntervalMs
}
markRefreshed(formId: string, now: number): void {
this.lastRefreshAt.set(formId, now)
}
}
卡片侧使用时,不要一上来就请求:
async function loadFormSnapshot(formId: string, now: number): Promise<FormSnapshot> {
const cached = await readCachedSnapshot(formId)
if (cached && !guard.canRefresh(formId, now, 5 * 60 * 1000)) {
return {
...cached,
fromCache: true
}
}
const remote = await requestLatestData()
const next = {
formId,
title: remote.title,
countText: `${remote.count} 条待处理`,
updatedAt: now,
fromCache: false
}
await saveCachedSnapshot(next)
guard.markRefreshed(formId, now)
return next
}
这个方案不是为了少写几行代码,而是为了把刷新节奏控制住。卡片是桌面上的常驻入口,不能用页面按钮点击的思路去请求接口。
卡片最怕的不是数据旧一点,而是直接空白。网络失败、接口超时、账号状态临时异常时,如果卡片没有旧快照,用户看到的就是一个坏入口。
我会把失败处理写成这样:
async function safeRefreshForm(formId: string, now: number): Promise<FormSnapshot> {
const cached = await readCachedSnapshot(formId)
try {
const remote = await requestLatestData()
const next = {
formId,
title: remote.title,
countText: `${remote.count} 条待处理`,
updatedAt: now,
fromCache: false
}
await saveCachedSnapshot(next)
return next
} catch (err) {
if (cached) {
return {
...cached,
fromCache: true
}
}
return {
formId,
title: '暂时无法刷新',
countText: '稍后再试',
updatedAt: now,
fromCache: true
}
}
}
这里的重点是兜底顺序:
- 有旧快照,就先展示旧快照;
- 没有旧快照,才展示轻量错误态;
- 错误态不要做成长说明,卡片空间有限;
- 完整错误原因放到打开后的页面里处理。
互动卡片会有按钮,比如“继续处理”“查看详情”“刷新”。这些按钮不应该直接在卡片侧处理完整流程。卡片侧适合传一个明确意图:
interface FormActionIntent {
formId: string
action: 'openDetail' | 'refresh' | 'continue'
payloadId?: string
createdAt: number
}
页面打开后再接住这个意图:
function handleFormIntent(intent: FormActionIntent): void {
if (intent.action === 'openDetail' && intent.payloadId) {
openDetailPage(intent.payloadId)
return
}
if (intent.action === 'refresh') {
refreshCurrentPage()
return
}
openHomePage()
}
这样卡片和页面都清楚:卡片只表达“用户想做什么”,页面负责“这件事怎么完整执行”。
| 方案 | 适合场景 | 风险 |
|---|---|---|
| 每次触发都请求 | 数据强实时且频率很低 | 很容易造成接口压力和耗电 |
| 固定间隔刷新 | 天气、日程、待办数量这类摘要 | 间隔太短会浪费资源 |
| 缓存优先,过期再刷 | 大多数卡片入口 | 数据可能略旧,但体验稳定 |
| 用户点击后刷新 | 用户明确需要最新数据 | 要避免连点重复请求 |
我更倾向“缓存优先,过期再刷”。如果用户点了刷新,再走用户触发刷新;如果只是系统调度或卡片恢复显示,先看缓存和刷新间隔。
如果应用里有多张卡片,不要每张卡片都手写一遍刷新判断。可以封装一个快照仓库:
class FormSnapshotStore {
private cache: Map<string, FormSnapshot> = new Map()
read(formId: string): FormSnapshot | undefined {
return this.cache.get(formId)
}
write(snapshot: FormSnapshot): void {
this.cache.set(snapshot.formId, snapshot)
}
isExpired(formId: string, now: number, ttlMs: number): boolean {
const snapshot = this.cache.get(formId)
if (!snapshot) {
return true
}
return now - snapshot.updatedAt >= ttlMs
}
}
真实项目里可以把这个 Map 换成持久化存储,但接口形态尽量保持简单:读快照、写快照、判断过期。这样卡片的刷新策略不会散落在多个生命周期函数里。
我用脚本跑了两个场景:
{
"repeatRefresh": {
"firstFromCache": false,
"secondFromCache": true,
"remoteCalls": 1
},
"networkFallback": {
"title": "今日摘要",
"fromCache": true,
"remoteCalls": 1
}
}
第一组证明 5 分钟内重复触发不会重复打接口;第二组证明网络失败后能显示旧快照,不会让卡片空白。
互动卡片刷新异常时,我会按这个顺序查:
- 是否每次生命周期触发都直接请求接口;
- 是否有快照缓存,缓存里有没有更新时间;
- 是否区分系统调度刷新和用户主动刷新;
- 网络失败时有没有旧数据兜底;
- 卡片点击是否只传意图,而不是在卡片侧做完整页面逻辑。
互动卡片写稳以后,它对应用是加分项:用户不用打开完整页面也能看到摘要,点一下又能进入完整流程。反过来,如果刷新边界没拆好,它会变成一个隐藏的高频请求入口。卡片越轻,边界越要清楚。
更多推荐



所有评论(0)