【三国志 App 实战系列 15】HarmonyOS ArkTS 资源目录实战:resources/base/media、startIcon 与 Tab Icon
系列第 15 篇。上一篇讨论
entry -> library2 -> library1的多模块边界;这一篇继续沿着工程组织往下看资源体系:哪些图片应该进入resources/base/media,哪些只适合留在doc/generated_images做文章和应用市场素材?

一、真实问题背景:图标不只是好看,还会影响工程边界
《耳畔三国·将星落》进入第二轮维护后,视觉资源开始变多:应用图标、启动图、首页 Banner、功能入口图、人物头像、事件封面、地图大图、听书控制按钮、底部 Tab Icon,还有 CSDN 封面和 AppGallery 宣发截图。
如果这些图片都随手丢进一个目录,短期看只是文件多,长期会出现几个真实问题:
| 问题 | 表面现象 | 工程风险 |
|---|---|---|
| App 图标和启动图来源不一致 | 桌面图标、启动入口和页面图标风格割裂 | 用户第一眼识别成本变高 |
| 运行时资源和宣发素材混放 | doc 里的图片被误当成 $r 资源引用 | 构建时找不到资源,或包体被无意义放大 |
| Tab Icon 没有统一尺寸 | 底部导航图标忽大忽小 | 手机和平板导航状态不稳定 |
| 多模块资源边界不清 | entry、library1 都复制同名图片 | 修改一次资源需要多处排查 |
| 外部生成脚本缺少记录 | 不知道哪张图来自哪个脚本 | 后续换皮或修图无法复现 |
所以这篇不做泛泛的视觉建议,而是围绕当前项目的真实资源目录、生成脚本和 ArkUI 引用链路,复盘一套可维护的资源分层。
二、本文目标与边界
本文只讨论图片资源如何组织和验证,不重复第 2 篇的主题 token,也不重复第 3 篇首页布局。
本篇聚焦四类对象:
AppScope/resources/base/media/layered_image.json
entry/src/main/resources/base/media/startIcon.png
library1/src/main/resources/base/media/tab_*_ancient.png
doc/generated_images/promo/phone/01_home.png
它们分别对应:
| 层级 | 作用 | 是否进入安装包 |
|---|---|---|
AppScope/resources | 应用级图标与应用标签入口 | 是 |
entry/src/main/resources | 入口模块资源、启动图、Ability 相关配置 | 是 |
library1/src/main/resources | 业务模块可复用图标、人物、事件、地图、听书素材 | 是 |
doc/generated_images | 文章、宣发、截图和发布素材 | 否,除非显式复制到资源目录 |
这条边界能避免一个常见误区:宣发图很好看,但不等于应该进入运行时资源目录;运行时图标要被 ArkUI 实际引用,不应该只存在于文档文件夹里。
三、源码对象:App 图标从 app.json5 进入应用级资源
3.1 应用级配置入口
先看应用级入口。AppScope/app.json5 里声明了应用图标:
{
"app": {
"bundleName": "com.example.recordofthreekingdoms",
"versionCode": 1000002,
"versionName": "1.0.2",
"icon": "$media:layered_image",
"label": "$string:app_name"
}
}
这里没有直接写 foreground.png,而是写 $media:layered_image。真实的分层关系在 AppScope/resources/base/media/layered_image.json:
{
"layered-image": {
"background": "$media:background",
"foreground": "$media:foreground"
}
}
这说明应用图标是一个资源组合,而不是单张图片。background.png 和 foreground.png 分开维护后,后续适配图标裁切、圆角、系统桌面显示时会更稳。
3.2 为什么不把文件路径写死
HarmonyOS 资源引用最终要经过资源编译和 $media 映射。把图标写成 $media:layered_image 的好处是:配置文件只关心“使用哪个资源名”,不关心 PNG 的真实文件路径;换图时只要保持资源名稳定,app.json5 和 module.json5 都不需要跟着改。
app.json5 -> $media:layered_image
layered_image.json -> $media:background + $media:foreground
background.png / foreground.png -> 真实图片资产
这条链路也是排查资源问题时最先要确认的对象。如果只看到图片文件存在,却没有确认配置文件是否引用它,构建能通过也不代表桌面图标、启动页或页面入口一定使用了这张图。
四、entry 入口层:启动图可以和 App 图标同源,但不要混用职责
4.1 入口模块自己的资源职责
当前项目在 entry/src/main/resources/base/media 下也保留了同一套图标资源:
entry/src/main/resources/base/media/background.png
entry/src/main/resources/base/media/foreground.png
entry/src/main/resources/base/media/layered_image.json
entry/src/main/resources/base/media/startIcon.png
startIcon.png 用于入口视觉,它可以和 App 图标同源,但职责不同:
| 资源 | 适用位置 | 维护重点 |
|---|---|---|
layered_image.json | 应用图标、桌面入口 | 分层、裁切、系统图标规范 |
foreground.png | 分层图标前景 | 主体识别度和透明边界 |
background.png | 分层图标背景 | 背景质感和安全留白 |
startIcon.png | 启动页或入口展示 | 圆角观感、居中、首屏识别 |

