HarmonyOS NEXT ArkTS/ETS 严格模式编译错误的系统性修复方法,以及真机运行时崩溃(undefined is not callable / this 归属)与「编译通过但静默失效」(UI 不刷新 / 点不动 / 派生值 undefined / 命中被合成节点吃掉 / 阴影被裁)的定位。当 DevEco/hvigor 构建报 ArkTS Compiler Error(10505001/10605008/10605030/10605029/10605038/10605093/10905209/10905220/10905236/10905402 等)、Rollup "Unexpected token"、组件报错级联污染调用页、打开弹窗/菜单即闪退,或「代码没报错但列表不更新 / 按钮点不动 / 整块不渲染 / 阴影消失」时使用。适用于 HarmonyOS 原生应用开发(ArkTS/ETS)。
Install
npx skillscat add troiloryan/icey-harmony-skill/harmony-arkts-lint-fix Install via the SkillsCat registry.
HarmonyOS ArkTS 严格模式编译错误修复
触发场景
- DevEco 构建报大量 ArkTS Compiler Error / hvigor Rollup Error
- 从 Flutter/Web 移植到 HarmonyOS 时报错风暴
- 成片诡异级联错误(UI component cannot be used / Cannot find name xxx)
- 真机运行时崩溃:
TypeError: undefined is not callable/Cannot read property of undefined,
尤其"打开某个弹窗/菜单立刻闪退"(多为this归属问题,见 @Builder 规则)
核心诊断原则
- 先找根错误,级联后清:成片 10505001/10905236(如 "Row cannot be used"、"Cannot find name margin"、"Scroll only one child")几乎都是某文件/某组件先发生编译错的级联。用
grep -oP "At File: E:[^ ]+" buildlog.txt按文件统计,先修每个文件的第一个非级联错误(尤其自定义组件自身文件,如 GlassCard.ets 失败会污染所有调用页)。 - 每次让用户在 DevEco 重跑拿新日志,不要凭旧日志反复猜。
已知 API 铁律(本 SDK 已验证,按官方文档)
颜色
Color($r('app.color.x'))非法 → 直接用$r('app.color.x')(Resource 可被 .fontColor/.backgroundColor 接受)。Color只是枚举,无构造调用。blendOpacity在 Resource 和 ColorMetrics 上都不存在 → 在 base/dark 的resources/{base,dark}/element/color.json里定义带 alpha 的资源色(#AARRGGBB,如 92%→#EA+原色、12%→#1F+原色),代码用$r('app.color.card_bg_92')。ColorMetrics(@kit.ArkUI)只有resourceColor/blendColor等,无 blendOpacity。
fileIo(@kit.CoreFileKit)—— ⚠️ 模块级函数,不是 File 实例方法
本 SDK 实测签名($DevEco/sdk/default/openharmony/ets/api/@ohos.file.fs.d.ts):
declare function openSync(path: string, mode?: number): File; // :2634 → 返回 File 对象
declare function writeSync(fd: number, buffer: ArrayBuffer | string, options?): number; // :3651 → 要数字
declare function closeSync(file: number | File): void; // :416 → 两者都收openSync的返回值是File接口({ readonly fd: number, readonly path, readonly name, ... },:3870),
不能当 fd 用 → 否则 10505001Type 'File' is not assignable to type 'number'。
正确写法:const f = fileIo.openSync(p, m); fileIo.writeSync(f.fd, buf); fileIo.closeSync(f);- 没有
File.writeSync()/File.closeSync()实例方法。 fileIo.accessSync(path)返回 boolean(不存在返回false,不抛异常)。
cryptoFramework(@kit.CryptoArchitectureKit)
- 摘要:
createMd('SHA256')(无 createHash);md.update/digest均 async 需await。 - HMAC:
createMac('SHA256')(无 createHmac);createSymKeyGenerator('HMAC');convertKey返回Promise<SymKey>需await;mac.init/update需await;mac.doFinal()无参,await返回DataBlob。 - 对称:
createCipher('AES256|GCM|PKCS7');cipher.init/doFinalasync。 - 随机:
createRandom().generateRandomSync(16)返回DataBlob({data: Uint8Array})→await后取.data。 GcmParamsSpec:字段iv/authTag/aad类型均为DataBlob(需{ data: xxx }包装),且aad必填。util.Base64Helper().decode()返回Promise<Uint8Array>(需 await)。utf8ToBytes:new Uint8Array(buffer.Buffer)报 ArrayBufferLike 不兼容 →new Uint8Array(buf.buffer)。- 类型注解:
cryptoFramework.DataBlob/SymKey/SymKeyGenerator/Cipher/GcmParamsSpec/Mac可用;Hash不存在(应为 Md,建议不写注解靠推断)。 - 🔴
Md.update()返回Promise<void>,不是链式对象(2026-10-08 实测):
凡是"摘要类 API"(Md/Mac)都按这个两步写,别照搬 Node 的链式风格。❌ const d = await md.update(buf).digest() // digest 不存在,Property 'digest' does not exist on type 'void' ✅ await md.update(buf); const d = await md.digest();
@Builder 规则(最易踩)
- @Builder 方法体内禁止声明变量(const/let)→ 报 10905209 "Only UI component syntax can be written here"。把计算移到普通 private 方法,在 UI 表达式里调用(方法调用 OK,如
Text(this.buttonLabel()))。 - @Builder 方法体内禁止裸表达式语句(连无声明的副作用调用、如埋点
PerfProbe.bump('x');也不行)→ 报 10905204'PerfProbe.bump('x');' does not meet UI component syntax。⚠️ 错误码与上一条不同(10905204 vs 10905209),别照错码找错方向。副作用要放普通方法或.then/箭头回调里(2026-09-11 加性能埋点时实测)。 @BuilderParam content的箭头里只能调 @Builder 方法,不能内联 UI 组件(如content: (): void => { Row() {...} })→ 会炸 Rollup "Unexpected token"。正确:content: (): void => { this.xxxContent(); },内容放独立 @Builder。- 🔴 跨组件传
@BuilderParam必须「箭头包一层」,禁止裸方法引用(2026-09-15 真机闪退,运行时错误不是编译错误)——
症状:TypeError: undefined is not callable,打开弹窗/展开菜单即闪退,栈落在 builder 体内**第一个this.方法()**的位置。
根因:ArkUI 转译器把接收方组件里的调用写成✅ content: (): void => { this.formContent(); } // 箭头 → 词法 this = 本组件 ❌ content: this.formContent // 裸引用 → this 被换成接收方组件this.content.bind(this)(),this是接收方
→ 裸传的方法体内this.previewH/this.formTab全指向接收方(无此成员)。箭头忽略.bind、保留词法 this。
尾随闭包不受影响(转译后本身就是箭头),所以「多 @BuilderParam 改具名传参」那一轮最易引入。
唯一豁免:builder 体零this引用时裸传安全(菜单类 onClick 全用模块级/静态调用即可,这也是 Player 「menu 内禁止this.方法」的由来)。
自查:grep -n "content: this\.\|topContent: this\.\|cardHeader: this\.\|sheetSlot: this\."(有命中即 bug)。 - 🔴
bindMenu/ 其他@Builder闭包内的this未绑定 → 真机undefined is not callable闪退
(2026-09-14 CoverShapePicker 实证:shapeMenu的MenuItem.onClick写死this.applyShape(card.value),bindMenu的 Menu 闭包内 this 未绑定 →this.applyShapeundefined → 点下拉瞬间闪退;日志:崩溃前一条是MenuItem CLK)。
判据:日志出现「打开菜单/下拉瞬间闪退 +xxx is not callable」= 先怀疑闭包内this.方法。
修复:菜单项 onClick 一律改调模块级函数或静态方法(applyShapeFromMenu(value),逻辑与组件内方法一致:
写设置 + 联动更新 AVSession);组件树内(网格卡片 onClick 等)this 正常,不动。
项目先例:AppearanceSettings 的 themeMenu/materialMenu 注释记载同一坑(bindMenu 闭包内 this 未绑定、
组件方法this.applyTheme真机 undefined 闪退),其 9 处 withMenu 的 onClick 全走模块级函数/静态方法,
唯 CoverShapePicker 漏改 —— 新增任何 bindMenu 菜单项,onClick 一律模块级函数/静态方法。 - 🔴
@Builder的「按值传递参数」是快照 —— 由状态算出来的值绝不能当参数传(2026-09-16 实测:
设置项下拉"选了之后内容变了、但下拉标签文本不变",无任何报错,属静默 UI 不刷新类):
官方口径:按值传递时状态变量的改变不会引起@Builder内部的 UI 刷新。
参数只留静态事实(字段名、候选数组、下标这类不随状态变的东西)。❌ this.dropValueSmall(this.notebookLabel(), this.notebookItems()) // 值在调用点就算好 → 冻结在首帧 ✅ @Builder notebookDrop() { Text(this.notebookLabel()) … } // 值在 builder 体内现读
⚠️ 同族坑:@Builder以值快照收「选中态」(dirItem(label, selected)收selected)→ 选中不切换,
必须在体内读this.selectedXxx。区分:这是值快照问题;下面那条是宿主组件不重建问题 ——
两个症状都是"UI 不动",但一个靠"值改成体内读"、一个靠"build() 链里内联引用一次"。 - ⚠️
@BuilderParam里的状态,要「宿主组件 build 内联引用」一次才重建(铁律 #34):
builder 经 @BuilderParam 交给子组件渲染时,宿主不重建就不会带上新值(表现为"点 tab 没反应")。
做法:build()末尾挂一次内联读,如.opacity(this.formTab >= 0 ? 1 : 1)。 - 调
this归属类问题时,别猜——读本机构建缓存里的转译产物(保留注释、可读):<module>/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/**/*.ts。
它直接给出 ArkUI 生成的真实调用形态(.bind(this)()/previewArea(parent = null)/ 尾随闭包→箭头),
比翻文档快且确定;栈里的文件:行:列也能与转译产物逐行对上,用于定位「哪个this.方法()是第一个炸的」。 alignSelf只接受 ItemAlign(Start/Center/End),不接受 HorizontalAlign/VerticalAlign。- 类体内不能声明
const/let(报 10505001A class member cannot have the 'const' keyword):
模块级常量一律写在 class 外面(缩进也要对齐模块级,别留在类的方法之间)。
⚠️ 写进类体后不仅这两行报错,引用它的地方还会连带Cannot find name 'XXX'——
看到「10505001 ×N + Cannot find name ×M」混在一起,先查这几个名字是不是被塞进了某个 class 体内
(2026-09-16 实测:3 个常量误入 class → 该模块整体编译失败,整个 HAP 出不来)。
ArkTS 严格模式编译错误速查表(2026-10-08 实证,按错误码)
一次全量 UI 编译产出 69 个错,收敛到下列 8 类。按错误码反查,不要逐个猜。
| 错误码 | 消息要点 | 根因 | 修法 |
|---|---|---|---|
| 10605008 | arkts-no-any-unknown |
① Record<string, any> ② resp.header['x'] 推断成 any ③ 裸 as ESObject |
① 改 Record<string, ESObject> ② resp.header['x'] as string | undefined ③ 不能写 as ESObject,必须落到 Record<string, ESObject> |
| 10605030 | arkts-no-structural-typing |
const e: MyError = new Error(...) |
直接 const e = new MyError(...),别手写目标类型标注 |
| 10605074 | arkts-no-destruct-decls |
const [a, b] = await f() |
const p = await f(); const a = p[0]; const b = p[1]; |
| 10505001 | Duplicate identifier 'x' |
静态字段 x 与 static get x() 同名 |
字段改私有名(ver),或 getter 改名 |
| 10505001 | Type 'File' is not assignable to type 'number' |
openSync() 返回 File 当 fd 用 |
见上面 fileIo 铁律 |
| 10505001 | Property 'code' is missing in type 'Error' but required in type 'MyErr' |
同 10605030 | 同上 |
| 10505001 | Property 'key' in type 'X' is not assignable to the same property in base type 'CustomComponent' |
@Prop key: string 撞上 CustomComponent.key() |
属性改名(itemKey)—— 自定义组件里永远别叫 key |
| 10505001 | Cannot find module '../kit.Foo' |
用点号代替斜杠 | 目录必须 ../kit/Foo(../kit.Foo 只在部分模板文件里成立) |
| 10505001 | Conversion of type 'Error' to type 'Record<string, number>' may be a mistake |
跨类型强转索引签名 | 用自定义 Error 子类替代 as Record<...> |
| 10905210 | '@Entry' decorated component, 'build' can have only one root node |
Column + 弹窗/自定义组件两个根 |
包进 Stack({ alignContent: Alignment.TopStart }) { ... } |
| — | IterableIterator 无 forEach(不是 ArkTS 报错,是 stdlib 缺失) |
Map.values().forEach(...) |
Array.from(map.values()) + 索引 for 循环 |
| — | Rollup Unexpected token |
上一条语法错导致的连锁(错误行号偏前,别只盯报出行) | 往上找最近的真实语法错 |
| 10505001 | Property 'onLongPress' does not exist on type 'TextAttribute' |
Text/普通组件没有 onLongPress |
外包 Stack().gesture(LongPressGesture({repeat:false}).onAction(...)) |
| 10605008 | Promise.resolve([]) / Promise.resolve(x) 报 any |
泛型推断为 never[] / 落到 any |
显式 Promise.resolve<string[]>([]) |
| 10505001 | Cannot find name 'width'(在 build 尾部) |
组件尾随闭包之后又写了链式属性 | 见下方 LSN-025 补充 |
| — | 几十条假语法错、报在文件开头几行 | @Builder 后跟 private → 解析中断,后面全是级联报错 |
@Builder 不带访问修饰符(见下) |
🔴 @Builder 不能带 private(2026-10-08 实测,浪费一轮编译)
❌ @Builder private foo() { … } // 解析中断 → 后续几十条 Declaration or statement expected
✅ @Builder foo() { … }症状很有欺骗性:首个真错误报在 @Builder 声明行附近(如 :28 Identifier expected),
真正的连锁错误散布在文件后半。判据:错误跨十几行且首条是 Identifier expected / Unexpected token
→ 先 grep @Builder\s+(private|public|protected)。
🔴 组件尾随闭包后不能链式(LSN-025 的通用形态)
❌ SubPageScaffold({ title: 'x' }) { List() {…} }
.width('100%').height('100%') // Declaration or statement expected / Cannot find name 'width'
✅ SubPageScaffold({ title: 'x' }) { List() {…}.width('100%').layoutWeight(1) }链式必须挂在组件自己的闭合括号内(最后一个 } 之前),不能挂在其后。
同理:build() 的根 Stack(){…} 后再写 .width() 是合法的(根容器未被尾随闭包消费),
但带尾随闭包的组件中间层不行 —— 判据是「这个 } 后面是否还有组件参数在等着」。
排查顺序:先按「文件 + 错误码」聚合(同一文件 3 条以上先修根),再从上到下——
自定义组件自身错 → 调用页级联错 → 页面结构错。
Rollup 报的行号不可信(它报的是转译产物的位置,往往指不到真正的源码行)。
组件/属性
- 自定义组件的成员名不能与「通用属性方法」重名(报 10505001
Property 'x' in type 'Foo' is not assignable to the same property in base type 'CustomComponent')→ 改名(如size→iconSize、tabIndex→formTab),同步改所有引用点。- 范围比「size/width/height」大得多:基类链带全部通用属性方法——
CommonMethod/CommonShapeMethod/ScrollableCommonMethod(本 SDKopenharmony/ets/component/common.d.ts实测 255 个),含tabIndex(焦点顺序,:18815)/key/id/opacity/animation/onClick/focusable/backgroundColor/border/margin/padding/zIndex… 一律避开。 - 判定口径(别一刀切改名):只查「该名字是不是某个
*Attribute/CommonMethod类里声明的方法」。- 只是某接口的字段(
title、color、x、y)→ 不冲突,可放心用。 - 落在专用 Attribute 上的名字(如
tabBar只在tab_content.d.ts的TabContentAttribute)→ 不冲突,自定义组件里叫tabBar()/tabItem()的 @Builder 合法。
- 只是某接口的字段(
- 自查脚本(逐行跟踪
declare class|interface归属,只收属性类的 4 空格方法声明,再与@State/@Prop/@Link/...成员名求交集):
能在改名前一次性查出全仓潜在撞名,避免「改完一版 → 用户 Build → 又报一个」的往返。
注意第一版容易假阳性:若把 common.d.ts 里所有^\s{4}(\w+)[:?]字段也收进来,会误报出title/color/x/y;务必限定在方法 + 属性类。
- 范围比「size/width/height」大得多:基类链带全部通用属性方法——
- 匿名对象字面量必须对应声明的 interface/class(10605038)→ 定义接口;动态字段用点访问(
params.icon=),禁止params['icon'](10605029)。 - interface 实例不能传给
Record<string, X>参数(缺索引签名)→ http helper 的 query/body 参数类型改Object,内部as Record<string, Object>;或函数返回类型用具体接口(如Promise<HuaweiParseResult | null>)。 - 路由页必须有
@Entry(main_pages.json 已列但文件缺 → 10905402)。 - 🔴 卡片/表单页的模块级变量名会跨文件撞(2026-09-16 实测,报 10505001
Cannot redeclare block-scoped variable 'NUM_OFFSET_Y',报错指向两个不同文件):entry/src/main/ets/widget/pages/下的卡片页(form_config.json的 src 指向的@Entry文件)
不各自独立作用域 —— A 页与 B 页各写一次const NUM_OFFSET_Y = 2就撞。同类名:TODAY_RED/DATA_BLOCK_W/COLS(同项目里 4x4 卡片也各有一份,只是当次错误列表截断没全报)。
⇒ 卡片页模块级常量一律带卡片后缀(NUM_OFFSET_Y_1X2),或把常量挪进组件内部。
⚠️ 这类错误不在宿主文件里,改"设置/宿主"需求时极易漏看 —— 收尾必须真跑一次构建。
静默失效与命中排查(编译通过但运行不对)
这类问题编译零报错、日志无异常,只能靠"看编译产物 + 按 zIndex/覆盖层列清单"定位。本工程(Icey-Reader)
已各栽过一轮,结论固化如下。项目专属内容(双内核/导入管线/数据模型)不在此列,见其docs/。
1. 🔴 struct 内禁用 get 访问器(命中率最高,2026-10-06 实证)
- 症状:派生值取到
undefined(fontSize(undefined)⇒ 整行/整块不渲染)/ 参与运算变NaN
(scrollToIndex(NaN)⇒ 动作静默失效,目录不定位、进度上报坏值)。 - 根因:ArkTS 把
.ets编译成.ts时,手写get访问器被整段丢弃(@Prop 自动 getter 保留、
普通方法保留、手写get一个都不留)。 - 修法:派生值一律写私有方法(
this.xxx())或提前算进普通成员。 - 历史中招:
BookCover.cellSize/colCount/titleX/titleY、ReaderKitPage.tocIndex(目录不定位)、ReaderPage.currentChapterIndex(进度上报坏值)。 - 自查:
grep -rn "get [a-zA-Z_]" entry/src/main/ets --include=*.ets(struct 内手写 getter 应无命中)。
2. 🔴 自定义组件调用点的通用属性会生成合成节点 __Common__(吃命中)
- 症状:父组件里给组件内部节点设了
HitTestMode.None,那块仍吃点击;只写了padding的留白带
成了命中终点。 - 根因:
TtsSubtitle({...}).padding({bottom:96})编译后生成__Common__合成节点包住组件,
合成节点无 hitTestBehavior 设置(默认 Default = 自身响应 + 阻止兄弟) ⇒ 它的 padding 区也是命中区,
组件内部的 None 管不到调用点这一层。 - 修法(两条缺一不可):① 不需要时门控渲染让该层根本不存在(
if (this.ttsOn));
② 调zIndex让该接收命中的层排在被吃命中的层之上。 - 实例:阅读页底栏 7 按钮全灭 —— 字幕层调用点 padding(96) 盖住底栏(88),且字幕层
zIndex(3)> 菜单层zIndex(2)。
3. hitTestBehavior 语义(不递归)
| 取值 | 自身 | 子节点 | 兄弟 |
|---|---|---|---|
Default |
响应 | 响应 | 阻止 |
Block |
响应 | 阻止(连同自己按钮) | 阻止(相对父容器) |
Transparent |
响应 | 响应 | 不阻止 |
None |
不响应 | 不响应 | 不阻止 |
Block的"阻止兄弟"是相对父容器 ⇒ 必须加在整层,只加小块无效。None不阻止任何一方 ⇒ 挡不住下层 native surface / 子节点。- 排查先问"命中停在哪个节点"(列全部整屏覆盖层含
opacity 0、按 zIndex 从高到低问)。
4. 🔴 LazyForEach 列表"整表刷新"只有一条真路(.key() 不是重建原语)
- 症状:数据层全对(
@Watch触发、books换了新数组),屏幕条目数/内容不变;
空列表导入第一本会立刻显示、已有书再导入就不显示。 - 根因:
.key('epoch_'+n)是测试专用标识(@test,编译器 WARNkey' can only be used for testing directories),
对渲染零影响。能真正销毁子树的原语只有if的分支号 和ForEach的 key。 - 正解:外层套
if (this.dataEpoch % 2 === 0) { A } else { A }(两分支逐字同内容 ⇒ 分支号翻转即销毁重建,
抽@Builder就不销毁了);行内刷新用ForEach([\k_${tick}`], …)(但Refresh` 只能一个子组件,包不住整个 List)。 - 数据源实例必须稳定:
LazyForEach(source, …)创建时把监听器注册在当时对象上;this.source = new Xxx()后旧实例仍被攥着 ⇒ 新书永远不出现、notifyRowOf打空。
⇒ 实例建一次,用setRows()就地换数据。 - 别重建太勤:打开书→
markRead→阅读次数+1 这类不影响行结构的,无条件重建会每次返回列表跳回顶部
⇒ 用行签名(每行 key 拼起来)当判据,签名变才重建。
5. 带参 @Builder 根节点事件归属不确定
- 表现:给带参
@Builder的根节点挂onClick⇒ 点不动、无日志。 - 口径:带参
@Builder不挂事件,抽成@Component(组件边界 = 事件边界)。layoutWeight(1)这类布局属性也必须挂在组件自己根节点、写在调用处无效。
6. 🔴 离屏截图 getComponentSnapshot().get(id) 被父容器 height('100%') 截断
- 父容器
.height('100%')会把自适应高度的离屏子节点截断到整屏高 ⇒ 长内容拦腰截断。
承载容器必须显式给足高度(上界用「字数×行高」证明)。 .id()不要写在自定义组件调用点(落到__Common__合成节点,截到的是容器);透传进组件、打在组件自己根节点上。- 渐隐/覆盖遮罩用负 margin 叠(占位高 0),别用
height('100%')覆盖层(百分比按父级约束解析成"最大高度"把卡片撑爆)。
7. ⚠️ fileIo.accessSync(path) 返回 boolean(不存在返回 false,不抛异常)
- "不存在就建目录"不能写
try { accessSync } catch { mkdirSync }(catch 永不执行)⇒ 随后openSync报13900002。
⇒ 直接mkdirSync+ 吞掉"已存在"异常。
8. 🔴 clip(true) 与 .shadow() 写在同一个节点 = 阴影被裁(2026-10-07 结构性发现)
clip(true)把自己节点的绘制(含向外扩散的阴影)一起裁掉 ⇒ 同节点写clip+shadow等于没有阴影。- 为什么长期没发现:旧阴影右下偏移被裁掉的是左上、右下仍露大半,读作"有效阴影"。改成无偏移/居中时才暴露。
- 修法:需裁内容(图/渐变塞进圆角)→ 双层(外层 Stack 做 size+borderRadius+shadow,内层 Stack 做 clip);
只想浮起 → 直接给外层容器加.shadow,别在 clip 节点上加。 - ⚠️
shadow()的offsetX对整行轨道必须 0(右偏让左端无阴影、右端重,读作整体歪)。 - ⚠️⚠️
.shadow的轮廓 = 它所在节点自身的形状(2026-10-08 补):圆角只写在内层
「裁图节点」上、shadow挂在外层没有borderRadius的容器上 ⇒ 阴影按直角矩形画,
在卡的圆角外露出四个直角,读作「封面圆角了、但阴影衬托出矩形」(用户原话)。
⇒ 两处都要写borderRadius:内层裁内容 + 外层(挂 shadow 的那层)给形状。
排查:grep -rn "\.shadow({" -B 6 entry/src/main/ets --include=*.ets,逐处确认同节点有borderRadius。 - ⚠️⚠️ 父滚动容器默认
CONTENT_ONLY⇒ 阴影在 padding 区被裁(2026-10-08 第三处独立成因):
官方文档原文:「Grid、Scroll 的默认值为ContentClipMode.BOUNDARY,List、WaterFlow 的
默认值为ContentClipMode.CONTENT_ONLY」,而CONTENT_ONLY = 0的定义是
"Clip to content rect inside margin & padding"(按扣掉 padding 的内容区裁剪)。
⇒List挂了.padding({ left:16, right:16 })时,子项阴影向左右扩散的部分落在
padding 区⇒ 被裁掉(症状:只有左右被切平,上下正常)。这与"父容器 padding 是
阴影落脚点"的直觉相反 —— 空间存在 ≠ 可绘制。
修法:List().clipContent(ContentClipMode.BOUNDARY)(= 裁到组件外框、含 padding、不含 margin)。
⚠️ 至此「阴影被裁」共三处独立成因,别合并记:① 同节点clip(true)+shadow;
② shadow 节点无borderRadius;③ 父滚动容器默认CONTENT_ONLY。
10. ⚠️⚠️ Tabs/HdsTabs 的 onChange 在切换动画结束后才触发(2026-10-08)
症状:左右滑动切 tab 不跟手 —— 内容页已跟手到位,底部 tabBar 高亮才切
(点击切换则读作"页面先过去、高亮过一会儿再亮")。
根因(SDK 文档明写):TabContent 的高亮若读 currentIndex,而 currentIndex
只在 onChange 更新,就必然滞后整段动画时长。
| 回调 | 触发时机 |
|---|---|
onChange(index) |
切换动画结束后 |
onAnimationStart(index, target) |
动画起手(松手过阈值那一刻) |
onSelected / onUnselected |
动画起手(Hds 6.1.0(23) 起,低 compatibility 会闪退) |
onGestureSwipe / onContentDidScroll |
逐帧 —— ⚠️ HdsTabs 未透出(仅标准 Tabs 有) |
判据:同页标题栏正常(读的是 onAnimationStart 写的 chromeIndex)、只有底部高亮滞后
⇒ 唯一差异就是读的索引不同。
修法:tabBar 选中态与标题栏同源,都读 chromeIndex(由 onAnimationStart 在动画起手
写 targetIndex);currentIndex 只保留给受控 index 与 onChange 兜底。
- ⚠️ 做不到"拖动中随位移渐变高亮":需要逐帧的
onGestureSwipe/onContentDidScroll,
HdsTabs 没透出。本修法把滞后收敛到松手过阈值那一刻,是 HdsTabs 上限。 - 排查:
grep -rn "currentIndex ===" entry/src/main/ets --include=*.ets(tabBar 高亮读它即错)。
11. ⚠️ 订阅一个"从未被写入"的 @StorageLink 键 = 静默死代码(2026-10-08)
症状:@StorageLink('someKey') 的组件"看着接好了",但值永远是初始值 ——
父页面改了对应状态,本组件毫无反应。编译不报错、运行不报错、日志也没有。
根因:@StorageLink 只订阅键名。若全项目从未 AppStorage.setOrCreate('someKey', …),
该键不存在 ⇒ 本组件读到的是自己声明的默认值 ⇒ 永不更新。
实证:某组件订阅
'homeCurGroup'/'homeCurSegment'想跟随父页面的分组切换 ——
但父页面的curGroup/curSegment是@State(不广播到 AppStorage)。grep该键零写入点 ⇒ 分组联动是死代码,且分段过滤完全没做(取全库 ⇒ 混着别的类型)。
口径:新增"订阅某键"前先 grep 该键的写入点:
grep -rn "setOrCreate.*'<键名>'" entry/src/main/ets --include=*.ets # 无输出 ⇒ 不能订阅两条正路:① 由父 @Prop 透传(状态本就在父手上;子组件不必理解任何筛选规则 ⇒
口径零漂移)—— 优先;② 让状态持有者新增广播(适合跨页/跨组件多处消费)。
12. ⚠️ 枚举/档位的"钳制"写法会把新档位静默吞掉(2026-10-08)
症状:新增一档(如 viewMode 加"封面墙 = 2"),但切换按钮点了没反应 ——
不报错、值也没写进去。
根因:老代码的白名单式钳制没跟上:
// ✗ 旧写法:非 1 一律归 0 ⇒ 传入 2 被静默降级成 0
const mode = v === 1 ? 1 : 0;口径:加档位时同步审计所有钳制点:
grep -rn "=== 1 ? 1 : 0\|> 1 ? 1\|<= 1" entry/src/main/ets --include=*.ets更稳的写法是把合法值显式列出((v === 1 || v === 2) ? v : 0)。
9. ⚠️ aboutToDisappear 不只在"主动退出"时触发(2026-10-07 语义陷阱)
- 三种触发:① 用户主动返回(可预期);② 进程被杀/崩溃(不触发);③ 应用退到后台后系统回收该页面(照常触发)。
- 后果:任何靠"
aboutToDisappear=用户主动离开"的判据偶发失效(用户说"有时候…")。 - 修法:读
AppStorage('appInBackground')区分前后台 + 在"主动退出唯一入口"处显式再做一次
(两道独立防线,任一条失效另一条仍正确;取不到键按保守方向——该做没做是破坏性的,多做一次通常不是)。
排查 SOP(点不动 / 不生效先看)
- 列全部整屏覆盖层(含
opacity 0)。按zIndex从高到低问"它吃不吃这一下"。 grep -rn "hitTestBehavior\|\.zIndex(" entry/src/main/ets --include=*.ets。- 看编译产物真实节点:
<module>/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/**/*.ts
(__Common__合成节点、被丢弃的 getter 都在这里现形)。 - 改完真机复验(命中类"看着对"和"点得动"是两回事,已因此白改过两轮)。
编译验证:本机有 CLI,别靠猜(2026-09-16 补齐)
hvigorw 在 DevEco 安装目录里,项目根通常没有 wrapper:
# 纯类型校验(几十秒,最常用);$DevEco = DevEco Studio 安装根
"$DevEco/tools/hvigor/bin/hvigorw.bat" \
--mode module -p product=default -p module=entry@default -p buildMode=debug \
default@CompileArkTS --no-daemon
# 完整打包(含签名,能出 hap)
<同上> assembleHap --no-daemon- 需要
DEVECO_SDK_HOME(DevEco 装好即有)+oh_modules(ohpm install)。 - PowerShell 工具不回显 stdout →
*> $env:TEMP\b.log重定向,再Read。 - ⚠️ 本机(2026-10-08 复核)hvigor 日志是 UTF-8,不是 UTF-16 —— 它用 box-drawing 字符(
─ │)画分隔线,
很容易被误判成宽字符编码。iconv -f UTF-16LE会解码成硰瘲❰栠獡戠敥之类的乱码。
稳妥写法:先试 UTF-8,失败再兜底 UTF-16LE(用 python 读字节 + 试探性解码,别用 shell iconv 猜)。 grep "COMPILE RESULT"判断成败不可靠:成功时该行不输出(grep退出码 1,看着像失败)。
判据用echo $?(hvigorw 的真实退出码):0 = 编译通过。- 末尾
> hvigor WARN: No signingConfig found for product default是正常的(debug 无签名),
只要退出码 0 且entry/build/default/outputs/default/entry-default-unsigned.hap存在即为成功。 - 判定"错误是不是我引入的":删掉整个
<module>/build全量重编 —— 若错误一模一样,
就是真实的源码错误(不是增量/缓存问题);再配合git status看报错文件是否被自己改过。
别急着改,先分清"自己引入的"和"HEAD 本来就坏的"(后者要么一并修,要么明确报告)。 - 🔴
Error Code: 00308018 Unknown Error不是代码错误(2026-10-08 实测,本机必现):
真实报错是> hvigor ERROR: 00308018,但.hvigor/outputs/build-logs/build.log里搜不到这个码
—— 它来自 hvigor 之外的环节:增量编译时 hvigor 要批量删entry/build/default/cache下的.ts
缓存(约 160 个文件),被 safe-delete 钩子按「批量删除需确认」拦下。
判据:① ArkTS 编译阶段无ERROR:列出;②.hvigor日志里查不到该码;③ 报在CompileArkTS而非具体文件。
修法:rm -rf entry/build/default/cache后重跑。
⚠️ 删除与构建必须分成两条命令 ——rm -rf X && hvigorw …串成一条会被整条拒(连带构建也没跑),
且日志文件也没写成,表现为"下一步报 No such file"。 - 🔴 定位报错文件不能只看
N ERROR: 10505001那一行(2026-10-08 实测):
编号行只有错误码没有路径,真实信息在紧邻的下一行且常被硬折行截断成两三段
(路径太长时 hvigor 按固定列宽换行,…/entry/s+rc/main/ets/…被拆开)。.hvigor/outputs/logs/details/details.json里的ERROR_MESSAGE也只有 CODE 没有文件
(别指望它,比日志还少)。可靠写法 —— 按行号打印命中行的上下文再肉眼拼路径:
输出形如lines = log.splitlines() hits = [i for i, l in enumerate(lines) if 'ERROR:' in l and '10505001' in l] for i in hits: for j in range(i, min(len(lines), i + 4)): # 编号行 + 后续 3 行(Error Message + 折行片段) print(lines[j])Error Message: Property 'sf_tab_music' does not exist on type 'typeof SFIcons'. At File: …/entry/s+rc/main/ets/pages/subPages/HomeDetailPage.ets:40:31—— 两段拼起来才是完整路径。
⚠️COMPILE RESULT:FAIL {ERROR:3}里的 3 往往只列出 2 条(编号只到 2),
所以"改一个再编译"可能只消掉 1 条,别以为差得少。
签名配置:p7b 可换,p12/cer 必须配套(2026-10-08)
多项目共用同一开发者账号时,debug 签名材料可以跨项目整套复用(含加密密码密文),
不必每个项目重新生成 —— 实测两个不同包名的 p7b 内嵌 development-certificate 是同一张证书。
- 目录命名照同族项目(如 Icey-Player-Harmony):
signing/debug/debug.{cer,csr,p12,p7b},
别自造文件名(用户对命名有明确预期)。.gitignore只忽略/signing/material/、/signing/debug/material/(哈希命名中间缓存),p7b/cer/p12/csr 必须进仓库。 build-profile.json5用仓库相对路径(signing/debug/debug.p12)+ product 补"signingConfig": "default",这样换机器 clone 即可签。- 换签名材料后必须
rm -rf signing/debug/material/—— 那是按材料哈希缓存的中间产物,
不清会报验签失败(见错误码表)。
| 错误码 | 含义 | 排查 |
|---|---|---|
| 00303116 | storePassword/keyPassword 长度 < 32 |
密码为空或填了明文;要用 DevEco 加密后的 0000001A… 密文 |
| 00303242 | Signature material verification failed |
p12 与 p7b 内证书不配套 或 signing/*/material/ 缓存脏 |
| 00303074 | bundleName in app.json5 does not match … SigningConfigs |
p7b 的 bundle-name 与 AppScope/app.json5 不一致(换包名后忘了换 p7b) |
验 p7b 绑定了哪个包名(不用装任何工具):
openssl asn1parse -inform DER -in signing/debug/debug.p7b 2>/dev/null | grep -o '"bundle-name":"[^"]*"'
# 输出形如 {"version-name":"2.0.0",…,"bundle-name":"com.bili.downloader",…,"type":"debug",…}顺带能看到 app-identifier(AGC 分配)与 debug-info.device-ids(白名单设备)。
静态预检(CLI 不可用时,用于捕获 Cannot find name / xxx is not exported)
跑不了 hvigor CLI 时,用下面的 Python 扫描替代第一道防线。注意 report 会误报:十六进制色串(#FFFFFF)、JSON、export class Foo { static readonly NONE } 的静态成员、xxx.ENUM 成员访问都会命中"大写标识符"规则;只把独立出现且非注释行的当真。
# 1) 未定义的大写标识符:负向后顾排除成员访问 (?<![.\w]),跳过 // 与 * 注释行
use_pat = re.compile(r'(?<![.\w])([A-Z][A-Z0-9_]{3,})\b')
# 2) import 符号 vs 目标文件 export 一致性
EXPORT = r'export\s+(?:const|let|var|class|enum|interface|type|function)\s+(\w+)' # ← function 必须带上,否则 export function 被误报
# 还要处理 `export { a, b }` 与 `@Builder @Component export struct X`
# 路径解析:os.path.normpath(join(dirname(file), target)) + '.ets' ← 漏加 .ets 会全体 TARGET-MISSING
# 3) 括号/引号平衡:count('{')-count('}')、count('(')-count(')')Edit 工具的隐性风险(真实踩过):old_string 若覆盖到 const XXX: number = 4; 这一行,而 new_string 只留注释、忘记重写 const/去掉原本行,符号就凭空消失,报 Cannot find name 'XXX'。
判据:改动涉及抽常量/重命名时,改完必须 grep 该符号的「定义处」与「引用处」同在,不能只 grep 引用。
修复流程
- 收日志 → 按文件统计错误分布,找根错误文件。
- 修自定义组件自身 → 清调用页级联。
- 对照上面 API 铁律逐一核对。
- 先跑上面静态预检(尤其"抽常量/重命名"类的结构性改动),再交给用户进 DevEco。
- grep 验证无残留:
Color(,blendOpacity,createHash/createHmac,randomBytes,size: N,alignSelf(VerticalAlign|HorizontalAlign,@Builder内const。 - 让用户在 DevEco 重跑;有新错继续,旧日志别反复用。