HarmonyOS 7 HSP + Localization Kit:共享组件资源导出、语言回退与相对路径失效诊断【鸿蒙心迹】

发布时间:2026/10/1 14:55:29
HarmonyOS 7 HSP + Localization Kit:共享组件资源导出、语言回退与相对路径失效诊断【鸿蒙心迹】 把公共设置面板从 entry 搬进 HSP 后中文环境一切正常切到英文标题退回中文图标还变成宿主模块里的同名文件。组件代码没有报错问题却已经写在资源边界里。这次整理的是一个叫LocaleScope Lab的小工程。它有一个uikitHSP向 entry 和 feature 两个 HAP 提供网络重试面板。迁移前组件在宿主里用相对路径读图字符串则由宿主传入迁移后为了省参数我把字符串和图片一起放进 HSP却保留了旧的相对路径写法。结果很有迷惑性。entry 中正好也有retry.png所以图片“能显示”只是显示错了。zh_CN资源齐全中文测试也通过en_US少了dialog_retry运行时回落到 base 的中文文案。只有切到英文并从 feature 页面打开面板两个问题才同时出现。本轮诊断编号为loc_20261001_05。资源审计最初统计 base 42 个字符串、en_US 41 个缺失键为dialog_retry另外发现 2 处相对路径。修复后状态为PASS15:24 在en-US环境运行标题显示Network retryHSP 图标来源为uikit语言回退数量为 0。一、HSP 里的资源属于组件不属于调用方HSP 用于运行时复用代码和资源与宿主应用一起发布。组件既然放进 HSP它自己的图片、字符串和配置也应该在 HSP 内部保持高内聚。调用方不应知道组件内部资源名更不应该靠目录层级去猜图片位置。旧代码看起来只是普通路径Image(../../resources/base/media/retry.png)。问题在于相对路径最终可能按调用模块解释。组件从 entry 移到 HSP 后目录结构已经不是原来的所有权边界宿主恰好存在同名文件时错误会被“成功加载”掩盖。官方 HSP 指南也明确建议在组件内部使用$r或$rawfile访问当前模块资源避免相对路径引用错误。另一个常见做法是让宿主直接写$r(app.string.uikit_dialog_retry)。它能工作却把 HSP 内部命名暴露给每个调用方。资源改名以后所有宿主都要跟着改HSP 的封装就只剩一层目录。当前方案由 HSP 导出资源门面。下面这段代码解决的是“调用方知道内部资源名”和“相对路径落到宿主模块”的问题。// uikit/src/main/ets/resources/SharedRes.etsexportclassSharedRes{staticretryIcon():Resource{return$r(app.media.retry_panel_icon)}staticretryTitle():Resource{return$r(app.string.dialog_retry)}staticretryAction():Resource{return$r(app.string.action_retry)}}// uikit/index.etsexport{SharedRes}from./src/main/ets/resources/SharedRes页面组件同样位于 HSP 内部直接使用SharedRes.retryIcon()和SharedRes.retryTitle()。entry 若确实需要跨包访问某个资源也只依赖门面方法不依赖dialog_retry这个内部名字。返回Resource而不是已经解析好的字符串还有一个好处资源选择可以跟随当前配置。若把中文字符串在模块初始化时解析成普通string并缓存语言切换后组件仍可能拿到旧值。二、语言回退能保证有值却不能保证值是对的HarmonyOS 资源匹配会按照偏好语言和限定词寻找最合适的目录找不到匹配资源时会回到默认 base 资源。因此en_US少一个键不一定崩溃它可能安静地显示 base 中的中文。这正是 LocaleScope Lab 难查的地方。开发机默认中文base 也是中文测试一直通过。切到英文以后按钮“Retry”正常标题却还是“网络重试”。从 API 角度看资源解析成功从产品角度看这已经是本地化缺陷。我的处理原则是base 必须完整目标语言目录也要通过覆盖率检查。允许回退的键必须进入白名单例如品牌名或法规要求保留的专有名词其他缺失一律在构建前报错。下面的脚本解决的是“运行时回退掩盖缺失翻译”的问题。它比较 base 与 en_US 的字符串键并同时扫描 ArkTS 中遗留的../resources相对路径。// tools/audit-hsp-resources.tsconstbasereadStringKeys(uikit/src/main/resources/base/element/string.json)constenUSreadStringKeys(uikit/src/main/resources/en_US/element/string.json)constfallbackAllowListnewSet([product_name])constmissing[...base].filter(key!enUS.has(key)!fallbackAllowList.has(key))constrelativeRefsscanArkTS(uikit/src/main/ets,sourcesource.includes(../resources/)||source.includes(..\\resources\\))constreport{auditId:loc_20261001_05,baseKeys:base.size,enUSKeys:enUS.size,missing,relativePathCount:relativeRefs.length,result:missing.length0relativeRefs.length0?PASS:BLOCKED}console.log(JSON.stringify(report,null,2))if(report.result!PASS)process.exit(2)第一次执行输出baseKeys42、enUSKeys41、missing[dialog_retry]、relativePathCount2构建状态为BLOCKED。补齐英文资源并改用资源门面后base 与 en_US 都是 42 个键相对路径为 0。这个脚本不需要解析整个编译产物重点是把最容易静默失败的规则提前。正式工程还可以继续检查占位符数量、复数格式、图片密度限定词和深色模式资源但不能把所有语言都机械要求 100% 相同是否允许回退要结合产品支持范围定义。三、运行时诊断要保留模块来源资源门面修好后我又加了一层运行时诊断。原因很简单静态脚本只能看到源码无法证明实际运行时拿到的是哪个模块的资源。如果宿主与 HSP 存在同名资源仅看界面截图很难分辨来源。LocaleScope Lab 的ResourceProbe记录资源的bundleName、moduleName和 id并通过当前上下文的 ResourceManager 解析字符串。解析失败时保留错误码例如找不到匹配资源时关注9001004而不是统一显示“资源异常”。这段代码解决的是“页面看见错误文案却不知道来自哪个模块”的问题。import{BusinessError}fromkit.BasicServicesKitexportinterfaceResourceProbeResult{name:stringmoduleName:stringvalue?:stringerrorCode?:number}exportasyncfunctionprobeString(context:Context,name:string,resource:Resource):PromiseResourceProbeResult{try{constvalueawaitcontext.resourceManager.getStringValue(resource)return{name,moduleName:resource.moduleName??unknown,value}}catch(error){consterrerrorasBusinessErrorreturn{name,moduleName:resource.moduleName??unknown,errorCode:err.code}}}如果直接使用资源名动态查询也可以调用getStringByName()。不过 HSP 对外仍应优先导出受控的Resource避免宿主拼写内部名字。动态名称查询更适合诊断页和资源审计工具不适合让业务页面到处散落字符串常量。ResourceProbe只在调试构建显示详细模块信息正式版本不把内部模块结构暴露到用户界面。日志中也不记录用户输入只记录资源名、模块和解析结果。DevEco Studio 图中左侧可以看到uikitHSP 的SharedRes.ets、base 与 en_US 资源目录以及审计脚本中间代码使用$r(app.string.dialog_retry)导出资源右侧模拟器显示en-US / Network retry底部 HiLog 则记录moduleuikit、fallback0和auditPASS。四、语言切换后不要继续用旧字符串缓存另一个真实问题发生在系统语言切换以后。应用收到配置更新页面重新构建了HSP 里的单例却仍缓存着之前解析的普通字符串。结果新页面一半是英文一半还是中文。我把缓存分成两类Resource描述可以复用解析后的本地化字符串不跨配置缓存。确实需要缓存时必须把语言、色彩模式等配置摘要放进 key并在配置更新时递增localeEpoch。下面的实现解决的是“HSP 单例跨语言继续返回旧值”的问题。exportclassLocalizedTextCache{privateepoch:number0privatevalues:Mapstring,{epoch:number,value:string}newMap()invalidate():void{this.epochthis.values.clear()}asyncresolve(context:Context,key:string,res:Resource):Promisestring{constcachedthis.values.get(key)if(cached?.epochthis.epoch)returncached.valueconstvalueawaitcontext.resourceManager.getStringValue(res)this.values.set(key,{epoch:this.epoch,value})returnvalue}}// EntryAbility.onConfigurationUpdate 中通知 uikit// localizedTextCache.invalidate()当前 Demo 切换语言后不自动保留旧面板实例而是关闭弹层、清空解析值再按新配置重新打开。实际产品若必须无感切换需要让组件状态与本地化文本分离不能因为清缓存把用户填写的表单也一起清掉。五、最终验收不是“英文能显示”修复后的运行页把三个证据放在一起当前区域为en-US标题解析结果为Network retry资源来源模块为uikit。审计卡片显示 base 42、en_US 42、缺失键 0、相对路径 0诊断编号仍是loc_20261001_05。15:24 的日志固定为[LocaleScope] auditloc_20261001_05 base42 en_US42 [LocaleScope] missing0 relativePaths0 resultPASS [LocaleScope] localeen-US moduleuikit keydialog_retry [LocaleScope] valueNetwork retry fallback0我还补了两个反向测试。第一把dialog_retry从 en_US 删除脚本必须在构建阶段返回BLOCKED第二把图片改回相对路径扫描结果必须变成relativePaths1。只有“错误确实能被门禁拦住”这套检查才不是一张永远绿色的装饰报表。六、HAR 转 HSP 时容易漏掉的边界第一HAR 是编译态复用代码和资源会跟随使用方编译HSP 是运行时复用资源所有权和跨模块访问更值得明确。迁移不能只改模块类型和依赖声明。第二组件内部使用$r对外需要的资源通过门面导出。不要把内部资源名字写进每个宿主也不要依靠宿主提供同名资源“补齐”组件。第三base 目录必须有默认资源。语言目录缺失时系统回退可以避免空值却不能替代翻译覆盖率检查。第四图片、rawfile 与字符串的策略不同。大文件是否适合放入 HSP、是否会被多个功能同时使用需要结合包体和运行时访问决定不能一概迁移。第五资源错误码要分类。9001003是资源名无效9001004是没有匹配资源9001006涉及循环引用。把它们全捕获成“加载失败”会让真正的修复方向消失。第六配置更新不仅是语言。色彩模式、屏幕密度与设备形态都可能改变资源匹配缓存 key 不能只写zh或en就认为覆盖全部情况。七、共享模块最怕“碰巧能用”这个问题最值得复盘的地方是所有错误最初都能显示出一个结果相对路径碰巧命中宿主同名图片缺失英文碰巧回退到中文旧缓存碰巧在默认语言下没有变化。没有崩溃反而更容易带到提测阶段。改完以后HSP 只对外暴露资源门面构建脚本检查语言键和相对路径运行时探针确认模块来源配置更新负责失效缓存。loc_20261001_05最终的 PASS 不只是“页面看起来对”而是能说明资源从哪里来、为什么选中这份语言、错误发生时会在哪一层被拦住。共享组件真正的稳定性不是宿主帮它把缺口补上而是换一个宿主、换一种语言、换一次配置以后它仍然清楚地拥有自己的资源。参考资料HarmonyOS应用程序包术语HAR/HSPHarmonyOS应用内 HSP 开发与资源访问Localization KitResourceManager APIHarmonyOS多语言资源匹配规则