
1. 三角关系是什么先搞清楚谁负责什么很多刚接触鸿蒙开发的人第一次看到 ArkTS、ArkUI、SDK/API 这三个词时都会有一种“好像每个都认识但组合起来就糊涂了”的感觉。尤其是当你打开 DevEco Studio 新建一个工程看到一大堆后缀为.ets的文件时这种困惑会瞬间放大我到底是在写 ArkTS还是在写 ArkUI那些ohos.*开头的引入又是谁的 API先说结论ArkTS 是语言ArkUI 是界面框架SDK/API 是系统能力接口。这三者不是并列关系而是三层不同维度的东西只不过在.ets源码文件里被写在了一起所以让很多人误以为它们是同一种技术。如果这个结论还不够直观想象一下你在一家餐厅后厨工作。ArkTS 是你炒菜用的锅和勺它决定了你能怎么翻勺、怎么控制火候ArkUI 是菜单和摆盘模板它定义了一道菜端上桌时长什么样、按什么顺序上菜SDK/API 则是后厨的水电气和食材供应链你需要盐、需要水、需要电都得通过这些管道去获取。你做一道菜三者缺一不可但它们各自负责的事情完全不同。这个“三角关系”理不顺后面的开发一定会踩坑。我在帮别人 review 代码时见过太多问题其实都源于职责边界没分清有人把业务逻辑全塞进 UI 的build()方法里有人在一个纯逻辑类里 import 了 UI 组件还有人根本分不清哪些能力需要调 API、哪些能力 ArkUI 自己就提供了。1.1 一种更贴近代码的拆解方式我们用更 code 的方式来理解。ArkTS 是你在.ets文件里写的那层语言本身它规定了变量怎么声明、类型怎么定义、函数怎么写、类怎么组织。ArkTS 不是一门全新的语言它是在 TypeScript 的基础上做了约束和增强最核心的变化就是强制静态类型——这在后面讲踩坑时会重点展开。ArkUI 则是一套声明式 UI 框架它提供了一组被称为“UI 组件”的积木Text、Button、Column、Row、List、Scroll等等以及一套状态管理机制。你在代码里写Button(点击)这不是 ArkTS 的语法而是 ArkUI 框架暴露出来的组件接口。ArkTS 只是负责把这些组件描述出来真正把界面渲染到屏幕上的是 ArkUI 的运行时引擎。SDK/API 则是两层含义。SDK 是你在 DevEco Studio 里配置的那套开发套件它包含了编译器、调试器、模拟器镜像、系统库声明文件等API 是这套 SDK 提供给应用调用的系统能力接口比如获取设备信息、调用相机、申请权限、网络请求、数据持久化等。在代码里它们体现为import进来的那些模块例如import { camera } from kit.CameraKit; import { http } from kit.NetworkKit; import { BusinessError } from kit.BasicServicesKit;注意这些kit.*或ohos.*开头的模块就是系统 API 在 ArkTS 中的入口。1.2 一条业务需求的完整流转链路把三者放进同一条业务链路里看关系会更清楚。假设你要实现一个功能点击一个按钮从网络拉取一段文本并显示到界面上。第一步你在 ArkUI 的build()方法里声明一个Button它的onClick事件里调用一个你自定义的、用 ArkTS 写的函数。第二步这个 ArkTS 函数内部调用 SDK 提供的网络请求 APIhttp.createHttp()发起异步请求。第三步请求返回后你再用 ArkTS 把数据赋值给一个用 ArkUI 状态装饰器比如State修饰的变量。第四步ArkUI 检测到这个状态变了自动重新渲染把新文本显示到Text组件里。整个过程里ArkUI 负责“界面长什么样、状态变了怎么刷新”ArkTS 负责“逻辑怎么写、数据怎么整理”SDK/API 负责“系统能力从哪来、网络请求怎么发出去”。任何一个环节出问题功能都跑不通但它们彼此又不互相替代。理解这条链路后你再看文档、搜报错、看源码都会比之前清晰得多。2. ArkTS 和 ArkUI 的职责边界最容易混淆的一对如果说三角关系里最容易让人迷惑的部分那一定是 ArkTS 和 ArkUI 之间的边界。SDK/API 好歹是import进来的外部东西物理边界还算清楚但 ArkTS 和 ArkUI 写在同一个文件里语法又长得很像很多初学者根本分不清某个写法到底是语言特性还是框架特性。这个区分真的很重要因为它直接决定了你查资料时搜索什么关键字、报错时去哪个文档找答案、设计代码结构时把逻辑放到哪个层次。2.1 ArkUI是声明式描述ArkTS是逻辑执行ArkUI 的核心思想是“声明式”你描述界面应该是什么样而不是一步步告诉系统怎么画。比如Entry Component struct HelloPage { State message: string Hello HarmonyOS; build() { Column() { Text(this.message) .fontSize(24) .fontWeight(FontWeight.Bold) Button(更新) .onClick(() { this.message Hello ArkUI; }) } .width(100%) .padding(16) } }这里Entry、Component、State、build()、Text()、Button()、.width()、.padding()这些东西全部都是 ArkUI 框架提供的语法和接口。ArkTS 在里面只负责两件事一是声明message这个变量的类型是string并初始化它二是写onClick回调里那句this.message Hello ArkUI的赋值逻辑。为了验证这个区分你可以做个简单的思想实验如果你在build()方法里写一个for循环或者定义一个局部变量然后做复杂运算代码是可以跑的但设计上并不推荐。ArkUI 的build()方法讲究的是“简洁地描述 UI”它每次刷新时都可能重新执行在里面做耗时逻辑、网络请求、文件读写相当于每次界面刷新都把这些事情重跑一遍性能一定会出问题。2.2 状态管理到底属于谁状态管理是 ArkTS 与 ArkUI 交叉最密集、也最容易搞混的区域。从表面上看State、Prop、Link、Provide、Consume、Observed、ObjectLink这些装饰器都是出现在代码里的“注解”它们修改的又是普通 ArkTS 变量所以很多人默认它们是 ArkTS 的一部分。但实际上这套状态管理机制是 ArkUI 框架的核心能力只不过它的设计方式是“通过装饰器扩展了 ArkTS 的语法”。理解这一点有什么实际意义当你需要处理跨页面数据共享时你会明白这不只是“声明一个全局变量”这么简单而是要选择一个 ArkUI 框架支持的状态管理方案。比如全局 AppStorage、LocalStorage或者通过Provide/Consume做跨层级传递这些都不是 ArkTS 语言层面的能力而是 ArkUI 为应用开发提供的数据流通管道。反过来有些东西则是 ArkTS 语言层面的事情与 UI 完全无关。比如你定义一个普通的数据模型类class UserInfo { name: string ; age: number 0; getDisplayName(): string { return ${this.name}(${this.age}); } }这个类里面没有Observed没有State它完全是一个 ArkTS 的纯逻辑类。它可以在 UI 组件里实例化、也可以在网络层使用它不依赖 ArkUI 的任何运行时。想清楚这一层你就知道“业务模型应该尽量做成不依赖 UI 框架的纯 ArkTS 类UI 层的装饰器不要污染到模型层。” 这也是代码分层设计的基本功。2.3 一个常见的混淆误区的剖析我在不少论坛帖子和代码仓库里看到过这种写法在自定义的纯逻辑类里使用装饰器或者反过来在 ArkUI 组件文件里用面向对象的方式写一大堆与 UI 无关的复杂业务逻辑。先说前一种它通常表现为// 错误示范在纯逻辑类中使用 UI 状态装饰器 Observed class DataService { State count: number 0; // ... }State只能用在Component修饰的 struct 组件内部放在普通类里是无效的编译器甚至不会把它当作 ArkUI 状态来处理。你期望的“响应式更新”根本不会发生。再说后一种把大量业务逻辑塞进组件看起来能跑但组件会越来越臃肿状态被State装饰得密密麻麻最后 UI 一变就牵一发动全身测试也没法写。正确的边界感是ArkTS 代码负责“如何计算”ArkUI 负责“如何显示”API 负责“数据从哪来和系统能力从哪调用”。在写代码之前你先问一句自己这段代码是和界面显示强相关还是纯粹的逻辑处理如果是后者就把它移到独立的 ArkTS 模块里如果是前者就留在组件内并用合适的 ArkUI 状态装饰器管理。3. SDK/API 在三角关系中的位置能力的“供给侧”ArkTS 和 ArkUI 解决的是“应用内部怎么写、怎么显示”的问题但一个应用不可能活在真空里。你需要访问网络、读写文件、调用摄像头、申请权限、获取设备信息这些能力统统不在 ArkTS 语言和 ArkUI 框架的职责范围内它们是操作系统提供给应用的“系统能力”也就是 SDK/API 层。很多人的困惑在于这些 API 和 ArkTS、ArkUI 是怎么结合起来的为什么有些 API 可以返回一个对象让 UI 直接绑定有些 API 只能通过回调传数据要回答这些问题得先把 API 的分类和调用方式弄清楚。3.1 全局 navigation 与自定义动画场景中的 API 调用用近期鸿蒙开发圈子里讨论比较多的“自定义动画全局 navigation”场景来举例最能说明 API 在三角关系里的角色。假设你要实现一个全局页面跳转动效从首页点击列表项页面从右侧滑入同时列表项的背景色有一个渐变动画。这里你会用到三部分能力ArkUI 的Navigation组件和它提供的路由管理能力这部分属于 UI 框架层。通过Navigation的navDestination来声明页面目标通过bindSheet或自定义transition来实现转场。ArkUI 的动画能力包括animateTo()、transition()、keyframeAnimateTo()等。这些 API 的调用方式是“把属性变化包在动画闭包里”它们负责驱动 UI 状态的变化过程。如果是更复杂的动画比如粒子效果、物理模拟你可能还需要调用图形图像相关的系统 API比如kit.ArkGraphics2D里的能力或者直接操作Canvas组件。这些就属于 SDK/API 层了。我见过一个实际项目开发者在做自定义动画时把整个动画状态机逻辑写进了build()方法里用setInterval去反复改一个State变量结果动画卡顿明显。后来改成用 ArkUI 的animateTotransition组合配合Navigation的系统转场接口流畅度立刻上来了。这个例子想说明的是动画效果的“描述与驱动”属于 ArkUI但动画涉及的数据来源比如从网络加载图片素材、读取本地 Lottie 文件属于 API这两者要分开。很多小白以为动画卡顿就是 API 调用慢其实往往是把 API 调用放错了位置在 UI 渲染路径里同步做了网络请求。3.2 同步执行还是异步回调理解 API 的两个执行模型接触 SDK/API 时你一定会看到两类签名风格。一类是同步返回结果的比如let deviceInfo deviceManager.getDeviceInfo(deviceId);另一类是异步的通常以回调或 Promise 形式存在http.createHttp().request(url, (err, data) { // 回调里处理结果 }); // 或者 Promise 风格 let resp await http.createHttp().request(url);我在教学时经常遇到一个问题初学者不理解为什么 API 要分同步异步甚至有人把异步 API 直接当同步用结果拿到的值是初始值。其实核心原因在于系统 API 背后往往对应着跨进程调用或 I/O 操作。比如从相册读取一张图片系统需要访问媒体库服务可能还要解码、读取文件这些操作耗时远高于普通函数调用。如果在 UI 主线程里同步执行界面就会卡住。所以系统把这类能力设计成异步让你通过回调或 Promise 拿到结果不阻塞 UI。反过来一些纯本地、耗时极短的操作如读一个同步键值对则会提供同步接口。理解了这一点你就能预测一个 API 大概是同步还是异步而不是每次都去翻文档。判断标准就一条这个操作是否涉及文件系统、网络、跨进程、硬件访问如果是几乎一定是异步。3.3 从热搜词看 API 版本的管理误区我在整理资料时注意到一个有趣的热搜词“android sdk 安装 api 35”。虽然这是安卓开发的东西但它反映出一个共性问题开发者很容易把“ SDK 版本”和“API 版本”混为一谈进而引发依赖冲突和运行时异常。鸿蒙开发也不例外。在鸿蒙开发中你在 DevEco Studio 里配置的compileSdkVersion编译 SDK 版本、targetSdkVersion目标 API 版本、以及设备侧的系统版本三者是不同维度的事情。你用一个高版本的 SDK 编译不代表低版本设备就能跑你把targetSdkVersion调得很高系统就会启用新版的行为兼容策略比如权限规则变化、后台行为限制这些都可能让你的老代码在新版本系统上“突然”出问题。我在两个不同的项目里都遇到过一个是用旧 API 请求相机权限后在新版本设备上运行系统直接拒绝授权另一个是某个 API 在 API 12 后废弃了团队升级 SDK 时没注意编译显示告警运行时行为和新旧行为不一致产生了很难排查的 Bug。所以关于 API 版本有一条朴素的建议非常管用不要盲目追求最新 API要以你目标设备的最老系统版本来约束你使用 API 的版本。每个 API 在查阅文档时都看一眼睛它标注的最低支持版本since 或 since 标识确保它覆盖你的目标设备。另外升级 SDK 版本后至少要跑一遍全量回归尤其关注路由、权限、后台任务这三类行为改动多的地方。4. 三角关系中最容易踩的坑从编译错误到运行异常把 ArkTS、ArkUI、SDK/API 的关系讲清楚之后接下来这部分才是真正的干货——实际开发中常见的踩坑点。我收集了自己和身边同事踩过的、以及在社区里高频出现的问题按类型整理成三类每一类都是“三角关系”某个环节出了偏差直接导致的。4.1 类型不匹配ArkTS 强类型对 API 返回值的约束ArkTS 是强类型语言这一点和 TypeScript 不同。TS 在编译期有各种隐式转换和宽松检查但 ArkTS 为了保证运行时性能和安全做了更严格的类型约束。最典型的结果是API 返回的Object类型不能直接当具体类型用必须做类型断言或显式转换。举个真实例子我在一个项目里从 Preference 数据库读取数据let value preferences.get(userName, default); // 返回类型是 ValueType可能是 string | number | boolean 等 let name: string value as string; // 需要断言如果缺少断言ArkTS 编译器会报类型不兼容错误。刚开始很多人不理解觉得“TS 不是能自动推导吗”。这是最大的认知偏差ArkTS 不允许unknown隐式收窄你必须自己处理类型。这个设计会让代码啰嗦一点但它能把很多运行时的类型错误提前到编译期发现长期看是值得的。实际写代码时我的习惯是给所有 API 返回值的接收处加一个明确的类型注解避免让编译器去猜。特别是处理 JSON 解析时不要上来就JSON.parse()完直接访问属性因为返回的是Object你应该先定义一个接口再通过类型断言把它转成你的数据模型否则运行时访问不存在属性时会直接抛异常。4.2 UI 线程与耗时任务API 调用位置不对导致的卡顿前面说过系统 API 有很多是异步的但异步 API 也有回调执行线程的概念。鸿蒙中ArkUI 的 UI 操作只能在主线程执行而一些 API 的回调可能跑在子线程。我在一个项目里就踩过这样的坑从文件服务读取图片后直接在回调里更新State变量。起初代码看起来没什么问题fileIo.open(path, (err, file) { // 这里不能确定是否在主线程 this.imagePath path; // 更新 UI 状态 });在某种情况下这个回调确实运行在子线程直接改State会触发不了界面刷新或者刷新掉帧。正确的做法是先切到主线程再更新状态。ArkTS 里可以用taskpool或者Emitter来做线程切换也可以利用async/await结合主线程调度器。更稳妥的实践原则是把所有涉及 UI 状态更新的逻辑放回到主线程执行子线程只做数据计算和 I/O。判断自己在哪个线程最直接的方法是打日志看线程信息别靠猜测。还有一类隐蔽的问题把耗时同步 API 直接放在aboutToAppear()生命周期里。比如页面加载时要读一个较大的本地数据库用了同步查询接口结果这个查询可能耗时几百毫秒直接就卡在了页面切换的动画上。这种情况应该改为异步查询或者先显示加载占位数据到了再通过状态管理刷新。4.3 生命周期与 API 释放资源性 API 的典型问题第三类是资源性 API 的使用问题这类坑在新手里出现频率尤其高。什么是资源性 API就是调用后占用了系统资源使用完毕需要主动释放的 API。典型代表有camera相机预览流、audioCapturer音频采集、fileIo文件句柄、Location定位、http的长连接等。问题通常出现在组件销毁时开发者创建了资源但忘记在合适的生命周期回调如aboutToDisappear()里释放。结果就是一个页面反复进出资源不断累积最终系统报“Out of memory”或“No more handlers”。我之前调试过一个 Bug用户反复进入一个带相机预览的页面几次之后页面黑屏Logcat 里大量Camera service add failed错误。最终原因就是前一次未释放 camera 实例新实例创建失败。这类问题的排查思路其实很固定拿到资源时问自己一句“这个 API 需不需要 release/close/stop”在组件销毁生命周期里统一释放不要散落在各个回调里。如果资源在子页面使用要考虑页面没有真正销毁比如被 NavDestination 缓存的场景——这时候aboutToDisappear不一定会触发要结合onPageHide或路由监听来管理。还有一点需要特别提醒资源和 UI 状态一样也要遵循“谁创建、谁释放”的原则。如果一个 API 在 ViewModel 或数据仓库层创建了释放逻辑也要在那个层面处理不要依赖 UI 组件的生命周期去释放业务层的资源。5. 构建一个能跑通的最小示例把三角关系串起来理论讲再多不如一个能跑的最小示例。这一节我设计一个非常典型的小场景把 ArkTS、ArkUI、SDK/API 三者的协作关系完整串起来点击按钮请求网络上的一段 JSON 数据解析后显示在列表里并带一个简单的加载状态。5.1 场景与目标这个示例不复杂但它覆盖了三层完整的使用链路ArkUI 层Column、Button、List、LoadingProgress组件State状态装饰器。ArkTS 层定义一个数据模型接口写一个独立的请求服务类。API 层使用kit.NetworkKit的 HTTP 请求能力获取远端数据。通过这个示例你能看到三者的边界在哪里调用的顺序是什么以及异常处理该怎么放。5.2 代码结构与关键实现先看 ArkTS 的数据模型和请求服务这部分不依赖 UI// models/ArticleModel.ets export interface Article { id: number; title: string; author: string; } // services/ArticleService.ets import { http } from kit.NetworkKit; import { BusinessError } from kit.BasicServicesKit; import { Article } from ../models/ArticleModel; export class ArticleService { async fetchArticles(): PromiseArticle[] { const httpRequest http.createHttp(); try { const resp await httpRequest.request( https://example.com/api/articles, { method: http.RequestMethod.GET, expectDataType: http.HttpDataType.STRING } ); const rawData JSON.parse(resp.result as string) as Article[]; return rawData; } catch (err) { const e err as BusinessError; console.error(fetchArticles failed, code: ${e.code}, message: ${e.message}); throw err; } finally { httpRequest.destroy(); } } }注意两个细节resp.result是Object类型必须断言为string再JSON.parseJSON.parse的结果也要断言成Article[]否则 ArkTS 编译器不让你直接返回。另外httpRequest.destroy()放在finally里保证请求结束一定释放资源这就是第 4 节说的资源管理原则。再看 UI 层Entry Component struct ArticleListPage { State articles: Article[] []; State isLoading: boolean false; State errorMsg: string ; private service: ArticleService new ArticleService(); build() { Column({ space: 12 }) { Button(加载文章) .onClick(() { this.loadArticles(); }) if (this.isLoading) { LoadingProgress() .width(40) .height(40) } else if (this.errorMsg.length 0) { Text(加载失败${this.errorMsg}) .fontColor(#FF0000) } else { List({ space: 8 }) { ForEach(this.articles, (item: Article) { ListItem() { Column() { Text(item.title) .fontSize(18) Text(item.author) .fontSize(14) .fontColor(#888888) } .alignItems(HorizontalAlign.Start) .width(100%) .padding(12) .backgroundColor(#F5F5F5) } }, (item: Article) item.id.toString()) } .layoutWeight(1) } } .padding(16) } async loadArticles() { this.isLoading true; this.errorMsg ; try { const data await this.service.fetchArticles(); this.articles data; } catch (err) { this.errorMsg (err as BusinessError).message; } finally { this.isLoading false; } } }State修饰的articles、isLoading、errorMsg是 ArkUI 的响应式状态service.fetchArticles()是通过 API 获取数据Button的onClick和loadArticles的异步逻辑则完全是 ArkTS 的语法。5.3 运行效果与验证跑通这个示例后你可以做一个非常直观的实验来验证三角关系把loadArticles里的this.articles data注释掉换成console.log(data)。你会看到界面完全没有变化因为 ArkUI 的渲染是靠状态驱动的不更新状态API 拿到数据也不会自己显示到界面上。反过来如果你把Button的onClick直接写成this.service.fetchArticles()不赋值给State变量你会发现点击后没有任何反应——API 是发出了数据也回来了但 UI 不知道这件事。这两个小实验做完你对“ArkUI 负责显示、API 负责能力、ArkTS 负责逻辑串联”的理解会比看十篇文章都深刻。我在培训新人时经常让他们做这个练习效果很好。6. 我在实际项目中怎样把握这条关系线如果你已经理解了前面的内容那最后这部分算是一点心得体会。我在几个鸿蒙项目里做过架构设计也帮团队处理过不少编译错误和运行崩溃总结下来把握 ArkTS、ArkUI、SDK/API 这条关系线核心其实就几件事。6.1 一个非常好用的心智模型我在看代码时习惯给每个.ets文件快速分类它到底是以 UI 为主的组件文件还是以逻辑为主的服务文件还是数据模型文件。分类的标准很简单——看它头部 import 了什么如果 import 了kit.ArkUI的组件那它是 UI 文件如果只 import 了系统能力模块那它是逻辑服务如果什么都没 import只有类型定义那它是纯模型文件。这看起来是个很笨的判断方式但它真的能帮你快速识别代码结构问题。假如一个文件明明叫xxxService.ets结果头部 import 了Button、Text说明有人把 UI 逻辑塞进了服务层十有八九会有问题不是状态管理混乱就是生命周期泄漏。对应地代码分层时我的原则是纯数据模型和工具函数只用 ArkTS不 import 任何 kit 模块。业务逻辑服务用 ArkTS 系统 API不引入 UI 组件。页面和组件用 ArkUI ArkTSAPI 调用通过服务层间接完成。这样强制划分后你可以在 UI 层之外充分做单元测试也可以在不改 UI 的情况下替换底层 API 实现比如从 HTTP 切换到 WebSocket。6.2 排查问题时的“三角定位法”当代码出了 Bug我也有一套固定的排查顺序叫“三角定位法”。第一步先判断 Bug 出现在哪一层界面显示不对优先查 ArkUI 的状态和组件写法逻辑结果不对优先查 ArkTS 代码逻辑拿不到数据或权限异常优先查 API 的调用方式和版本兼容性。第二步看层与层的边界有没有被破坏。最常见的情况是数据没显示出来你以为 API 出了问题查了半天发现是 API 正常返回了但内存里的对象和 UI 绑定的对象不是同一个状态管理边界没接好或者是 API 返回的数据类型和模型定义不一致类型断言错误或者是你调 API 的方式让它跑在了错误的位置线程或生命周期。第三步才是去查文档和报错信息。很多人一上来就复制报错去搜索其实在没有先定位层级的情况下搜索效率是非常低的。比如一个类型错误可能是 ArkTS 强类型抛出的也可能是 API 返回结构变化导致的搜索同一个报错可能出来两种截然不同的答案先做层级的判断再去搜能少走一半弯路。6.3 给新入坑的开发者几条实操建议如果把这一整篇文章浓缩成几条可执行的习惯我建议你做到下面这几件小事第一写代码前先声明边界。不要一上来就写 UI先定义数据模型再定义服务类最后才在页面里连线。这样能强制你区分三角关系中每一层的职责。第二把 API 调用包装在服务层里。不要在 UI 组件里直接散落各处调系统 API统一封装之后权限申请、异常处理、资源释放都集中在一个地方出了问题好排查。第三给每个异步 API 调用都加错误处理。鸿蒙的 API 抛错方式有两种回调里给errPromise 里 reject。无论哪种都不能忽略。我看到过太多运行闪退就是因为网络请求失败后没有处理err代码直接继续执行空数据逻辑。第四升级 SDK 版本时主动查一下 deprecated 列表。DevEco Studio 升级时控制台输出的一堆黄色警告不要直接忽略里面往往藏着“你正在使用已废弃 API”的提示。把这些警告清零再继续开发是一个很好的习惯。最后想说的是ArkTS、ArkUI、SDK/API 这三者的关系说透了并不神秘但真正内化成自己的开发直觉还是需要在实际项目中一点点打磨。你可以在接一个小业务需求时刻意练习第 5 节那种“模型-服务-页面”的三层写法故意试错几次再对比一下把逻辑全堆在 UI 里带来的混乱很快就会建立起对这条关系线的肌肉记忆。