文章导读
- 本地内容目录不是演示用假数据,而是首页、搜索、详情和球员动态的离线兜底。
- 资讯、赛事、装备、球员拆成四份实体数据,页面按业务场景各取所需。
- 稳定 id 把搜索、收藏、浏览历史和详情页串成同一条内容链路。
- 联网数据写入
@StorageProp后,本地目录还能继续承担“缺什么补什么”的保底职责。
页面效果
首次启动应用时,首页上半部分会先展示轮播、金刚区和最新资讯;即使还没执行联网刷新,下面的资讯卡片也不是空白占位,而是能直接点进详情的真实内容。对读者来说,这种体验非常直观:应用刚装好就能浏览赛事、装备和球员,不需要等接口成功才“活过来”。
从这个界面可以反推当前工程对“本地目录”的要求并不低。轮播图要能跳详情,金刚区要能分流到装备、赛事、技术、球员,首页列表要能展示标签、时间、阅读数,后续搜索页还要把这些实体重新聚合。如果只是随手塞几条文本占位,很快就会在跳转、搜索或收藏时露馅。
实战拆解
很多示例应用只依赖接口返回,一断网就只剩空列表。羽球联盟没有走这条路,而是把资讯、赛事、装备、球员拆成独立目录,各自维护最接近页面展示的数据结构。这样做的第一个好处是“页面不用等后端定义完全稳定”。首页只消费文章卡片需要的字段,赛事页只关注赛事状态和比赛条目,球员页只关心头像、队伍、分类和摘要,各自迭代互不拖累。
第二个好处是“远程刷新和本地兜底可以并存”。首页和球员页都会优先读取@StorageProp里的远程数据;如果当前还没刷到内容,或者某一类资源没有覆盖完整,本地数组立即顶上,不会让首屏和详情页出现断层。读者在界面上看到的是正常列表,开发侧维护的却是一条很清晰的数据优先级:远程优先,本地保底。
第三个好处来自跨实体搜索。搜索页并没有简单地在远程数组里筛选,而是先把远程资讯/装备与本地目录合并,再做关键字匹配。这样即使线上只同步了部分内容,本地目录里的装备和资讯仍然能被搜到,用户不会因为“数据来源不同”得到两套割裂的搜索结果。
id 设计是这套目录能成立的关键。首页点击资讯卡片会记录浏览历史,搜索页点击结果会进入详情,收藏页和球员关注又会回到同一批实体。如果后续把本地数组替换成远程接口,但 id 跟着排序位置变化,那么历史记录、收藏映射和详情路由都会失效。这个项目从一开始就把稳定 id 当成基础契约,而不是“写完列表之后再补”的细节。
搜索与列表能证明什么
搜索页最能体现“本地目录不是摆设”。关键字输入YONEX后,页面会同时统计资讯命中数和装备命中数,再按分段 Tab 切换视图。这里的命中结果并不依赖单一接口,而是来自一份已经合并好的内容目录。只要本地目录里仍保留装备条目,即使远程刷新失败,搜索结果页也不会整个失效。
这张图还说明了另一个实现细节:目录拆分并不意味着页面体验被拆碎。对用户而言,“资讯”和“装备”只是两个 Tab;对开发而言,它们背后分别对应文章数组和装备数组,两者通过统一的搜索入口被重新组织在一起。这正是本地内容目录的价值所在:底层按实体建模,上层按场景组合。
关键代码
private latestList(): Article[] { const src: Article[] = this.remoteArticles.length > 0 ? this.remoteArticles : ARTICLES; return src.slice(0, 8); } private allArticles(): Article[] { if (this.remoteArticles.length === 0) { return ARTICLES; } const remoteIds: Set<string> = new Set(); for (const a of this.remoteArticles) { remoteIds.add(a.id); } const merged: Article[] = []; for (const a of this.remoteArticles) { merged.push(a); } for (const a of ARTICLES) { if (!remoteIds.has(a.id)) { merged.push(a); } } return merged; } private matchedEquipments(): Equipment[] { const kw: string = this.keyword.trim().toLowerCase(); if (kw.length === 0) { return []; } return this.allEquipments().filter((e: Equipment) => { const name: string = (e.name ?? '').toLowerCase(); const brand: string = (e.brand ?? '').toLowerCase(); const summary: string = (e.summary ?? '').toLowerCase(); const tag: string = (e.tag ?? '').toLowerCase(); return name.indexOf(kw) >= 0 || brand.indexOf(kw) >= 0 || summary.indexOf(kw) >= 0 || tag.indexOf(kw) >= 0; }); }这段代码把“本地目录怎样托住页面”讲得很清楚。首页的latestList()先判断远程资讯是否已经写入;没写入就直接回退到本地ARTICLES。搜索页的allArticles()则更进一步:不是简单二选一,而是先收集远程 id,再把本地里缺失的条目补回结果集。这样处理以后,远程数据可以不断刷新,本地目录仍然承担补位职责,而且不会因为重复 id 产生双份卡片。
装备搜索的实现也说明目录拆分是值得的。装备实体有自己的name、brand、summary、tag,和资讯文章并不是同一套字段;如果一开始把所有内容粗暴塞进一个总表,搜索逻辑要么写很多分支,要么被迫把模型压扁。现在每类内容都保留自己的表达能力,页面只在需要时做统一入口和统一交互。
取舍分析
把本地内容拆成多份目录,代价是维护成本会更显性。新增一类实体时,要补模型、卡片、列表、详情,有时还要补搜索和收藏规则;相比“整个应用只维护一份 JSON”,前期确实多了几步。但这种成本换来的是长期可维护性。赛事有自己的时间状态和比赛明细,装备有品牌和类目,球员有队伍和项目分类,硬塞进同一张表只会把字段设计搞得越来越别扭。
另一处取舍在“本地目录到底只做兜底,还是参与日常体验”。这个项目显然选择了后者。首页列表、搜索结果、球员动态都默认依赖本地目录起步,联网刷新只是逐步替换内容,而不是从零生成页面。这种设计让首屏体验更稳,也让调试和演示环境更可控,代价则是本地数据需要持续维护,不能写成一次性的占位文本。
如果后续真的接上完整后端,我也不建议马上删除这层目录。更现实的做法是继续保留“冷启动可看、弱网可用、远程增量覆盖”的能力,因为移动端最常见的问题恰恰不是接口完全不存在,而是偶发失败、字段不齐和局部刷新不同步。本地目录在这里承担的是产品韧性,而不是开发阶段的临时脚手架。
设计落点
- 本地目录按资讯、赛事、装备、球员拆分,页面各取所需,不被一张巨型表拖住。
- 首页、球员页、搜索页都优先消费远程状态,但本地目录随时可顶上,保证首屏和离线体验。
- 稳定 id 是历史记录、收藏、详情跳转和搜索结果复用的前提。
- 搜索入口统一,底层模型分治,这样既保留了读者侧的一致交互,也保留了工程侧的实体边界。
易踩坑
- 不要把列表下标当 id,刷新排序或切换远程数据后,收藏和历史马上会串位。
- 不要让远程数组直接完全覆盖本地数组,否则只要接口返回不完整,搜索和详情就会缺项。
- 不要把所有内容压成一份万能模型。赛事、装备、球员字段差异很大,后期会越来越难维护。
- 不要把本地目录写成纯展示文案。只要它承担真实兜底,就必须能支撑跳转、搜索和详情页。
验证方式
- 冷启动应用,不手动刷新,确认首页轮播、金刚区和最新资讯已经可用。
- 断网后进入赛事、装备、球员相关页面,确认列表不是空白,详情页可以正常打开。
- 搜索
YONEX、全英、石宇奇这类关键词,确认不同实体仍能被命中。 - 从首页或搜索页进入详情,再查看历史/收藏相关入口,确认同一条内容能被稳定关联。
参考资料
- [ArkUI 状态管理总览(官方文档)](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-state-management-overview-V5)
- [ArkUI 列表与滚动容器(官方文档)](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-container-list-V5)
小结
第 08 篇想说明的重点很简单:本地内容目录不是为了“让项目看起来像有数据”,而是为了让首页、搜索、详情、球员动态这些真实页面在弱网和冷启动条件下仍然成立。只要把实体边界、稳定 id 和远程覆盖策略设计清楚,本地目录就能从临时演示数据,变成一层真正提升产品稳定性的工程能力。