给素材库接网络,难点不在于发一次请求,而在于网络不可用时编辑器仍然要能打开。素材面板不应该因为远程地址配置错误、接口返回空内容或 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 标记但仍可导入

远程缓存当前不承担的职责

当前缓存是进程内内存缓存,不保证应用重启后仍保留远程快照,也不实现增量更新、签名验证或复杂离线同步。对当前素材规模而言,本地兜底比把网络缓存做成另一套持久化系统更重要。

这一篇的重点是让远程素材失败时编辑器仍有可靠的本地内容。素材库中的一个典型可编辑来源是拼豆图案,下一篇把字符模板怎样落成图层、颜色和画布讲清楚。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