原生鸿蒙像素画板实战 19:远程素材缓存
给素材库接网络,难点不在于发一次请求,而在于网络不可用时编辑器仍然要能打开。素材面板不应该因为远程地址配置错误、接口返回空内容或 JSON 格式异常而变成一片空白。bitArt 把远程素材当作“可选增强”,本地素材始终是可靠兜底;缓存则负责减少重复请求,并向界面明确暴露数据是否已经过期。

远程素材必须是可失败的增强路径
如果页面启动时只等远程接口返回,离线用户会失去全部素材。更隐蔽的是内存缓存直接把内部数组暴露给组件:某个组件排序或 push 后,下一次打开素材库就得到被意外修改的全局数据。接口 JSON 也不能只要能解析就相信;categories 和 materials 不是数组时,后续渲染同样会失败。
网络提供者与缓存仓储如何分工
网络素材只能改善体验,不能成为编辑器能否启动的前提。本地快照和缓存共同保证网络失败时仍有一份可用内容。

缓存不能直接返回可被页面改写的引用
MaterialLibraryService 的 getSnapshot 先问缓存,没有缓存就立即返回本地快照并保存副本。refreshSnapshot 才尝试远程;远程返回合法数据时与本地快照合并,否则再回退本地。MaterialCacheRepository 在读取时克隆 categories 和 materials,并计算 stale 标记。缓存过期并不等于数据不能用,它只是告诉界面“可以尝试刷新”,而不是让素材突然消失。
远程提供者先检查是否启用远程素材,再请求固定的 snapshot JSON。请求失败、空 body、JSON 解析异常或结构不合法都返回 null。这些失败被限制在服务内部,页面只会接到一份仍可显示的 MaterialLibrarySnapshot。
获取素材快照时,缓存和本地来源先于网络请求,保证首屏可用。
static getSnapshot(): MaterialLibrarySnapshot {
const cachedSnapshot = MaterialCacheRepository.getSnapshot();
if (cachedSnapshot !== null) return cachedSnapshot;
const localSnapshot = LocalMaterialProviderService.getSnapshot();
MaterialCacheRepository.saveSnapshot(localSnapshot);
return localSnapshot;
}
static async refreshSnapshot(): Promise<MaterialLibrarySnapshot> {
const remoteSnapshot = await RemoteMaterialProviderService.fetchSnapshot();
if (remoteSnapshot !== null) {
const mergedSnapshot = MaterialLibraryService.mergeWithLocalFallback(remoteSnapshot);
MaterialCacheRepository.saveSnapshot(mergedSnapshot);
return mergedSnapshot;
}
return MaterialLibraryService.getSnapshot();
}
远程与本地合并时要保留本地兜底
本地与远程分类合并时,先保留远程分类,再按 ID 补入远程中没有的本地分类;素材数组则把远程项和本地项并列,确保本地模板永远可见。缓存 TTL 设为 6 小时,过期时标记 stale,但不立刻清空数据。这个策略适合非关键内容:优先保证编辑器可用,再争取内容新鲜度。
static getSnapshot(): MaterialLibrarySnapshot | null {
if (MaterialCacheRepository.cachedSnapshot === null) return null;
const snapshot = MaterialCacheRepository.cloneSnapshot(MaterialCacheRepository.cachedSnapshot);
snapshot.stale = Date.now() - MaterialCacheRepository.cachedAt > MATERIAL_CACHE_TTL_MS;
return snapshot;
}
static saveSnapshot(snapshot: MaterialLibrarySnapshot): void {
MaterialCacheRepository.cachedSnapshot = MaterialCacheRepository.cloneSnapshot(snapshot);
MaterialCacheRepository.cachedAt = Date.now();
}

缓存状态不等于创作项目状态
素材缓存属于仓储层,不属于项目持久化。清缓存只影响下次取得素材的来源,不会删除用户的画布、图层或已导入的素材。界面可以根据 sourceLabel 和 stale 做轻量提示,但不应把网络状态混入当前项目的脏状态。
缓存的 stale 标记是内容状态,不是项目修改;刷新素材不应该让用户画布变脏。
if (!NetworkApiClientService.isRemoteMaterialsEnabled()) return null;
const result = await NetworkApiClientService.getText('/materials/snapshot.json');
if (!result.ok || result.body.length === 0) return null;
try {
const dto = JSON.parse(result.body) as RemoteMaterialSnapshotDto;
if (!Array.isArray(dto.categories) || !Array.isArray(dto.materials)) return null;
return { categories: dto.categories, materials: dto.materials, updatedAt: dto.updatedAt ?? Date.now(),
stale: false, sourceLabel: '远程素材' };
} catch (_error) { return null; }

网络素材链路里应该主动防住的失败
- 远程失败时返回 null 交给上层回退,不要把异常继续抛到素材面板。
- 缓存读取和写入都要克隆快照,避免组件修改全局数组。
- 缓存过期只表示建议刷新,不等于应该清空当前可用素材。
远程素材接口里不应携带账号、私密地址或令牌。文章中的示例只描述公开快照结构,真实部署时应由网络配置服务处理地址与开关。把“素材数据正确”与“网络配置正确”分开,排查时也更容易定位问题。
缓存策略要经得起离线场景
远程缓存最有价值的测试不是网络正常,而是网络故意不正常。可以先禁用远程开关、再模拟 404、空 body、JSON 语法错误和字段类型错误,逐次打开素材库。每一种情况都应得到本地快照,而不是空列表或未捕获异常。随后恢复一个合法远程快照,确认它会与本地分类合并,本地模板没有被远程内容挤掉。
缓存副本也值得单测。取出一次快照后,对调用方数组做排序或临时 push,再重新取缓存,第二次结果不应该继承第一次的修改。若发生继承,说明仓储把内部引用暴露给了界面。最后模拟超过 TTL,检查 stale 标记出现但素材仍能导入。这样能把“内容不新鲜”和“内容不可用”这两种状态清楚分开。

怎样确认网络失败不会清空素材
网络检查要故意制造失败响应,只有回退行为正确才算素材链路可靠。
- 关闭远程素材开关或断开网络,确认素材库仍显示本地内容。
- 模拟空响应、非法 JSON 和缺少数组字段的响应,确认页面没有崩溃且回退到本地快照。
- 刷新后修改某个页面使用的数组,重新读取缓存,确认原缓存没有被污染。
- 等待或模拟 TTL 过期,确认快照标记为 stale 但内容仍可用于导入。
远程素材检查记录
断网后仍显示本地素材
异常响应不让面板崩溃
缓存读取返回副本
过期内容带 stale 标记但仍可导入

远程缓存当前不承担的职责
当前缓存是进程内内存缓存,不保证应用重启后仍保留远程快照,也不实现增量更新、签名验证或复杂离线同步。对当前素材规模而言,本地兜底比把网络缓存做成另一套持久化系统更重要。
这一篇的重点是让远程素材失败时编辑器仍有可靠的本地内容。素材库中的一个典型可编辑来源是拼豆图案,下一篇把字符模板怎样落成图层、颜色和画布讲清楚。
更多推荐




所有评论(0)