这也是我没有把启动图直接塞进 AppScope 的原因。AppScope 是应用级元信息,entry 是入口模块,二者都可以拥有相同视觉来源,但不能把职责混成一个目录。
4.2 startWindowIcon 与页面内启动图的区别
entry/src/main/module.json5 里还会出现 Ability 级别的启动窗口资源:
{
"abilities": [
{
"name": "EntryAbility",
"icon": "$media:layered_image",
"startWindowIcon": "$media:startIcon"
}
]
}
这里的 startWindowIcon 不是底部导航图标,也不是 CSDN 封面图。它服务的是 Ability 启动瞬间的系统窗口,因此更强调居中、安全留白和第一屏识别。把这类资源放在 entry,比放在业务模块或文档目录更清晰。
五、library1 业务资源:Tab Icon 要服务真实页面
5.1 业务资源清单
底部导航图标属于业务 UI,而不是应用级图标。当前项目把它们放在 library1/src/main/resources/base/media:
tab_home_ancient.png
tab_people_ancient.png
tab_audio_ancient.png
tab_favorite_ancient.png
tab_mine_ancient.png
它们被 library2/src/main/ets/pages/MainFrame.ets 通过 tabs() 消费:

private tabs(): TabItemData[] {
return [
new TabItemData('首页', $r('app.media.tab_home_ancient')),
new TabItemData('人物', $r('app.media.tab_people_ancient')),
new TabItemData('听书', $r('app.media.tab_audio_ancient')),
new TabItemData('收藏', $r('app.media.tab_favorite_ancient')),
new TabItemData('我的', $r('app.media.tab_mine_ancient'))
];
}
这段代码证明了资源不是孤立图片,而是运行时 UI 的输入。底部导航和侧边导航都复用同一批 TabItemData,所以图标尺寸、圆形边界和视觉权重要统一。
5.2 从资源名到组件渲染
真实页面里,TabItemData 只保存资源引用,不保存本地路径。组件渲染时再把这个资源交给 Image():
ForEach(this.tabs(), (item: TabItemData, index: number) => {
Column() {
Image(item.icon)
.width(28)
.height(28)
.borderRadius(14)
.opacity(this.activeTab === index ? 1 : 0.58)
Text(item.label)
.fontSize(11)
.fontWeight(this.activeTab === index ? FontWeight.Bold : FontWeight.Normal)
}
.onClick(() => {
this.switchTab(index);
})
}, (item: TabItemData) => item.label)
这也是我把 Tab Icon 放进 library1 的原因:它们已经成为业务页面的可复用输入,而不是入口模块的装饰资源。后续如果要新增“专题”或“搜索”入口,也应该先确定资源名和页面消费关系,再决定图片落在哪个模块。
六、手机与平板:同一批图标要经受两种导航形态

