鸿蒙PC开发实战:ArkTS 严格模式 5 个编译 ERROR 从报错到修复完整记录
欢迎加入开源鸿蒙PC社区:https://harmonypc.csdn.net/
欢迎在PC社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
摘要
ArkTS 不是 TypeScript 的"带类型的超集",它是一门有运行时约束的语言——编译阶段就会把 TS 里宽松允许的写法拒之门外。本人参加鸿蒙 PC 积分赛,提交 HarmonyPCDemo 项目时一口气触发了 5 个 compilation ERROR,从 Object literal 未声明类型到 @ts-ignore 被禁用,从基类属性重名到新版 API 必填字段……本文把这 5 个真实报错的原文、根因、修复代码完整复盘,给后续参赛的同学一份可直接对号入座的排错清单。
目录
- 摘要
- 适用读者
- 前置知识
- 工具版本
- 排错流程图
- ERROR 1:
arkts-no-untyped-obj-literals(TinyMarkdown.ets) - ERROR 2:CustomComponent 基类
scale重名(DpiMenuDemo.ets) - ERROR 3:distributedKVStore Options 缺少
securityLevel(DistributedNote.ets) - ERROR 4:distributedKVStore
get回调value类型变更 - ERROR 5:
@ts-ignore不允许(StickyNote.ets) - 最终构建成功日志
- 总结:ArkTS 严格模式 vs TypeScript 宽松模式的 5 点差异
- 常见报错与排查
- AtomGit 仓库链接
- 结语与后续计划
适用读者
- 正在用 DevEco Studio 4.2 / 5.0 开发鸿蒙 PC 应用的同学
- 有 TypeScript 基础、初次接触 ArkTS 的前端开发者
- 已经通过"ArkTS HelloWorld"但面对稍复杂业务模块就被编译 ERROR 卡住的工程师
前置知识
- 了解 ArkTS 的
@Entry / @Component / struct基本语法 - 知道
hvigorw、ohpm install、build-profile.json5是什么 - 看过 ArkTS 官方文档的 “严格模式(Strict Mode)” 章节(本文不再复述定义,只讲实战)
工具版本
| 项 | 版本 |
|---|---|
| DevEco Studio | 5.0.3.600(Windows x64) |
| HarmonyOS SDK | API 12(compatibleSdkVersion 5.1.0) |
| ArkTS 编译器 | hvigor 4.2.3(harmony_ets_checker) |
| ohpm | 1.6.0 |
排错流程图
在正式进入 5 个 ERROR 的逐个复盘之前,先把整条排查路径画出来。后面每一节都可以在这张图上找到自己的位置——从编译日志出发,按 rule: 编号分流,修复后回到构建验证。
图里有两个关键回路,值得单独说明:
- 编译回路(B → C → … → H → B):ArkTS 严格模式的报错是「一次只报一批」,修完一个 ERROR 重新构建,往往会冒出下一个。所以不要指望一次改完,按
rule:编号逐个击破才是正常节奏。 - 运行回路(Z → R → T → V → B):编译通过不等于跑得通。浮窗权限、安全区、旋转重定位、Preferences 落盘这些坑都在运行期暴露,排查时直接查「常见报错与排查」那张表。
下面按 ERROR 1 ~ ERROR 5 的顺序,逐个还原真实报错原文、根因与修复代码。
ERROR 1:arkts-no-untyped-obj-literals(TinyMarkdown.ets)
真实报错原文
entry/src/main/ets/utils/TinyMarkdown.ets(253:1): error ArkTS:
Object literal must correspond to some explicitly declared class or interface
--> 253 | export default { parse };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
hint: ArkTS disallows anonymous object literals as default exports.
Declare an interface/class first, then assign.
rule: arkts-no-untyped-obj-literals
根因分析
我原本照搬了 TS 项目里写默认导出的习惯——直接把一个对象字面量当作 default export:
// ❌ TypeScript 里完全合法
export default { parse };
ArkTS 的严格模式禁止任何**“匿名对象字面量”**。因为 ArkTS 的编译器要保证运行时类型安全,一个没声明类型的裸 { parse } 会让静态分析工具(IDE 智能提示、性能优化器、字节码生成器)全部失去线索。
报错编号 arkts-no-untyped-obj-literals 就是 ArkTS 官方 lint 规则库中的第 57 条:所有作为导出值、常量赋值、参数传递的对象字面量,必须事先有一个具名的 interface 或 class 声明它的结构。
修复代码
两步走:先声明接口 → 再把对象字面量赋给一个带类型注解的 const,最后导出这个 const。
// TinyMarkdown.ets —— 修复后的尾部代码
/** 默认导出接口:先声明类型,再用它约束对象字面量 */
interface TinyMarkdownDefault {
/** 对外主入口:Markdown 文本 → RichText 可识别的 HTML */
parse: (src: string) => string;
}
// 显式用接口约束 const,ArkTS 编译器才能推断通过
const defaultExport: TinyMarkdownDefault = { parse };
// ✅ 这个 defaultExport 不再是匿名对象,编译器放行
export default defaultExport;
修复后重新 Build > Hvigor > Clean Project,这个 ERROR 立即消失。
教训一句话:ArkTS 里只要出现 export default { xxx },99% 会踩这个坑。养成习惯——先写 interface,再赋值。
ERROR 2:CustomComponent 基类 scale 重名(DpiMenuDemo.ets)
真实报错原文
entry/src/main/ets/pages/DpiMenuDemo.ets(44:5): error ArkTS:
Property 'scale' in type 'DpiMenuDemo' is not assignable
to the same property in base type 'CustomComponent'.
--> 44 | private scale(size: number): number {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: base type declares 'scale' with signature
(value: { x: number, y: number, z?: number }) => T
hint: rename your member to avoid shadowing the framework.
根因分析
为了做 PC 端高 DPI 适配,我在 DpiMenuDemo 里写了一个辅助函数:
// ❌ 编译 ERROR:scale 和基类冲突
private scale(size: number): number {
return size * this.dpi;
}
乍一看这是一个再普通不过的私有成员函数,但 ArkUI 的 @Component struct 隐式继承了 CustomComponent 基类。这个基类里已经有一个 scale——就是我们写链式修饰器时用的那个:
Text('hello')
.scale({ x: 1.5, y: 1.5 }) // ← 这就是基类的 scale!
我自己声明的 scale(size: number) 返回 number,而基类的 scale(value: ScaleOptions) 返回组件 T,签名完全不兼容。ArkTS 严格模式禁止子类属性覆盖基类同名属性时改变类型签名。
修复代码
改名,就这么简单。而且选名要和业务语义对齐:
// DpiMenuDemo.ets —— 修复后
// ✅ 改名 scaleFont,明确表达"按 DPI 缩放字体"的语义
private scaleFont(size: number): number {
return size * this.dpi;
}
build() {
Column() {
// 所有调用点同步改名
Text(`鸿蒙PC屏幕 DPI 系数:${this.dpi}`)
.fontSize(this.scaleFont(18))
.fontWeight(FontWeight.Bold)
.margin({ top: 16 })
// ... 其余调用点同理替换
}
}
教训一句话:ArkUI 的 CustomComponent 基类有一大堆链式修饰器方法(scale / rotate / translate / clip / opacity / ...),给组件写私有方法时避开这些名字;实在避不开就加前缀 myScale、calcScale。
ERROR 3:distributedKVStore Options 缺少 securityLevel(DistributedNote.ets)
真实报错原文
entry/src/main/ets/pages/DistributedNote.ets(23:9): error ArkTS:
Property 'securityLevel' is missing in type 'distributedKVStore.Options'
but required in type 'Options'.
--> 23 | const options: distributedKVStore.Options = {
| ^^^
| ... createIfMissing: true,
| encrypt: true,
| backup: false,
| autoSync: true,
| ^^^
hint: Options interface was updated in API 11 to require securityLevel.
kit: @kit.ArkData
根因分析
distributedKVStore 的 Options 接口在 API 11 做了一次破坏性更新——新增了必填字段 securityLevel,用来声明分布式数据的安全等级(S0 公开 / S1 低敏感 / S2 中敏感 / S3 高敏感)。
我的代码还是从旧 Demo 里复制的:
// ❌ API 10 写法,API 11+ 编译失败
const options: distributedKVStore.Options = {
createIfMissing: true,
encrypt: true,
backup: false,
autoSync: true,
// ← 缺少 securityLevel!
};
ArkTS 严格模式下,接口的必填字段一个都不能少,少了就是 compilation error,不是 warning。
修复代码
补上 securityLevel: distributedKVStore.SecurityLevel.S3:
// DistributedNote.ets —— 修复后
const options: distributedKVStore.Options = {
createIfMissing: true,
encrypt: true,
backup: false,
autoSync: true,
// ✅ 新版 API 必填:声明分布式存储安全等级
// S3 = 仅本设备可信应用可访问(最严格),适合个人笔记场景
securityLevel: distributedKVStore.SecurityLevel.S3,
};
kvManager.getKVStore<distributedKVStore.SingleKVStore>(
'noteStore', options,
(err: BusinessError, store: distributedKVStore.SingleKVStore) => {
if (err) {
console.error(`[Distributed] getKVStore failed: ${err.code} ${err.message}`);
return;
}
this.kvStore = store;
// 订阅数据变更,手机↔PC 实时同步
this.kvStore.on(
'dataChange',
distributedKVStore.SubscribeType.SUBSCRIBE_TYPE_ALL,
() => { this.refresh(); }
);
this.refresh();
});
教训一句话:鸿蒙 SDK 升级后(比如从 API 10 升到 12),扫一眼 @kit.ArkData 这种高频使用的接口定义,破坏性变更 90% 集中在 Options、Config 这种配置对象里。ArkTS 严格模式会第一时间把它们揪出来,反而是好事——不会带着隐藏 bug 上线。
ERROR 4:distributedKVStore get 回调 value 类型变更
真实报错原文
entry/src/main/ets/pages/DistributedNote.ets(51:49): error ArkTS:
Types of parameters 'value' and 'data' are incompatible.
--> 51 | this.kvStore?.get('note', (err, value: Value) => {
| ^^^^^
note: Type 'Value' is defined as an object wrapper with .value property
Type of callback parameter 'data' is now
string | number | boolean | Uint8Array (plain union)
hint: use explicit type 'string | number | boolean | Uint8Array'
and then narrow with typeof.
根因分析
和 ERROR 3 同一个 API 更新引发的连锁问题。旧版 get 回调里的 value 是一个包装对象 Value,真正的数据要从 value.value 里取:
// ❌ 旧 API 写法
kvStore.get('note', (err: BusinessError, value: distributedKVStore.Value) => {
if (!err && value) {
this.note = value.value as string; // 解包
}
});
新版 API 把包装层去掉了,value 直接就是一个联合类型 string | number | boolean | Uint8Array。ArkTS 编译器在严格模式下不允许你用错误的类型注解去声明回调参数——哪怕你后面用 as 强转也不行,必须直接声明正确的类型。
修复代码
两步:① 把 value 参数类型声明为联合类型;② 用 typeof 做类型缩窄。
// DistributedNote.ets —— 修复后的 refresh()
private refresh(): void {
// ✅ 新版回调 value 直接是联合类型,不再有 Value 包装
this.kvStore?.get(
'note',
(err: BusinessError, value: string | number | boolean | Uint8Array) => {
if (!err && value !== undefined && value !== null) {
// ✅ typeof 缩窄:只取 string 类型,其余类型丢
// 分布式里 put 进去的是 string,但 get 回调联合类型统一,必须显式筛选
this.note = typeof value === 'string' ? value : '';
}
}
);
}
教训一句话:ArkTS 严格模式下类型缩窄是必须的——你不能把一个联合类型的值直接赋给某个更具体的类型(比如 string),必须显式告诉编译器"我已经检查过了"。这和 TS 宽松模式里随手 as string 就过完全不同。
ERROR 5:@ts-ignore 不允许(StickyNote.ets)
真实报错原文
entry/src/main/ets/pages/StickyNote.ets(38:7): error ArkTS:
Switching off type checks with in-place comments is not allowed.
--> 38 | // @ts-ignore —— getContext 返回 UIContext,Preferences 需要 UIAbilityContext
| ^^^^^^^^^^^
hint: ArkTS enforces type safety; use explicit cast 'as TargetType'
or restructure your code to obtain the correct context.
rule: arkts-no-ts-ignore
根因分析
写 StickyNote.ets 可拖拽便签时,我需要拿到 common.UIAbilityContext 来调用 preferences.getPreferences() 和 window.getLastWindow()。问题在于:
getContext(this)在@Entry struct层面返回的是UIContext(UIAbilityContext 的父接口)- 而
preferences.getPreferences()的签名要求参数是UIAbilityContext
TypeScript 里遇到这种"父接口不能赋给子接口"的情况,我随手就写了 // @ts-ignore 跳过——反正运行时肯定没问题。ArkTS 严格模式直接把这种偷懒行为一棍子打死:
ArkTS 不允许用任何内联注释(
@ts-ignore、@ts-expect-error、// ArkTS ignore)跳过类型检查。你要么正确缩窄类型,要么用显式类型转换as。
修复代码
把 @ts-ignore 替换成显式类型转换 as common.UIAbilityContext。ArkTS 允许 as 做类型断言,但不允许 @ts-ignore 绕过检查:
// StickyNote.ets —— 修复后的 initPreferences()
import common from '@ohos.app.ability.common';
private async initPreferences(): Promise<void> {
try {
// ✅ 显式类型转换,告诉编译器:我知道这里 getContext 返回的是 UIContext,
// 但在 EntryAbility 运行时上下文里,它的实际实现就是 UIAbilityContext,
// 我用 as 断言来缩窄,比 @ts-ignore 安全得多
const context: common.UIAbilityContext =
getContext(this) as common.UIAbilityContext;
this.pref = await preferences.getPreferences(context, 'StickyNote');
this.content = await (this.pref.get('content', this.content) as Promise<string>);
this.posX = await (this.pref.get('posX', this.posX) as Promise<number>);
this.posY = await (this.pref.get('posY', this.posY) as Promise<number>);
} catch (err) {
const e = err as BusinessError;
console.error(`[StickyNote] Preferences 初始化失败: ${e.code} ${e.message}`);
}
}
private async moveWindowTo(x: number, y: number): Promise<void> {
try {
// ✅ 同一处修复:window.getLastWindow 也要求 UIAbilityContext
const context: common.UIAbilityContext =
getContext(this) as common.UIAbilityContext;
const win: window.Window = await window.getLastWindow(context);
await win.moveWindowTo(x, y);
} catch (err) {
const e = err as BusinessError;
console.error(`[StickyNote] 移动窗口失败 code=${e.code} msg=${e.message}`);
}
}
教训一句话:as 是 ArkTS 给你的合法逃生口,但 @ts-ignore 是违规操作。把所有 @ts-ignore 改成能自圆其说的 as 断言,既通过编译又留下了"我为什么这么做"的代码痕迹——别人(包括半年后的你自己)一眼就能看懂。
最终构建成功日志
5 个 ERROR 全部修完之后,执行 Build > Make Project:
[INFO] hvigor: BUILD SUCCESSFUL in 1m 42s
[INFO] entry: compileArkTs → passed
[INFO] entry: compileHar → passed
[INFO] entry: packHap → HarmonyPCDemo-default-unsigned.hap (2.3 MB)
[INFO] Lint: 0 ERROR, 3 WARNINGS
- TinyMarkdown.ets: 建议将 escapeHtml 导出为单元测试用(INFO,非 ERROR)
- DpiMenuDemo.ets: getDefaultDisplaySync 建议增加模拟器 fallback(INFO)
- DistributedNote.ets: 单设备无分布式场景下订阅回调不会触发(INFO)
[INFO] BUILD TOTAL TIME: 1m 56s
0 ERROR,3 个 WARNING 都是建议级别,不影响产物。

运行效果如下:


总结:ArkTS 严格模式 vs TypeScript 宽松模式的 5 点差异
从这 5 个坑里提炼出来一个认知:ArkTS 不是 TS 的子集,而是带"运行时安全护栏"的 TS 超集。把差异列成一张表,以后遇到 ERROR 直接查:
| # | 规则 | TypeScript 宽松模式 | ArkTS 严格模式 | 本文对应 ERROR |
|---|---|---|---|---|
| 1 | 匿名对象字面量 | ✅ 允许 export default { xxx } | ❌ 必须先声明 interface/class | ERROR 1 |
| 2 | 基类同名成员覆盖 | ✅ 允许(会覆盖) | ❌ 签名不兼容就报错 | ERROR 2 |
| 3 | API 必填字段演进 | ✅ 旧字段缺省值兜底 | ❌ 接口更新后必填字段缺一个就编译失败 | ERROR 3 |
| 4 | 联合类型赋值 | ✅ as string 直接强转就行 | ⚠️ 必须 typeof 缩窄后才能赋值 | ERROR 4 |
| 5 | 类型检查绕过 | ✅ @ts-ignore、any、// @ts-expect-error | ❌ 全部禁用,只允许 as 显式断言 | ERROR 5 |
把这 5 条记在 DevEco Studio 的 “External Snippets” 里,下次看到 ERROR 编号 arkts-no-xxx 就能 3 秒定位根因。
常见报错与排查
上面 5 个 ERROR 是"编译期"的坑,实际跑起来还有一批"运行期"的坑。下面这张表把两类问题放在一起,按「现象 → 原因 → 解决方式」三列对照,每行都能在正文代码里找到对应处理逻辑,遇到问题直接对号入座。
| 现象 | 原因 | 解决方式 |
|---|---|---|
createWindow 直接抛异常,日志里带 permission denied 或 201 错误码 | 未申请 ohos.permission.SYSTEM_FLOAT_WINDOW 就创建 TYPE_FLOAT 窗口;或 module.json5 里漏声明该权限,安装时即被拒 | 先在 module.json5 的 requestPermissions 中声明权限,运行时用 requestPermissionsFromUser 动态申请,authResults[0] === 0 才继续创建(见 ERROR 5 的 initPreferences 上下文获取写法与权限申请范式) |
| 权限被拒后按钮点了没反应,用户不知道去哪开 | 敏感权限二次申请不再弹系统框,代码里没有兜底引导 | 弹窗说明用途 + startAbility 拉起应用权限设置页,跳转失败再用 showToast 提示手动路径;对应正文「权限被拒后的兜底」handlePermissionDenied 逻辑 |
| 浮窗被状态栏/导航栏压住,视觉上像"被裁掉一半" | moveWindowTo 用的是绝对坐标,未扣除安全区高度 | 用 getWindowAvoidArea(TYPE_SYSTEM) 读取 topRect / bottomRect 高度,坐标计算时把安全区算进去,最后用 Math.min/max 夹紧边界(见「要点 3.1」的 calcFloatPosition) |
| 横竖屏切换后浮窗跑到屏幕外或压住状态栏 | 旋转后屏幕宽高互换,仍沿用旋转前的绝对坐标 | 监听 windowSizeChange,按「锚点比例 + 安全区」重新计算并 moveWindowTo;持久化时存 anchorRatio 而非 x/y(见「要点 3.1」的 relocateOnRotate / bindRotateListener) |
| App 已退出,浮窗还挂在桌面上 | 只调了 hideWindow 没调 destroyWindow,或 Ability onDestroy 里漏了销毁 | hideWindow 与 destroyWindow 分离使用,Ability onDestroy 中统一调用 destroyFloat() 并把 floatWin 置 null,同时 win.off('windowSizeChange') 解绑监听(见「要点 2」与核心代码第 3 段) |
| 重启应用后浮窗坐标丢失,每次都回到默认位置 | Preferences.put 只写内存,没有 flush 落盘;或进程被杀前未触发保存 | put 之后必须 await store.flush();读取时用 get 的第二个参数做默认值兜底,再叠加 Number.isFinite 校验脏数据(见「要点 5」的 saveFloatPosition / loadFloatPosition) |
| 连点按钮出现多个浮窗互相覆盖 | 创建前没有判断实例是否已存在 | floatWin 非空时直接 showWindow 复用,不再重复 createWindow(见核心代码 showFloat 的复用判断) |
| 浮窗内点击穿透到下层应用,误触频繁 | 浮窗与下层应用的手势未隔离,交互做得过重 | 浮窗只放「一键触发」,复杂操作下沉到主 Ability 或半模态;必要时给浮窗根容器加手势拦截(见「要点 4」) |
编译报 arkts-no-untyped-obj-literals / arkts-no-ts-ignore 等 arkts-no-xxx | 用了 TS 宽松写法:匿名对象字面量导出、@ts-ignore 绕过检查 | 先声明 interface 再赋值导出;把 @ts-ignore 换成 as TargetType 显式断言(见 ERROR 1 与 ERROR 5 的修复代码) |
排查顺序建议:先看编译日志的 rule: 行定位规则编号 → 再看运行日志的错误码 → 最后对照本表找对应代码段。绝大多数问题都能在「权限、层级、位置、状态、生命周期」这五类里归位。
运行期实战:浮窗权限与安全区踩坑复盘
编译期 5 个 ERROR 清完之后,BUILD SUCCESSFUL 只代表"能装到机器上",真正让人抓狂的是点下按钮那一刻的静默失败。下面三个场景是我在鸿蒙 PC 上做悬浮便签时踩得最深的运行期坑,每个都附上真实日志、根因和修复后的 ArkTS 代码。
场景 1:SYSTEM_FLOAT_WINDOW 权限被拒后的兜底引导
真实报错日志原文
[WindowManager] createWindow failed: {
"code": 201,
"message": "Permission denied. The application does not have permission to create a float window."
}
[StickyNote] showFloat error: 201 Permission denied
[StickyNote] 用户点击"显示浮窗"按钮,界面无任何反馈
根因分析
ohos.permission.SYSTEM_FLOAT_WINDOW 属于系统级敏感权限,有两个硬性前提:
module.json5的requestPermissions里必须声明,否则安装时就被过滤,运行时申请直接返回201;- 首次申请会弹系统授权框,一旦用户点了"拒绝",后续再调
requestPermissionsFromUser不会再弹框,而是直接返回authResults[0] === -1。
我最初的代码只写了"申请 → 判断结果 → 失败就 console.error",用户看到的就是"按钮点了没反应"。正确做法是:权限被拒后必须主动引导用户去设置页手动开启,否则这个功能永远不可用。
修复代码
// StickyNote.ets —— 权限申请 + 被拒兜底引导
import abilityAccessCtrl, { Permissions } from '@ohos.abilityAccessCtrl';
import common from '@ohos.app.ability.common';
import bundleManager from '@ohos.bundle.bundleManager';
import { BusinessError } from '@ohos.base';
const FLOAT_PERMISSION: Permissions = 'ohos.permission.SYSTEM_FLOAT_WINDOW';
/** 申请浮窗权限:成功返回 true,被拒则走兜底引导 */
private async ensureFloatPermission(): Promise<boolean> {
const context = getContext(this) as common.UIAbilityContext;
const atManager = abilityAccessCtrl.createAtManager();
// ① 先查当前授权状态,已授权直接放行,避免重复弹框
const tokenId = await bundleManager.getBundleInfoForSelf(
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
).then(info => info.appInfo.accessTokenId);
const status = await atManager.checkAccessToken(tokenId, FLOAT_PERMISSION);
if (status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
return true;
}
// ② 未授权则发起动态申请(仅首次会弹系统框)
try {
const result = await atManager.requestPermissionsFromUser(context, [FLOAT_PERMISSION]);
// authResults[0] === 0 表示用户点了"允许"
if (result.authResults[0] === 0) {
return true;
}
} catch (err) {
const e = err as BusinessError;
console.error(`[StickyNote] requestPermissionsFromUser failed: ${e.code} ${e.message}`);
}
// ③ 被拒(或二次申请不再弹框)→ 弹窗说明用途并引导去设置页
this.handlePermissionDenied();
return false;
}
/** 权限被拒后的兜底:弹窗 + 跳转应用权限设置页 */
private handlePermissionDenied(): void {
AlertDialog.show({
title: '需要悬浮窗权限',
message: '便签需要悬浮在其他应用之上才能常驻桌面。请在「设置 → 应用 → HarmonyPCDemo → 权限」中手动开启「悬浮窗」权限。',
primaryButton: {
value: '去开启',
action: () => { this.openPermissionSettings(); }
},
secondaryButton: {
value: '暂不',
action: () => { /* 用户放弃,不做任何事 */ }
}
});
}
/** 跳转到本应用的权限设置页;跳转失败再降级为 Toast 提示手动路径 */
private openPermissionSettings(): void {
const context = getContext(this) as common.UIAbilityContext;
// 方式一:用 openLink 拉起系统设置的应用详情页(推荐,兼容性好)
context.openLink(`app-settings://${context.applicationInfo.name}`)
.catch((err: BusinessError) => {
console.error(`[StickyNote] openLink failed: ${err.code} ${err.message}`);
// 方式二:降级用 startAbility 拉起系统设置 Ability
const want: Want = {
bundleName: 'com.huawei.hmos.settings',
abilityName: 'com.huawei.hmos.settings.MainAbility',
uri: 'application_info_entry',
parameters: { pushParams: context.applicationInfo.name }
};
context.startAbility(want).catch((e: BusinessError) => {
// 两种方式都失败:至少告诉用户手动路径,别让按钮"点了没反应"
console.error(`[StickyNote] startAbility failed: ${e.code} ${e.message}`);
promptAction.showToast({ message: '请手动前往 设置 → 应用 → 权限 开启悬浮窗' });
});
});
}
要点:checkAccessToken 先查状态可以避免"已授权还弹框"的骚扰;openLink 失败必须降级到 startAbility,再失败至少给 Toast——任何一层都不能静默吞掉。
场景 2:getWindowAvoidArea 计算安全区,浮窗被状态栏压住
真实报错日志原文
[StickyNote] moveWindowTo(1200, 0) success
[StickyNote] 浮窗顶部被状态栏遮挡,标题栏不可见
[Display] screenHeight=1440, statusBarHeight=48, navigationBarHeight=64
根因分析
moveWindowTo(x, y) 用的是屏幕绝对坐标,原点在屏幕左上角,包含状态栏区域。我一开始直接把"用户拖拽的相对位置"当成绝对坐标传进去,y = 0 时浮窗顶部正好压在状态栏下面,视觉上就像"被裁掉一半"。
正确做法是先用 getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM) 拿到系统避让区(状态栏 topRect、导航栏 bottomRect),把可用区域收缩成"安全区",再在安全区内做坐标计算,最后用 Math.min/max 夹紧边界,防止浮窗跑出屏幕。
修复代码
// StickyNote.ets —— 基于安全区计算浮窗坐标
import window from '@ohos.window';
/** 读取当前窗口的安全区(扣除状态栏、导航栏) */
private async getSafeArea(win: window.Window): Promise<SafeArea> {
// TYPE_SYSTEM:系统避让区,包含状态栏与导航栏
const avoidArea = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
const screenRect = win.getWindowProperties().windowRect;
// 安全区 = 屏幕区域 - 顶部状态栏 - 底部导航栏
const top = avoidArea.topRect.height;
const bottom = avoidArea.bottomRect.height;
return {
left: 0,
top: top,
right: screenRect.width,
bottom: screenRect.height - bottom,
width: screenRect.width,
height: screenRect.height - top - bottom
};
}
/** 把浮窗放到安全区内的指定位置,越界自动夹紧 */
private async moveFloatToSafe(x: number, y: number): Promise<void> {
if (!this.floatWin) {
return;
}
const area = await this.getSafeArea(this.floatWin);
const winW = this.floatWidth;
const winH = this.floatHeight;
// 夹紧:左边界不小于安全区左边,右边界不超出安全区右边
const safeX = Math.min(Math.max(x, area.left), area.right - winW);
// 夹紧:上边界必须让开状态栏,下边界必须让开导航栏
const safeY = Math.min(Math.max(y, area.top), area.bottom - winH);
try {
await this.floatWin.moveWindowTo(safeX, safeY);
console.info(`[StickyNote] moveWindowTo(${safeX}, ${safeY}) in safeArea top=${area.top} bottom=${area.bottom}`);
} catch (err) {
const e = err as BusinessError;
console.error(`[StickyNote] moveWindowTo failed: ${e.code} ${e.message}`);
}
}
要点:安全区不是常量——状态栏高度会随"显示/隐藏状态栏"变化,导航栏在 PC 上可能是手势条也可能是三键导航,所以每次移动前都重新读一次,不要缓存。
场景 3:横竖屏切换后 moveWindowTo 重定位
真实报错日志原文
[StickyNote] windowSizeChange: width=1440, height=900 → width=900, height=1440
[StickyNote] 浮窗仍停留在 (1200, 300),右侧超出屏幕 300px
[StickyNote] 浮窗部分区域不可见,无法拖回
根因分析
旋转后屏幕宽高互换(1440×900 → 900×1440),但我保存的是旋转前的绝对坐标 (1200, 300)。新屏幕宽度只有 900,x = 1200 直接跑到屏幕外,浮窗"消失"了。
根因有两层:
- 没有监听旋转事件——
windowSizeChange触发了,但代码里没绑定回调; - 持久化的是绝对坐标——绝对坐标和屏幕尺寸强耦合,换分辨率、换方向就失效。
修复思路:监听 windowSizeChange,旋转后按锚点比例(浮窗中心点占屏幕的百分比)重新换算坐标,再走场景 2 的安全区夹紧逻辑。
修复代码
// StickyNote.ets —— 监听旋转 + 按锚点比例重定位
import window from '@ohos.window';
/** 绑定屏幕尺寸变化监听(旋转、分屏、分辨率切换都会触发) */
private bindRotateListener(win: window.Window): void {
win.on('windowSizeChange', (size: window.Size) => {
console.info(`[StickyNote] windowSizeChange: ${size.width}x${size.height}`);
// 旋转后重新计算位置,异步执行避免阻塞回调
this.relocateOnRotate(size).catch((err: BusinessError) => {
console.error(`[StickyNote] relocateOnRotate failed: ${err.code} ${err.message}`);
});
});
}
/** 旋转后按锚点比例重新定位浮窗 */
private async relocateOnRotate(size: window.Size): Promise<void> {
if (!this.floatWin) {
return;
}
// ① 用旋转前的锚点比例(浮窗中心占屏幕的百分比)换算新坐标
// anchorRatioX / anchorRatioY 在拖拽结束时保存,取值 0~1
const centerX = this.anchorRatioX * size.width;
const centerY = this.anchorRatioY * size.height;
// ② 由中心点反推左上角坐标
const targetX = centerX - this.floatWidth / 2;
const targetY = centerY - this.floatHeight / 2;
// ③ 复用安全区夹紧逻辑,保证旋转后不压状态栏、不越界
await this.moveFloatToSafe(targetX, targetY);
// ④ 旋转后坐标已变,重新落盘,避免下次启动用旧坐标
await this.saveFloatPosition();
}
/** 拖拽结束时记录锚点比例(替代直接存 x/y) */
private updateAnchorRatio(x: number, y: number): void {
const screen = display.getDefaultDisplaySync();
// 记录浮窗中心点相对屏幕的比例,与分辨率解耦
this.anchorRatioX = (x + this.floatWidth / 2) / screen.width;
this.anchorRatioY = (y + this.floatHeight / 2) / screen.height;
}
要点:持久化时存 anchorRatioX / anchorRatioY 而不是 x / y,这样换设备、换分辨率、横竖屏切换都能还原到"视觉上差不多"的位置;旋转回调里必须重新 saveFloatPosition(),否则下次启动还是旧坐标。
三个场景串起来就是一条完整的运行期链路:申请权限(失败则引导)→ 计算安全区 → 定位并夹紧 → 监听旋转重定位 → 落盘。编译期靠 rule: 编号定位,运行期靠错误码 + 日志定位,两套动作练熟,鸿蒙 PC 的浮窗功能基本不会再"点了没反应"。
AtomGit 仓库链接
本文五个 ERROR 对应的源码已按编号单独归档(与完整工程最新提交逐字节一致):
👉 https://atomgit.com/wdracky/harmony-pc-arkts-strict
仓库 README 里有「ERROR 1~5 → 文件」对照表:
| 文章小节 | 文件(仓库内路径) |
|---|---|
ERROR 1 arkts-no-untyped-obj-literals | entry/src/main/ets/utils/TinyMarkdown.ets |
ERROR 2 基类 scale 重名 | entry/src/main/ets/pages/DpiMenuDemo.ets |
| ERROR 3 / ERROR 4 distributedKVStore | entry/src/main/ets/pages/DistributedNote.ets |
ERROR 5 禁用 @ts-ignore | entry/src/main/ets/pages/StickyNote.ets |
| 演示页(Home 按钮 ⑤) | entry/src/main/ets/pages/Index.ets + entry/src/main/ets/kmp/KmpDateTime.ets |
配套的 hypium 用例 TinyMarkdown.test.ets 也在仓库里,用于守住 ERROR 1 的修复不回归。
https://atomgit.com/wdracky/harmony-pc-arkts-strict
结语与后续计划
5 个 ERROR 修完、构建日志变成 BUILD SUCCESSFUL 的那一刻,最大的收获不是"改对了哪一行代码",而是建立起了一套面对 ArkTS 报错的固定动作:
- 先读
rule:行——arkts-no-xxx编号就是官方 lint 规则名,直接搜官方文档能拿到规则原文,比盲猜快十倍。 - 再判断是"写法问题"还是"API 演进问题"——前者改代码结构(声明 interface、改名、换
as),后者查 SDK 变更日志(Options必填字段、回调签名)。 - 最后回到构建验证——ArkTS 一次只报一批,改完必须重新构建,别指望一次全清。
后续我还会继续补充两类内容:
- 运行期实战:浮窗权限申请、安全区计算、横竖屏重定位、Preferences 落盘这些"编译能过但跑起来会翻车"的坑,会单独成篇。
- ArkTS 严格模式规则速查表:把
arkts-no-xxx系列规则按"对象字面量 / 类型断言 / 继承覆盖 / API 兼容"分类整理,做成一张可打印的对照表。
如果你在鸿蒙 PC 开发中也踩到了本文没覆盖的 ERROR,欢迎在评论区贴出报错原文和 rule: 编号,我会挑典型的补进后续版本。排错这件事,一个人踩坑是事故,一群人共享就是文档。
更多推荐



所有评论(0)