为什么鸿蒙选择 ArkTS
HarmonyOS 的应用开发语言是 ArkTS。它基于 TypeScript,但做了两件关键取舍:收敛掉一部分动态特性(如 any、运行时增删对象属性),换来更强的静态检查;同时把声明式 UI 的能力直接内置到语言里(装饰器 + 组件函数)。
为什么要限制动态性?因为在端侧,界面渲染对性能与可预测性要求很高。静态类型让编译器在打包阶段就能完成优化与校验,减少运行时的类型判断开销,也让大型工程的协作更安全——改动一个接口,编译器会替你把所有受影响的调用点找出来。
ArkTS 不是“又一种语言”,而是 TypeScript 的工程化收敛版本:牺牲一部分灵活性,换取一致性、性能与可维护性。
如果你有前端基础,上手 ArkTS 会很快;真正的门槛不在语法,而在状态管理与组件生命周期的心智模型。这两块想清楚了,写页面会变得非常顺手。
声明式语法:描述界面而不是操作界面
声明式 UI 的核心思想是:你只描述“界面应该长什么样”,不去手动操作视图对象。状态变了,框架负责计算差异并更新界面。
ArkTS 的写法是:在 build() 方法里用组件函数(如 Column、Text、Button)描述界面树;组件属性用链式调用设置;子组件用尾随块传入。
@Entry
@Component
struct HelloPage {
@State message: string = '你好,鸿蒙';
build() {
Column({ space: 12 }) {
Text(this.message)
.fontSize(24)
.fontWeight(FontWeight.Bold)
Button('打个招呼')
.onClick(() => { this.message = '你好,ArkTS'; })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
注意几个细节:@Entry 标记页面入口;@Component 标记自定义组件;@State 修饰的变量变化会触发界面刷新;Button 的文本直接写在构造参数里。整段代码没有任何“创建控件、逐个设置属性”的命令式语句——这就是声明式。
类型系统与空安全
ArkTS 在 TypeScript 基础上强化了类型约束:不允许隐式 any,不允许在运行时给对象添加新属性,接口与泛型的使用与 TS 基本一致。
- 显式类型变量、函数返回值、参数都建议写类型,编译器会严格检查。
- 空安全用 ?. 与 ?? 处理可能为空的值,避免运行时空指针崩溃。
- 接口与泛型用 interface 定义数据模型,用泛型约束集合与工具函数的类型。
- 受限动态性不支持 any、不支持运行时增删属性,这是刻意的工程约束。
把数据模型写成 interface,是鸿蒙工程里很值得养成的习惯:接口既约束了网络返回值,也约束了组件之间的参数传递,一处改动编译器会帮你找出所有受影响的代码。
另一个实用特性是可空类型。ArkTS 要求你显式处理 undefined 与 null,例如 const name: string = user?.profile?.nickname ?? '游客';。这条规则看起来啰嗦,却能挡掉端侧最常见的一类崩溃。
状态管理:@State / @Prop / @Link
状态管理是 ArkTS 最重要的概念。装饰器不同,数据的“来源”和“同步方向”也不同。
| 装饰器 | 方向 | 典型用途 |
|---|---|---|
| @State | 组件内部可变 | 页面自己的状态,如输入框内容 |
| @Prop | 父 → 子(单向) | 子组件只读地展示父级数据 |
| @Link | 父 ↔ 子(双向) | 子组件修改后需要同步回父级 |
| @Observed / @ObjectLink | 对象内部属性变化 | 类实例的深层字段变更 |
两个最容易踩的坑:第一,@Link 必须由父组件用 $ 前缀传引用(如 $count),直接传值会报错;第二,数组或对象只有在整体替换时才稳定触发刷新,修改深层字段需要配合 @Observed 与 @ObjectLink。想清楚这两点,绝大多数状态同步问题都能自查。
组件与自定义组件
自定义组件让界面可以复用。用 @Component 修饰 struct 即定义一个组件,它可以有自己的状态、生命周期与对外参数。@Builder 用于抽取可复用的 UI 片段,适合写列表项、卡片这类重复结构。
@Component
struct TodoItem {
@Prop title: string;
@Link done: boolean;
build() {
Row({ space: 10 }) {
Checkbox()
.select(this.done)
.onChange((v: boolean) => { this.done = v; })
Text(this.title)
.fontSize(16)
.decoration({
type: this.done ? TextDecorationType.LineThrough : TextDecorationType.None
})
}
.width('100%')
.padding(12)
}
}
组件拆分的原则与函数拆分一致:职责单一、参数清楚、副作用集中在状态变化处。一个 build() 超过 150 行就该考虑拆分了,否则后续改样式的成本会急剧上升。
配套的还有几个常用装饰器:@Builder 抽 UI 片段,@Styles 抽通用样式,@Extend 扩展内置组件的样式方法。合理使用能显著减少重复代码,但不要为了抽象而抽象——三层嵌套的 @Builder 反而更难读。
构建第一个页面:待办清单
把上面的知识整合起来,写一个最小的待办清单页面:输入框加按钮新增,列表展示,点击复选框标记完成。
interface Todo { id: number; title: string; done: boolean; }
@Entry
@Component
struct TodoApp {
@State items: Todo[] = [];
@State input: string = '';
private nextId: number = 1;
private add(): void {
const t = this.input.trim();
if (t.length === 0) { return; }
this.items = [...this.items, { id: this.nextId++, title: t, done: false }];
this.input = '';
}
build() {
Column({ space: 12 }) {
Row({ space: 8 }) {
TextInput({ text: this.input, placeholder: '输入待办事项' })
.layoutWeight(1)
.onChange((v: string) => { this.input = v; })
Button('添加').onClick(() => this.add())
}.width('100%')
List({ space: 8 }) {
ForEach(this.items, (item: Todo) => {
ListItem() {
Row({ space: 10 }) {
Checkbox().select(item.done)
.onChange((v: boolean) => { item.done = v; })
Text(item.title).fontSize(16).layoutWeight(1)
}.width('100%').padding(10)
}
}, (item: Todo) => item.id.toString())
}.layoutWeight(1)
}
.width('100%').height('100%').padding(16)
}
}
这里用 [...this.items, newItem] 生成新数组来触发刷新,是 ArkTS 状态更新的重要习惯:不要直接 push 后指望界面更新。ForEach 的第三个参数是键生成函数,必须返回稳定且唯一的值,否则列表会出现错位复用——表现为“勾选了 A,B 也变了”。
工程结构与模块化开发
一个标准的鸿蒙工程由主模块(entry)与若干共享模块组成。典型的 entry 目录结构是:pages 放页面,components 放可复用组件,model 放数据模型与网络请求,utils 放工具函数,resources 放资源文件。
- resources/base/element/string.json:文案资源,便于统一改词与国际化。
- resources/base/media:图片、图标等媒体资源。
- resources/base/profile/main_pages.json:页面路由注册表,新增页面必须在此登记。
跨模块复用建议打成共享包:HAR(静态共享包)在编译期被合入使用方,体积更小、调试直接;HSP(动态共享包)在运行时共享,适合体积较大的公共能力。工具类与设计系统适合 HAR,多应用共用的重资源适合 HSP。
无论哪种方式,都建议把“页面只做展示、逻辑放 model 与 utils”作为团队约定。鸿蒙应用的典型问题是页面组件越写越大,最后几千行的 build() 让任何改动都变得危险。
常见坑与调试技巧
新手常见的五个坑,基本都是状态与列表相关。
- 直接修改数组元素:
this.items[0].done = true不会触发刷新,应改用 @Observed/@ObjectLink 或整体替换数组。 - ForEach 键不唯一:用索引当键会导致列表项复用错乱,务必用业务唯一 ID。
- @Link 传值而非引用:父组件忘了写 $ 会直接编译报错,报错信息通常很直接,别急着怀疑环境。
- 忘记注册路由:新页面在 main_pages.json 里没有登记,跳转时会找不到页面。
- 在 build() 里做副作用:请求、计时器、日志都不应放在 build 中,它会被多次调用。
在声明式框架里,界面函数是“纯描述”。把副作用从 build() 里赶出去,是稳定性提升最快的一步。
调试方面,善用 hilog 打印带业务标签的日志(如 hilog.info(0x0000, 'TodoApp', '新增:%{public}s', title)),并在 DevEco Studio 的日志面板按标签过滤。下一步建议继续读 ArkUI 的布局与组件体系,把界面能力补全。