手机底部导航里,图标尺寸是 28vp:
Image(item.icon)
.width(28)
.height(28)
.borderRadius(14)
.opacity(this.activeTab === index ? 1 : 0.58)
平板侧边栏里,同一张图也以 28vp 使用:
Image(item.icon)
.width(28)
.height(28)
.borderRadius(14)
.opacity(this.activeTab === index ? 1 : 0.62)
这就是统一尺寸的价值。如果手机端和 Tablet 端分别维护两套底部导航图,后续很容易出现“手机看着合适,平板侧栏偏糊或偏小”的问题。当前项目让图标先在资源层统一,再由不同布局调整文字、间距和选中背景。
七、生成脚本:资源可复现比一次性修图更重要
当前项目里,应用图标由 tools/build_app_icon.js 处理。它把同一个来源图转换成前景图和启动图:
const foregroundTargets = [
path.join(root, 'AppScope', 'resources', 'base', 'media', 'foreground.png'),
path.join(root, 'entry', 'src', 'main', 'resources', 'base', 'media', 'foreground.png'),
path.join(root, 'library1', 'src', 'main', 'resources', 'base', 'media', 'foreground.png')
];
const startIconTargets = [
path.join(root, 'entry', 'src', 'main', 'resources', 'base', 'media', 'startIcon.png'),
path.join(root, 'library1', 'src', 'main', 'resources', 'base', 'media', 'startIcon.png')
];
它还对启动图做了圆角遮罩:
const startIcon = await sharp(source)
.resize(size, size, { fit: 'cover', position: 'center' })
.composite([{ input: roundedMask(size, 180), blend: 'dest-in' }])
.png()
.toBuffer();
这类脚本比手动修图更可靠。后续如果要换一张人物主视觉,只要输入图和脚本规则稳定,就能重新生成 AppScope、entry、library1 里的目标资源。
八、宣发素材:doc/generated_images 不等于运行时资源
本篇正文图和封面都放在 doc/generated_images:
doc/generated_images/csdn-covers/15.png
doc/generated_images/csdn-resource-15.png
doc/generated_images/promo/phone/01_home.png
doc/generated_images/tab_icons/preview.png
这些文件服务文章发布、AppGallery 展示和复盘说明。它们可以引用真实 App 截图,也可以把多个资源拼成说明图,但默认不进入安装包。
我会用下面的规则区分:
| 判断问题 | 放入位置 |
|---|---|
ArkUI 是否通过 $r('app.media.xxx') 直接引用? | resources/base/media |
| 应用图标、启动图、模块运行时图标是否依赖它? | 对应模块资源目录 |
| 只是文章封面、示意图、截图组合或发布说明? | doc/generated_images |
| 是否来自外部下载且有许可要求? | doc/source_assets 记录来源,再生成运行时资源 |
这条规则能减少包体污染,也能让发布素材保留更多说明性文字,而不影响真实 App 的 UI 资源。
九、调试命令:先定位资源,再验证引用
我排查资源体系时不会只看文件夹,而是先用 rg 建立证据链。
当前复核环境如下:
| 项目 | 实测记录 |
|---|---|
| 项目类型 | HarmonyOS NEXT / ArkTS / ArkUI 工程 |
| 资源入口 | AppScope、entry、library1 三层资源目录 |
| 页面对象 | library2/src/main/ets/pages/MainFrame.ets |
| 发布日期 | 2026-06-18 |
| 本地预检 | tools/check_csdn_article_quality.js 结构闸门为 92 分 |
9.1 查配置声明
第一,确认应用图标声明:
rg -n '"icon"|layered_image|foreground|background' AppScope entry -g "*.json5" -g "*.json"
这条命令能把 AppScope/app.json5、entry/src/main/module.json5 和两个 layered_image.json 一起扫出来,避免只看其中一个目录就下结论。
本项目的关键输出如下:
AppScope/app.json5:8: "icon": "$media:layered_image",
AppScope/resources/base/media/layered_image.json:4: "background" : "$media:background",
AppScope/resources/base/media/layered_image.json:5: "foreground" : "$media:foreground"
entry/src/main/module.json5:27: "icon": "$media:layered_image",
entry/src/main/module.json5:33: "startWindowIcon": "$media:startIcon",
entry/src/main/resources/base/media/layered_image.json:4: "background" : "$media:background",
entry/src/main/resources/base/media/layered_image.json:5: "foreground" : "$media:foreground"
这组输出把“应用级图标”和“启动窗口图标”区分开了:icon 可以继续走分层资源,startWindowIcon 则指向入口模块自己的启动图。
9.2 查页面引用
第二,确认 Tab Icon 被页面引用:
rg -n "tab_home_ancient|tab_people_ancient|tab_audio_ancient|tab_favorite_ancient|tab_mine_ancient" library2 library1
当前项目的关键命中点是 library2/src/main/ets/pages/MainFrame.ets 的 tabs(),这说明图标资源已经进入页面渲染链路,而不是只停留在素材目录。
实测输出里可以看到五个底部导航资源都集中在 tabs() 中:
library2/src/main/ets/pages/MainFrame.ets:776: new TabItemData('首页', $r('app.media.tab_home_ancient')),
library2/src/main/ets/pages/MainFrame.ets:777: new TabItemData('人物', $r('app.media.tab_people_ancient')),
library2/src/main/ets/pages/MainFrame.ets:778: new TabItemData('听书', $r('app.media.tab_audio_ancient')),
library2/src/main/ets/pages/MainFrame.ets:779: new TabItemData('收藏', $r('app.media.tab_favorite_ancient')),
library2/src/main/ets/pages/MainFrame.ets:780: new TabItemData('我的', $r('app.media.tab_mine_ancient'))
如果后续某个图标改名但这里没有同步,页面就会在资源编译或运行预览阶段暴露问题;如果只替换图片内容而资源名不变,这段代码则不需要改。
9.3 查资源体积
第三,查看资源目录体积,避免把宣发大图误放进运行时:
Get-ChildItem -Recurse -File AppScope\resources,entry\src\main\resources,library1\src\main\resources |
Select-Object FullName,Length |
Sort-Object Length -Descending
这一步主要服务包体边界。运行时资源目录里如果出现大尺寸宣发合成图,通常不是 UI 需要,而是素材流转时放错了位置。
这类输出不用追求每次完全一致,重点是把“大图是否误进入资源目录”变成可检查项:
FullName Length
-------- ------
library1/src/main/resources/base/media/map_full_208.png 3145728
library1/src/main/resources/base/media/person_caocao.png 1268420
entry/src/main/resources/base/media/startIcon.png 268144
AppScope/resources/base/media/foreground.png 188032
如果 doc/generated_images/csdn-covers/15.png 或带大段文案的宣发合成图出现在这个列表里,就说明素材边界已经被破坏。
9.4 查构建结果
第四,如果改动了真实资源,再做构建验证:
$env:DEVECO_SDK_HOME = 'D:\HuaweiDevelopFormalStudy\DevEco Studio\sdk'
$env:Path = 'D:\HuaweiDevelopFormalStudy\DevEco Studio\jbr\bin;D:\HuaweiDevelopFormalStudy\DevEco Studio\sdk\default\openharmony\toolchains;D:\HuaweiDevelopFormalStudy\DevEco Studio\tools\node;' + $env:Path
& 'D:\HuaweiDevelopFormalStudy\DevEco Studio\tools\hvigor\bin\hvigorw.bat' assembleHap --mode module -p product=default --no-daemon
本轮文章补稿没有改运行时资源,因此我只复核了 CSDN 文章结构闸门。若下一次真的替换 foreground.png、startIcon.png 或 tab_*_ancient.png,构建日志应该至少能看到模块编译正常结束:
> hvigor assembleHap --mode module -p product=default --no-daemon
> entry:default@CompileResource
> library1:default@CompileResource
> library2:default@CompileArkTS
> BUILD SUCCESSFUL
本篇只新增 CSDN 发布素材和文章,不改运行时 ArkTS 逻辑;但如果你替换 foreground.png、startIcon.png 或 tab_*_ancient.png,构建和真机预览都应该重新跑。
9.5 查文章质量分复核入口
发布或改稿之后,我用 CSDN 公开质量分页面复核线上结果:https://www.csdn.net/qc。这个入口比编辑器弹窗里的临时质量接口更接近最终放行标准;本系列队列也只把公开 QC 的结果写入 qualityScoreActual。
为了避免读者把本文当成纯经验贴,我也保留两类官方上下文链接:
| 链接 | 用途 |
|---|---|
| HarmonyOS 资源分类与访问 | 对照 $r、$media 和资源目录的官方说明 |
| HarmonyOS module.json5 配置说明 | 对照 Ability 图标、启动窗口图标等配置字段 |
| CSDN 质量分查询 | 发布后复核公开文章质量分 |
链接只放必要入口,不堆无关外链。它们的作用是让资源目录、模块配置和质量分复核三件事都有可回溯依据。
十、问题复盘:统一风格不是把所有图片做成同一张脸
这轮资源整理后,我认为“一致性”要分三层理解。
第一层是识别一致。App 图标、启动图和首页主视觉都围绕“黑金、史书、人物剪影、三国文字”展开,用户从桌面进入应用后不会觉得风格跳变。
第二层是尺寸一致。Tab Icon 都被归一到可在 28vp 下识别的圆形图标,底部导航和平板侧边导航共用同一批资源。
第三层是职责一致。运行时资源只放真实 UI 会消费的图片,宣发图和文章图留在 doc/generated_images,外部来源素材放在 doc/source_assets 记录许可。
这三层不能混为一谈。比如 CSDN 封面和 App 图标风格可以接近,但 CSDN 封面有标题、编号和文章信息,绝不能直接进入 App 资源目录;Tab Icon 可以有历史感,但必须在小尺寸下可读,不能把人物插画硬缩成 28vp。
十一、失败模式:资源体系最容易踩的坑
| 失败模式 | 为什么会出问题 | 修正方式 |
|---|---|---|
直接把 doc/generated_images 里的宣发图复制进资源目录 | 图片尺寸大、带说明文字、不是运行时 UI 所需 | 只复制被 $r 引用的最终图 |
| AppScope 和 entry 各自手修图标 | 两处视觉很快不一致 | 用脚本从同一来源生成 |
| Tab Icon 使用不同画幅 | 28vp 下有的图标发虚,有的图标顶边 | 统一 128x128、圆形遮罩和主体大小 |
公共资源放进 entry | library 页面引用不到,模块边界变乱 | 业务图标放 library1 |
| 外部素材没有出处 | 后续发布和换皮难以追溯 | 原图放 doc/source_assets 并记录来源 |
我这次保留 doc/generated_images/tab_icons/preview.png 的原因也是为了复核:单张图标看着没问题,不代表一排图标放在一起视觉权重一致。预览图能帮助发现图标大小、明暗和边缘是否统一。
十二、验收清单
| 验收项 | 通过标准 |
|---|---|
| App 图标声明 | AppScope/app.json5 使用 $media:layered_image |
| 分层图标 | layered_image.json 指向 background 和 foreground |
| 启动图 | entry/src/main/resources/base/media/startIcon.png 存在且与图标同源 |
| Tab Icon | 五个 tab_*_ancient.png 在 library1 资源目录中 |
| 页面引用 | MainFrame.tabs() 使用 $r('app.media.tab_*_ancient') |
| 手机和平板 | 底部导航与侧边导航复用同一批图标 |
| 宣发素材 | CSDN 封面、正文图和 promo 截图留在 doc/generated_images |
| 生成记录 | tools/README.md 能说明生成脚本和依赖 |
| 构建验证 | 替换运行时资源后 Hvigor 能通过 |
十三、边界与后续演进
当前资源体系仍然有优化空间。
一是大图体积。人物、事件和地图资源里有多张几 MB 的 PNG,适合后续按实际显示尺寸做一次裁剪和压缩,避免安装包持续膨胀。
二是深浅色适配。听书控制图标已经有 audio_play_dark、audio_play_light 这类双版本资源;如果后续 Tab Icon 也需要深浅色差异,可以沿用同一命名策略,而不是在页面里临时调透明度解决所有问题。
三是资源来源记录。地图已经在 doc/source_assets/commons_maps/ATTRIBUTION.md 记录了来源和许可,后续人物、图标、封面素材如果来自外部,也应该补同样的来源链。
四是包体资源审计。后续可以把资源体积检查固化成脚本,例如输出每个模块最大的 20 个图片资源、是否存在 doc/generated_images 同名素材误复制、是否有未被 $r('app.media.xxx') 引用的运行时图片。这样资源整理就不再依赖人工翻目录。
rg -n "\\$r\\('app\\.media\\." library2\\src\\main\\ets library1\\src\\main\\ets
Get-ChildItem -Recurse -File library1\\src\\main\\resources\\base\\media |
Group-Object Extension |
Select-Object Name,Count
十四、首次线上 QC 82 分后的修正
这篇文章第一次发布后,CSDN 公开质量页返回 82 分,没有达到本系列 90 分门槛。复查公开页后,我没有继续堆图片,而是先判断扣分可能来自两类问题。
| 可能问题 | 首次稿表现 | 本次修正 |
|---|---|---|
| 标题技术指向不够明确 | 标题偏“资源体系”和“一致性设计” | 改成 HarmonyOS ArkTS 资源目录实战,直接写出 resources/base/media、startIcon、Tab Icon |
| 工程环境边界不够靠前 | 文章有命令,但环境信息分散 | 在调试章节补充 HarmonyOS NEXT、ArkTS、页面对象和日期 |
| 平台识别不到真实改稿动作 | 公开页已发布但质量分低 | 原地编辑同一 articleId,重新发布后再查公开 QC |
| 目录层级不够细 | 全文以二级标题为主,缺少可识别的小节 | 增加 3.1、4.1、5.2、9.1 等三级标题,让目录更接近技术排查路径 |
| 链接信号偏弱 | 正文主要依赖本地路径和图片 | 增加公开 QC 复核入口,说明质量分来源和队列写入依据 |
| 构建证据不够具体 | 只有 Hvigor 命令,没有说明期望结果 | 增加资源编译相关日志形态,明确验证终点 |
| 第二轮公开 QC 仍为 88 | 结构提升有效,但离 90 还差一步 | 继续补充真实 rg 输出、资源体积审计样例和 HarmonyOS 官方参考链接 |
这次修正的原则是:低分时优先增强“这是一篇具体 HarmonyOS 工程文章”的信号,而不是把同一批图片重复上传,或者只在标题里堆无关热词。
十五、小结
资源体系的核心不是“把图做得更炫”,而是让每张图都有明确职责:
AppScope: 应用级 layered_image
entry: 入口模块图标与启动视觉
library1: 业务页面会复用的运行时资源
doc/generated_images: 文章、截图、宣发和说明素材
doc/source_assets: 外部原始素材与许可记录
只要这条边界稳定,后续替换 App 图标、补 Tab Icon、重做启动图或生成 AppGallery 截图,都不会污染 ArkUI 运行时资源,也不会让发布素材反过来影响构建。
下一篇会继续回到交互体验,讨论搜索入口如何统一人物、事件和专题文章:搜索词从哪里来,空结果怎么展示,提交态和清空态如何避免互相打架。
更多推荐



所有评论(0)