Hvigor+Node.js:多产品图标引用链一致性检查【鸿蒙心迹】
同一个 HarmonyOS 应用开始区分普通版和专业版后,发布工作里最容易被忽略的变化不是新增了几个页面,而是同一工程会产生不同产品配置。开发时看到的桌面图标,不一定能代表另一个 product 的最终图标;默认入口跑得通,也不证明专业版所引用的资源文件仍然存在。
考虑一个演示工程 IconMatrix:default 和 readerPro 两个构建产品都声明了自己的图标和名称。某次资源整理后,readerPro 仍引用 $media:pro_icon,但 AppScope/resources/base/media/pro_icon.png 没有进入本地源文件集合。页面运行逻辑可能完全正常,可交付包的外观配置却已经留下风险。
本文不把这个情形写成平台“审核拒绝”的亲历故事。我们设计的是一条构建前资源引用审计,用固定样本演示 REL-ICON-061 检查任务:两个产品、六项规则、五项通过、一项失败,最终状态为 BLOCKED。这只是业务门禁判断,不是 HarmonyOS 应用市场的正式审核结论。

一、先界定验证对象,不让脚本越权
华为的多目标构建文档明确介绍了工程级 build-profile.json5 中 app.products 可按产品配置 icon、label,未配置时存在默认值继承关系。文档也说明,真正的产物仍取决于目标 product、模块 target 和构建工具处理的优先级。因此脚本应区分两个层次:源配置引用是否可解析与最终发布产物是否符合预期。
前者可以在构建前静态检查;后者必须通过正式构建、安装和渠道验证。本文只实现前者,不尝试解析签名包内部资源表,也不声称扫描结果等于实际桌面效果。这样主动缩小目标,反而更容易让团队采用,因为门禁结论清楚、失败也可复现。
1. 一项图标引用,实际跨了三处文件
本例将核查范围锁定为工程级 build-profile.json5 的两个 product,以及 AppScope/resources/base/media/ 中的图标源文件和 AppScope/resources/base/element/string.json 中的名称项。它不自动解析所有变体资源目录、不合并 app.json5 / module.json5 / product 的复杂优先级,也不覆盖多 HAP 的 launcher entry 图标。
这个前提必须公开,否则开发者会误以为静态脚本已经完整模拟 Hvigor。对于图标使用 SVG、分层图标或资源变体的项目,应扩展资源解析器,而不是让简单检查器悄悄放行未知类型。
二、把产品配置变成可检查的数据
1. 示例配置先固定两条 product 记录
要解决的第一个问题是:普通版和专业版不能共享一个模糊的“当前图标”变量。下面是 build-profile.json5 中与本案例有关的局部片段,省略了签名、模块 target 和其他工程字段;正式项目不能把它当作完整构建文件直接覆盖。
{
"app": {
"products": [
{
"name": "default",
"icon": "$media:default_icon",
"label": "$string:default_name"
},
{
"name": "readerPro",
"icon": "$media:pro_icon",
"label": "$string:pro_name"
}
]
}
}
示例还约定 string.json 包含 default_name 和 pro_name 两条资源;default_icon.png 文件存在,pro_icon.png 则作为故意遗漏的测试样本。把这些前提写在检查之前,才能说明为什么结果必然是 5/6,而不是故作神秘地贴一张“检测失败”截图。
2. 三项规则分别解释三种失败
对每个 product 都执行三步:第一,图标引用必须符合本轮支持的 $media:<name> 格式;第二,指定源目录里至少存在一份 <name>.png;第三,label 引用的 $string:<name> 必须能在字符串资源表里找到。两个产品各三项,总共六项。这里的 png 限制是本检查器的输入合同,不是 HarmonyOS SDK 只支持 PNG 图标的声明。
检查器需要返回结构化记录,而不是一遇到缺文件就抛异常退出。只有这样,CI 和展示页才能从同一份报告派生 PASS、FAIL 和人类可读原因,避免开发者只拿到一个无上下文的失败退出码。
三、用 Node.js 实现只读审计,不修改工程
第二个问题是怎样读取 JSON5。Node.js 内置 JSON.parse 不能可靠处理 JSON5 的注释和尾随逗号,所以示例明确依赖标准 json5 包:在工程开发依赖中安装它,再用 node scripts/icon-audit.mjs 运行。不要通过 eval 执行配置文件,也不要随手删注释后假装完成了通用 JSON5 解析。
下面的模块从工程根目录读入配置,扫描两个指定产品的六项规则,返回数组。它不改写配置、不删除文件、也不做网络请求,因此失败后可以反复检查同一源树。
// scripts/icon-rules.mjs;安装开发依赖:npm install --save-dev json5
import { readFileSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import JSON5 from 'json5';
function row(product, rule, ok, detail) {
return { product, rule, ok, detail };
}
export function inspectIcons(root) {
const profile = JSON5.parse(readFileSync(join(root, 'build-profile.json5'), 'utf8'));
const strings = JSON.parse(readFileSync(
join(root, 'AppScope/resources/base/element/string.json'), 'utf8'
));
const keys = new Set(strings.string.map((item) => item.name));
const products = (profile.app.products ?? []).filter(
(p) => p.name === 'default' || p.name === 'readerPro'
);
const result = [];
for (const p of products) {
const iconValid = /^\$media:[A-Za-z_][\w]*$/.test(p.icon ?? '');
const iconName = iconValid ? p.icon.slice('$media:'.length) : '';
const iconPath = join(root, 'AppScope/resources/base/media', `${iconName}.png`);
const labelValid = /^\$string:[A-Za-z_][\w]*$/.test(p.label ?? '');
const labelName = labelValid ? p.label.slice('$string:'.length) : '';
result.push(row(p.name, 'ICON_SYNTAX', iconValid, p.icon ?? 'missing'));
result.push(row(p.name, 'ICON_FILE', iconValid && existsSync(iconPath), iconPath));
result.push(row(p.name, 'LABEL_KEY', labelValid && keys.has(labelName), p.label ?? 'missing'));
}
return { products: products.length, rows: result };
}
这段代码的风险边界也应当讲清楚。其一,资源目录是本 Demo 的约定路径,不是通用的资源优先级解析引擎;其二,existsSync 只证明文件存在,不证明图片尺寸、编码、透明通道或最终产物质量;其三,若 products 配置本身丢失,这段函数会返回空记录。生产门禁还应强制检查产品清单与预期数量吻合,不能接受零项检查却显示“全部通过”。
四、CI 应当只接受完整的六项报告
接下来要解决的是“检查没跑完却被当成通过”的情况。把预期产品名称、规则总数和失败数量一起写进退出条件,任何异常都转成明确的非零退出码。这里使用的是 Node.js 标准输出和退出码,不依赖虚构的 Hvigor 官方门禁 API;项目可以把它作为构建前独立步骤调用。
// scripts/icon-audit.mjs
import { inspectIcons } from './icon-rules.mjs';
const TASK_ID = 'REL-ICON-061';
try {
const report = inspectIcons(process.cwd());
const checks = report.rows.length;
const failed = report.rows.filter((r) => !r.ok);
const passed = checks - failed.length;
const blocked = report.products !== 2 || checks !== 6 || failed.length !== 0;
const status = blocked ? 'BLOCKED' : 'PASS';
console.log(`IconMatrix task=${TASK_ID} products=${report.products} checks=${checks}`);
console.log(`IconMatrix pass=${passed} fail=${failed.length} status=${status}`);
for (const item of failed) console.error(`${item.product}/${item.rule}: ${item.detail}`);
process.exitCode = blocked ? 1 : 0;
} catch (error) {
console.error(`IconMatrix task=${TASK_ID} status=BLOCKED`, error);
process.exitCode = 2;
}
在本文固定的资源样本下,逻辑上应生成 products=2 checks=6、pass=5 fail=1 status=BLOCKED,失败规则是 readerPro/ICON_FILE,对应丢失的 pro_icon.png。这是一组演示用预期输出,没有声称实际执行了目标工程的发布构建。团队接入 CI 时,建议把扫描报告保存为独立工件,并记录源提交号、产品组合和脚本版本,使失败具有可追溯性。
五、让开发者能在界面上解释失败
只保留控制台错误,往往让非开发角色误以为所有产品都坏了。本例另设 IconAuditPage.ets,展示一张由模型数据构造的只读报告:任务 REL-ICON-061、产品 default/readerPro、检查 5/6、状态 BLOCKED,并在异常详情中单独指出 readerPro 缺少 pro_icon.png。
下面是可以嵌入现有 ArkUI 页面的一段核心视图。注意它只是报告的展示层静态快照,尚未接入 Node.js 扫描结果的文件导入或 IPC。若未来需要同步数据,先设计带版本号的 JSON 报告,再做严格解析,不能让演示中的常量冒充实时构建事实。
@Entry
@Component
struct IconAuditPage {
@State taskId: string = 'REL-ICON-061';
@State passed: number = 5;
@State total: number = 6;
@State blocked: boolean = true;
build() {
Column({ space: 18 }) {
Text('IconMatrix · 图标引用链').fontSize(24).fontWeight(FontWeight.Bold)
Text(`任务 ${this.taskId}`)
Text(`产品 default / readerPro`)
Text(`检查 ${this.passed}/${this.total}`)
Text(this.blocked ? 'BLOCKED · 需要修复' : 'PASS')
.fontColor(this.blocked ? Color.Red : Color.Green)
Text('readerPro / ICON_FILE')
Text('缺失 pro_icon.png')
Text('演示报告 · 非真实上架审核结果').fontSize(12)
}.padding(24).width('100%')
}
}
这里的状态生命周期与后台构建完全解耦:脚本失败会通过 CI 的非零退出码阻断流程;ArkUI 展示页只帮助理解失败,不负责决定是否签名或提交。页面销毁无需维持文件监听器,等真正接入报告推送后再成对注册、取消订阅,防止重新进入页面重复消费同一个失败通知。

六、调试顺序与发布前收口
演示阶段应优先固定文件树:default_icon.png 存在、pro_icon.png 不存在、default_name 和 pro_name 都存在。然后以相同提交内容连续运行校验,确认总数始终为六,而不是因为过滤掉一个 product 变成三项却显示成功。补回缺失文件后再执行第二次校验,才有资格预期状态转成 PASS。
对真正的发布工程,还要增加文件类型检查、图片可解析性、产物中有效资源与实际桌面显示核验。特别是多 product 的继承与覆盖问题,不能只依赖 AppScope 路径推导;需要参照目标构建产品的配置优先级、安装测试及工具链输出。图中的图标卡片仅是为了说明哪个产品失败,不是实际安装截图。

完成这一轮设计,我们最终得到的不是“系统保证会拒绝错误图标”的结论,而是一条更朴素、可以逐步扩展的工程规则:声明要有源文件,产品数量要完整,检查总数要固定,失败应能定位到具体 product。这四件事建立起来,发布前的资源风险就能从碰运气转成可解释的日常检查。
官方依据包括华为 多目标与多产品构建指南、工程级 build-profile.json5 说明 及 Stage 模型图标与标签配置。其中产品字段、资源引用属于官方机制;ICON_SYNTAX、ICON_FILE、LABEL_KEY 和六项门禁则是本篇自定义策略,不应误写成 HarmonyOS 审核政策。
更多推荐


所有评论(0)