为什么鸿蒙选择 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 基本一致。

ArkTS 类型系统要点
  • 显式类型变量、函数返回值、参数都建议写类型,编译器会严格检查。
  • 空安全用 ?. 与 ?? 处理可能为空的值,避免运行时空指针崩溃。
  • 接口与泛型用 interface 定义数据模型,用泛型约束集合与工具函数的类型。
  • 受限动态性不支持 any、不支持运行时增删属性,这是刻意的工程约束。
ArkTS 入门示意:竖放的手机轮廓与半透明代码编辑窗口以发光线连接,背景有同心圆环光晕
ArkTS 的开发范式:一边是代码,一边是界面,两者通过状态绑定自动同步。

把数据模型写成 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 也变了”。