从演进脉络看,ArkUI 走过了一条清晰的路线:早期 HarmonyOS 支持基于 Java 的 XML 布局开发,随后引入 JS 开发范式,再到今天以 ArkTS + 声明式 UI 为核心。当前官方主推的是基于 ArkTS 的声明式开发范式,类 Web 开发范式和低代码开发作为补充途径,满足不同背景开发者的过渡需求。

要理解 ArkUI,首先要理解它的几个关键设计目标。

  • 声明式优先:开发者只需描述「UI 应该长什么样」,框架负责完成「如何渲染、如何更新」。状态改变后,UI 自动刷新,开发者不再手动操作视图节点的增删改。
  • 一次开发,多端部署:同一套 ArkUI 代码配合自适应布局、栅格系统与组件级多态能力,可以运行在手机、平板、折叠屏、车机、智慧屏等多种设备上。
  • 高性能渲染:ArkUI 并非简单地把声明式语法翻译成原生控件,而是自建了声明式前端、组件树、渲染管线与动画系统,通过限制动态化能力、消化布局计算和提升绘制效率来获得接近原生的性能。
  • 与系统能力深度整合:ArkUI 与鸿蒙的分布式能力、原子化服务(元服务)、Stage 模型、Ability 组件深度绑定,UI 层可以直接感知设备形态、窗口尺寸与系统环境的变化。

对于有 Web 前端、Android/iOS 或 Flutter、Compose、SwiftUI 背景的开发者来说,ArkUI 的学习曲线并不陡峭,因为声明式 UI 的核心思想是相通的。但它也有鲜明的自身特色:ArkTS 语言、独特的装饰器状态管理、组件内 build 函数返回 UI 描述、以及围绕「一次开发多端部署」构建的一整套响应式机制。

2. ArkUI 的总体架构:从声明式前端到渲染引擎

理解 ArkUI 的架构,是掌握它运行机制和性能调优的基础。整体上,ArkUI 可以分为应用层、框架层、引擎层和系统内核能力层几个部分。

2.1 声明式前端:ArkTS 与 UI 描述

在应用层,开发者使用 ArkTS 语言编写带有装饰器的组件。ArkTS 是在 TypeScript 基础上裁剪与增强而来,强调静态类型和安全约束,适合 UI 场景。每个自定义组件通过 struct 定义,在 build() 方法中声明 UI。装饰器如 @Entry@Component@State@Prop 等承载了页面入口、组件复用、状态管理等语义。

声明式前端负责将开发者书写的 UI 描述转换为框架内部可消费的数据结构,并建立「状态 - UI」之间的依赖关系。当状态变量发生变化时,框架能够精准定位到受影响的组件,而不是全量重建整棵组件树。

2.2 框架层:组件树、布局与状态管理

框架层是 ArkUI 的「大脑」,负责组件树的创建与维护、属性更新、布局计算、事件分发和动画调度。开发者写的每一个组件实例,在框架层都有对应的节点;build 函数的每次重新执行,都会生成新的 UI 描述,框架通过与上一次描述做 diff,找出需要更新的最小节点集合。

状态管理子系统在这一层运作。装饰器会在组件实例上注册状态,并建立状态与 UI 依赖的映射。状态更新时,框架能够实现细粒度的局部刷新:只重建依赖该状态的那些组件分支,其他分支保持不变。

2.3 引擎层:渲染引擎与绘制

引擎层负责把框架层产出的布局结果和绘制指令交给系统图形栈。ArkUI 的渲染引擎在鸿蒙图形能力之上构建,完成了测量、布局、绘制和合成等流程。针对文本、图片、图形特效等场景,引擎层做了大量硬件加速与缓存优化。

这里有一个值得注意的点:ArkUI 组件并不都是「原生控件」的包装,而是运行在自绘渲染管线中的实体。这意味着 ArkUI 在跨设备一致性上更有优势,但也要求框架对文字排版、手势、无障碍等细节进行完整接管。经过多个版本的迭代,ArkUI 在这方面的成熟度已经显著提升。

2.4 系统与内核能力层

最底层是鸿蒙系统提供的窗口管理、图形接口、GPU 加速、多模输入、无障碍、资源管理等能力。ArkUI 通过标准接口与这些能力交互,例如窗口尺寸变化会触发页面尺寸变化,折叠屏展开会通知框架重新布局,系统深浅色模式切换会驱动颜色资源刷新。

这样的分层设计带来了几个明显的好处:声明式前端与底层实现解耦,框架可以持续演进而不破坏应用代码;渲染引擎可以针对不同芯片和屏幕做优化;组件能力可以随系统升级逐步增强。

3. 开发环境与第一个 ArkUI 应用

在深入语法细节之前,先建立对开发流程的整体认识。ArkUI 的官方一站式开发工具是 DevEco Studio,它集成了项目创建、代码编辑、预览器、模拟器、编译构建、调试和性能分析工具。

3.1 环境准备

  1. 安装 DevEco Studio,建议使用与目标 HarmonyOS SDK 版本匹配的稳定版本。
  2. 在 DevEco Studio 中下载 HarmonyOS SDK、OpenHarmony SDK 与相关模拟器镜像。
  3. 配置签名信息,否则真机运行和部分调试能力会受到限制。
  4. 创建工程时选择 Empty Ability 模板,语言选择 ArkTS,即可得到一个最小的 ArkUI 工程。

工程结构遵循 Stage 模型:entry/src/main/ets 目录存放 ArkTS 源码,pages 目录存放页面组件,resources 目录存放字符串、图片、颜色等资源,module.json5 描述模块基本信息与页面路由配置。

3.2 最小可运行示例

下面是一个最经典的 Hello World 页面,展示了 @Entry@Componentbuild()Text 组件的基本用法。

@Entry
@Component
struct Index {
  build() {
    Column() {
      Text('Hello ArkUI')
        .fontSize(36)
        .fontWeight(FontWeight.Bold)
      Text('欢迎来到鸿蒙原生开发')
        .fontSize(16)
        .fontColor('#666666')
        .margin({ top: 12 })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

这个例子虽然简单,却体现了 ArkUI 的核心范式:用组件来描述 UI,通过链式调用设置属性,用容器组件组织布局。需要特别注意的是,@Entry 标注的组件是可路由的页面;@Component 标注的是可复用的自定义组件;build() 是唯一必须实现的方法,它描述组件要展示的内容。

4. ArkTS 语言:ArkUI 的编程基石

ArkTS 是 HarmonyOS 开发的主推语言,它与 TypeScript 有很高的相似度,但为了满足嵌入式场景和 UI 框架的性能要求,做了一系列限制与扩展。理解 ArkTS 的特性,能帮助开发者写出既符合规范又高性能的代码。

4.1 ArkTS 与 TypeScript 的关系

ArkTS 以 TypeScript 为基础,继承了静态类型、类、接口、泛型、装饰器(经过改造)等特性。同时,ArkTS 对 TypeScript 做了裁剪:不支持 any 类型的滥用、不支持无约束的动态类型、不支持 eval 等动态执行能力,限制对对象字面量的随意使用。这些约束让 ArkTS 更容易被静态分析和优化,从而获得更可预测的性能。

对于 UI 场景,ArkTS 引入了声明式 UI 扩展,包括 struct 组件定义、装饰器、UI 描述语法等。这些能力不是纯 TypeScript 语法,而是 ArkUI 框架在编译期和运行期共同支撑的语言扩展。

4.2 常用类型与语法

ArkTS 支持基础类型 numberstringbooleannullundefined,也支持 enumunion 类型、interfaceclass。在组件开发中,interface 常用于定义数据模型,class 常用于封装业务逻辑。

interface TaskItem {
  id: number
  title: string
  done: boolean
  priority: 'high' | 'normal' | 'low'
}

class TaskRepository {
  private tasks: TaskItem[] = []

  add(task: TaskItem): void {
    this.tasks.push(task)
  }

  remove(id: number): void {
    this.tasks = this.tasks.filter(item => item.id !== id)
  }

  all(): TaskItem[] {
    return this.tasks
  }
}

在上述代码中,TaskItem 用一个接口描述待办事项的数据结构,TaskRepository 封装了增删查逻辑。这种「数据模型 + 业务类」的组织方式在 ArkUI 项目中非常常见,能够让组件保持纯粹,只关注展示与交互。

4.3 空安全与类型检查

ArkTS 强调类型安全和空安全,在编译期就能拦截大量潜在错误。对于可能为空的变量,应显式使用联合类型标注,并在访问前进行判空。虽然严格模式可能让初学者一开始不太适应,但它能显著减少运行时崩溃,也能让 IDE 提供更准确的代码提示。

function getTitle(task: TaskItem | null): string {
  if (task === null) {
    return '未命名任务'
  }
  return task.title
}

5. 声明式 UI 核心思想:状态驱动视图

声明式 UI 是 ArkUI 的灵魂。在命令式 UI 中,开发者需要手动维护视图引用,状态变化后手动调用方法更新界面;而在声明式 UI 中,视图是状态的函数,UI = f(state)。状态改变后,框架自动重新执行相关组件的 build 逻辑,生成新的 UI 描述并完成增量更新。

这种范式带来的核心优势是:代码结构更清晰、状态流向更明确、界面一致性更强、bug 更少。它把开发者从「手动同步数据和视图」的繁琐工作中解放出来,让注意力聚焦在数据本身。

5.1 build 函数与 UI 描述

每个自定义组件的 build() 方法都返回一个 UI 描述。这个描述不是命令式的操作序列,而是一个树状结构。下面是一个包含条件渲染和循环渲染的例子。

@Component
struct TaskDashboard {
  tasks: TaskItem[] = []

  build() {
    Column() {
      if (this.tasks.length > 0) {
        ForEach(this.tasks, (task: TaskItem) => {
          Row() {
            Text(task.title)
              .fontSize(16)
            if (task.done) {
              Text('已完成')
                .fontColor(Color.Green)
            } else {
              Text('未完成')
                .fontColor(Color.Orange)
            }
          }
          .padding(12)
        }, (task: TaskItem) => task.id.toString())
      } else {
        Text('暂无任务,点击下方按钮添加')
          .fontColor('#999999')
          .margin({ top: 48 })
      }
    }
  }
}

这段代码展示了声明式 UI 的几个关键语法:if 用于条件渲染,ForEach 用于循环渲染。当 tasks 发生变化时,build 会重新执行,界面自动更新。注意 ForEach 的第三个参数是键生成函数,它帮助框架识别每个列表项,是高性能列表的关键。

5.2 状态驱动更新的最小更新原则

声明式 UI 并不意味着每次状态变化都重建整个页面。ArkUI 的状态管理会建立状态与 UI 之间的依赖关系,实现最小化更新。比如页面有五个组件,只有其中一个依赖某个状态变量,那么该状态变化时,只有这一个组件会被重新构建。

理解这一点对性能优化至关重要:应该把状态绑定到尽可能小的组件作用域,避免把大量状态堆在页面根部导致大范围刷新。这也是为什么大型页面要拆分成多个子组件,并把状态放在真正需要它的组件里。

6. 组件系统:组件的定义、生命周期与渲染控制

组件是 ArkUI 的基本构建单元。系统内置了丰富的组件,开发者也可以通过 @Component 自定义组件。掌握组件的定义方式、生命周期和渲染控制机制,是构建复杂界面的前提。

6.1 自定义组件的定义

自定义组件使用 @Component 装饰器配合 struct 定义,组件内可以声明状态变量、普通属性和成员方法。组件之间通过构造参数传递数据,通过状态装饰器实现响应式更新。

@Component
struct UserCard {
  name: string = ''
  avatar: string = ''
  isVip: boolean = false

  build() {
    Row() {
      Image(this.avatar)
        .width(48)
        .height(48)
        .borderRadius(24)
      Column() {
        Text(this.name)
          .fontSize(17)
          .fontWeight(FontWeight.Medium)
        if (this.isVip) {
          Text('VIP 会员')
            .fontSize(12)
            .fontColor('#C7A05A')
        }
      }
      .alignItems(HorizontalAlign.Start)
      .margin({ left: 12 })

      Blank()
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
  }
}

父组件使用自定义组件时,像使用内置组件一样传入参数:UserCard({ name: '张三', avatar: $r('app.media.avatar'), isVip: true })。普通参数在初始化后不具备响应式能力,如果需要响应式传递,就要使用 @Prop@Link 等装饰器。

6.2 组件生命周期

ArkUI 自定义组件提供了完整的生命周期回调,开发者可以在关键节点执行初始化、清理和资源管理。

  • aboutToAppear():组件即将出现时调用,适合做数据加载、订阅初始化。
  • aboutToDisappear():组件即将消失时调用,适合取消订阅、释放资源。
  • onDidBuild():组件 build 完成后调用,可用于执行依赖构建结果的操作。
  • aboutToReuse():配合组件复用使用,在组件复用时调用。
  • aboutToRecycle():复用的组件被回收时调用。

页面级组件还有 onPageShow()onPageHide(),在页面显示和隐藏时触发。理解这些生命周期有助于处理「何时加载数据、何时清理定时器、何时释放音频资源」等实际问题。

@Entry
@Component
struct MusicPage {
  @State playing: boolean = false
  private timerId: number = -1

  aboutToAppear(): void {
    // 页面即将出现,加载数据
    this.loadPlaylist()
  }

  aboutToDisappear(): void {
    // 组件销毁前清理定时器
    if (this.timerId >= 0) {
      clearInterval(this.timerId)
    }
  }

  onPageShow(): void {
    // 页面从后台回到前台,暂停的播放可在此恢复
  }

  onPageHide(): void {
    // 页面进入后台,可在此暂停播放
  }

  loadPlaylist(): void {
    // 模拟加载播放列表
  }

  build() {
    Column() {
      Text('我的音乐')
    }
  }
}

6.3 渲染控制:条件渲染与循环渲染

ArkUI 的渲染控制语法非常灵活。if/else 负责条件渲染,ForEach 负责列表渲染,LazyForEach 负责大数据量的懒加载渲染,Repeat 是更现代的循环渲染语法。ForEach 适合中小规模数据,LazyForEach 适合需要按需加载的长列表。

使用 LazyForEach 需要配合 IDataSource 接口实现数据源,框架会只渲染可视区域附近的 item,大幅降低内存占用和构建开销。

class SimpleDataSource implements IDataSource {
  private listeners: DataChangeListener[] = []
  private dataArray: string[] = []

  totalCount(): number {
    return this.dataArray.length
  }

  getData(index: number): string {
    return this.dataArray[index]
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    if (!this.listeners.includes(listener)) {
      this.listeners.push(listener)
    }
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener)
    if (pos >= 0) {
      this.listeners.splice(pos, 1)
    }
  }
}

在生产项目中,列表数据往往成百上千条,LazyForEach 是必须掌握的能力。它的数据源需要实现 totalCount()getData() 和监听器注册方法,框架在滚动过程中自动触发数据请求和组件构建。

7. 布局系统:从线性布局到复杂网格

布局是 UI 开发的核心工作。ArkUI 提供了一整套布局组件,从最基础的线性布局、层叠布局到弹性布局、栅格布局和相对布局,覆盖了绝大多数场景。理解每个布局容器的特性和适用场景,能让界面开发事半功倍。

7.1 线性布局:Row 与 Column

Row 沿水平方向排列子组件,Column 沿垂直方向排列。二者是最常用的容器,支持主轴对齐、交叉轴对齐、间距、权重等灵活配置。

@Component
struct LinearLayoutDemo {
  build() {
    Column({ space: 16 }) {
      Row({ space: 12 }) {
        Text('左')
          .width(80)
          .height(40)
          .textAlign(TextAlign.Center)
          .backgroundColor('#FF6B6B')
        Text('中')
          .layoutWeight(1)
          .height(40)
          .textAlign(TextAlign.Center)
          .backgroundColor('#4ECDC4')
        Text('右')
          .width(80)
          .height(40)
          .textAlign(TextAlign.Center)
          .backgroundColor('#45B7D1')
      }
      .width('100%')
      .height(80)
      .backgroundColor('#F0F0F0')

      Column({ space: 8 }) {
        Text('第一行')
        Text('第二行')
        Text('第三行')
      }
      .alignItems(HorizontalAlign.Center)
    }
    .padding(16)
  }
}

这里 layoutWeight(1) 是关键属性,它让「中」这个组件占据剩余空间,类似 Flex 布局的 flex: 1。通过组合 layoutWeightjustifyContentalignItems,可以实现大多数常规布局需求。

7.2 层叠布局:Stack

Stack 将子组件重叠放置,后声明的组件显示在上层。它常用于头像与徽标叠加、卡片上的浮层、图片上的文字遮罩等场景。

@Component
struct StackDemo {
  build() {
    Stack({ alignContent: Alignment.BottomEnd }) {
      Image($r('app.media.cover'))
        .width(320)
        .height(180)
        .borderRadius(16)
      Text('9.2 分')
        .fontSize(14)
        .fontColor(Color.White)
        .padding({ left: 10, right: 10, top: 4, bottom: 4 })
        .backgroundColor('#CC000000')
        .borderRadius(16)
        .margin({ right: 12, bottom: 12 })
    }
  }
}

通过 alignContent 可以统一控制所有子组件的对齐方式,也可以通过单个子组件的 positionoffsetalign 属性做个性化定位。

7.3 弹性布局:Flex

Flex 提供了比 Row/Column 更完整的弹性布局能力,可以显式设置主轴方向、换行方式、主轴和交叉轴对齐策略。当需要复杂排列或多行布局时,Flex 是更合适的选择。

@Component
struct FlexDemo {
  build() {
    Flex({ direction: FlexDirection.Row, wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
      ForEach([1, 2, 3, 4, 5, 6], (item: number) => {
        Text(item.toString())
          .width(100)
          .height(60)
          .textAlign(TextAlign.Center)
          .backgroundColor('#B8E0D2')
          .margin({ bottom: 8 })
      })
    }
    .padding(16)
  }
}

7.4 栅格布局:GridRow 与 GridCol

栅格布局是多设备自适应的利器。GridRow 将水平空间划分为等宽的列,GridCol 指定某个元素占据几列。配合断点系统,可以在不同屏幕宽度下使用不同的列数配置。

@Component
struct GridLayoutDemo {
  build() {
    GridRow({ columns: 12, gutter: 12 }) {
      GridCol({ span: { sm: 12, md: 6, lg: 4 } }) {
        this.card('卡片 1')
      }
      GridCol({ span: { sm: 12, md: 6, lg: 4 } }) {
        this.card('卡片 2')
      }
      GridCol({ span: { sm: 12, md: 6, lg: 4 } }) {
        this.card('卡片 3')
      }
    }
    .padding(16)
  }

  @Builder
  card(title: string) {
    Column() {
      Text(title)
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
    }
    .width('100%')
    .height(90)
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#EAF4F4')
    .borderRadius(12)
  }
}

上面的配置表示:在小屏(sm)时每张卡片占 12 列即整行,中屏(md)时占 6 列即一行两张,大屏(lg)时占 4 列即一行三张。这种写法是实现「一次开发、多端部署」的典型手段。

7.5 相对布局与其他布局组件

RelativeContainer 支持基于锚点的相对定位,通过为每个子元素设置参照物和对齐规则,可以构建高度定制化的界面。此外,ArkUI 还提供了 AbsoluteLayout(已不再推荐新代码使用)、Grid(网格容器,兼具布局与数据展示能力)、List(列表容器,支持滚动和懒加载)等。

在实际开发中,推荐优先使用语义清晰、自适应能力强的布局组件。复杂布局通常由 Row、Column、Flex、Grid 组合而成,而不是依赖绝对定位,因为绝对定位在不同屏幕尺寸下容易出现错位。

8. 状态管理:ArkUI 响应式更新的核心机制

状态管理是 ArkUI 声明式开发中最关键、也最容易让初学者困惑的部分。装饰器决定了变量的作用域、数据流向和更新机制,只有正确使用,才能让界面在数据变化时准确刷新。

8.1 组件内状态:@State

@State 用于组件内部的状态变量。当被 @State 修饰的变量发生变化时,会触发组件及其依赖该变量的子组件重新渲染。

@Component
struct Counter {
  @State count: number = 0

  build() {
    Column({ space: 16 }) {
      Text(`当前计数:${this.count}`)
        .fontSize(24)
      Button('加一')
        .onClick(() => {
          this.count++
        })
    }
  }
}

注意 ArkTS 中 @State 变量在初始化时可以直接赋值普通值,但复杂类型(如对象和数组)需要特别处理。对数组的修改要使用能触发观察的 API(如 pushsplice),而对整个数组重新赋值总是安全的。

8.2 父传子单向同步:@Prop

@Prop 用于父组件向子组件传递数据,在子组件内部是单向同步:父组件数据变化会同步给子组件,但子组件修改 @Prop 变量不会反向影响父组件。

@Component
struct CountDisplay {
  @Prop count: number = 0

  build() {
    Text(`父组件传入的值:${this.count}`)
  }
}

@Entry
@Component
struct PropDemo {
  @State total: number = 0

  build() {
    Column({ space: 12 }) {
      CountDisplay({ count: this.total })
      Button('父组件加一')
        .onClick(() => {
          this.total++
        })
    }
  }
}

8.3 父子双向同步:@Link

@Prop 不同,@Link 建立的是双向数据绑定。子组件修改 @Link 变量,会直接同步回父组件的源状态。父组件传参时需要使用 $ 前缀,例如 Child({ value: $parentValue })

@Component
struct Stepper {
  @Link value: number

  build() {
    Row({ space: 16 }) {
      Button('-')
        .onClick(() => {
          this.value--
        })
      Text(this.value.toString())
        .fontSize(20)
        .width(48)
        .textAlign(TextAlign.Center)
      Button('+')
        .onClick(() => {
          this.value++
        })
    }
  }
}

@Entry
@Component
struct LinkDemo {
  @State quantity: number = 1

  build() {
    Column({ space: 20 }) {
      Text(`购物车数量:${this.quantity}`)
      Stepper({ value: $quantity })
    }
  }
}

8.4 跨层级共享:@Provide 与 @Consume

当状态需要在多个层级的组件之间共享时,逐层传递会非常繁琐。@Provide 在祖先组件中提供状态,@Consume 在任意后代组件中消费状态,无需中间组件转手。

@Entry
@Component
struct ProvideDemo {
  @Provide('themeColor') theme: string = '#FF6B6B'

  build() {
    Column() {
      Text('设置主题色')
      ThemePanel()
    }
  }
}

@Component
struct ThemePanel {
  @Consume('themeColor') theme: string

  build() {
    Text(`当前主题色:${this.theme}`)
      .fontColor(this.theme)
  }
}

@Provide@Consume 通过相同字符串 key 配对,是跨层级通信的便捷方案。但过度使用会让数据流向难以追踪,建议在真正需要跨多级的场景下使用,并保持 key 的命名规范。

8.5 类对象观察:@Observed 与 @ObjectLink

对于自定义类对象,仅用 @State 只能观察对象引用本身的变化,无法感知对象内部属性的修改。@Observed 配合 @ObjectLink 可以观察类实例内部属性的变化。

@Observed
class UserProfile {
  name: string
  age: number

  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
}

@Entry
@Component
struct ObservedDemo {
  @State user: UserProfile = new UserProfile('张三', 28)

  build() {
    Column({ space: 16 }) {
      UserInfo({ user: this.user })
      Button('修改年龄')
        .onClick(() => {
          this.user.age++
        })
    }
  }
}

@Component
struct UserInfo {
  @ObjectLink user: UserProfile

  build() {
    Column() {
      Text(this.user.name)
      Text(`年龄:${this.user.age}`)
    }
  }
}

8.6 全局状态:LocalStorage、AppStorage 与 PersistentStorage

  • LocalStorage:应用内特定 Ability 作用域的状态存储,适合页面间共享但不需要持久化的数据。
  • AppStorage:应用级的全局状态,跨 Ability 共享。
  • PersistentStorage:将 AppStorage 中的指定键值持久化到本地,应用重启后数据仍然保留,适合保存用户偏好设置。
@Entry
@Component
struct SettingsPage {
  @StorageLink('fontScale') fontScale: number = 1.0

  build() {
    Column({ space: 16 }) {
      Text('字体缩放')
      Slider({
        value: this.fontScale,
        min: 0.8,
        max: 1.5,
        step: 0.1
      })
        .onChange((value: number) => {
          this.fontScale = value
        })
    }
    .padding(24)
  }
}

@StorageLink 与 AppStorage 双向绑定,@StorageProp 是单向绑定。需要持久化时,在应用启动阶段调用 PersistentStorage.persistProp('fontScale', 1.0),后续对 fontScale 的修改会自动落盘。

9. 页面路由与导航

一个完整应用离不开页面之间的跳转。ArkUI 提供了基于页面栈的 router 路由能力和更现代的 Navigation 组件。理解两者的区别和适用场景,对架构设计很有帮助。

9.1 基于页面栈的 router 路由

router 通过 pushUrl 打开新页面、back 返回上一页、replaceUrl 替换当前页、clear 清空页面栈。页面跳转时需要目标页已在 main_pages.jsonroute_map.json 中注册。

import { router } from '@kit.ArkUI'

@Entry
@Component
struct HomePage {
  build() {
    Column({ space: 16 }) {
      Text('首页')
        .fontSize(24)
      Button('跳转到详情页')
        .onClick(() => {
          router.pushUrl({
            url: 'pages/DetailPage',
            params: {
              id: 1001,
              title: 'ArkUI 路由示例'
            }
          })
        })
    }
  }
}

@Entry
@Component
struct DetailPage {
  @State id: number = 0
  @State title: string = ''

  aboutToAppear(): void {
    const params = router.getParams() as Record<string, Object>
    this.id = params['id'] as number
    this.title = params['title'] as string
  }

  build() {
    Column({ space: 16 }) {
      Text(this.title)
        .fontSize(24)
      Text(`ID:${this.id}`)
        .fontSize(16)
      Button('返回')
        .onClick(() => {
          router.back()
        })
    }
  }
}

9.2 Navigation 组件

Navigation 提供了更强大的导航能力,包括标题栏、返回键、单栏/双栏模式、导航栈管理等。它更适合构建具有多级页面结构的应用,尤其是需要自适应分栏的平板场景。

@Entry
@Component
struct NavigationDemo {
  @Provide('pageStack') pageStack: NavPathStack = new NavPathStack()

  @Builder
  contentBuilder(name: string) {
    Column() {
      Text(`当前页面:${name}`)
        .fontSize(24)
      Button('进入下一级')
        .onClick(() => {
          this.pageStack.pushPathByName('Detail', '从导航栈进入')
        })
    }
  }

  build() {
    Navigation(this.pageStack) {
      Column() {
        Button('打开详情')
          .onClick(() => {
            this.pageStack.pushPathByName('Detail', '初始详情')
          })
      }
    }
    .navDestination(this.contentBuilder)
    .title('导航示例')
  }
}

NavPathStack 维护导航栈,pushPathByName 按名称入栈。配合 NavDestination 可以为每个页面定义导航内容。Navigation 的优势在于它提供了标准化的转场动画、标题栏和分栏能力,是官方推荐的新页面导航方案。

10. 事件与交互体系

ArkUI 提供了一套完整的事件处理机制,包括点击、触摸、拖拽、手势、按键等。事件绑定以链式方法的形式出现在组件上,如 onClickonTouchonChangeonAppearonDisAppear 等。此外,通用手势系统支持单击、双击、长按、滑动、拖拽、捏合和旋转手势。

10.1 基础事件

@Component
struct EventDemo {
  @State pressCount: number = 0

  build() {
    Column({ space: 24 }) {
      Button('普通点击')
        .onClick(() => {
          this.pressCount++
        })

      Button('长按触发')
        .gesture(
          LongPressGesture()
            .onAction(() => {
              this.pressCount = 0
            })
        )

      Text(`点击次数:${this.pressCount}`)

      TextInput({ placeholder: '输入内容' })
        .onChange((value: string) => {
          console.info(`输入内容:${value}`)
        })
    }
    .padding(24)
  }
}

10.2 手势组合

复杂交互往往需要多个手势组合。ArkUI 支持通过 GestureGroup 组合手势,可以设置并行(parallel)或顺序(sequence)等组合方式。

@Component
struct GestureDemo {
  @State offsetX: number = 0
  @State offsetY: number = 0

  build() {
    Text('拖动我')
      .fontSize(20)
      .translate({ x: this.offsetX, y: this.offsetY })
      .padding(24)
      .backgroundColor('#AED9E0')
      .borderRadius(12)
      .gesture(
        PanGesture()
          .onActionUpdate((event: GestureEvent) => {
            this.offsetX = event.offsetX
            this.offsetY = event.offsetY
          })
      )
  }
}

手势系统在 ArkUI 中能力很强,但使用时要注意手势冲突。当多个手势绑定在重叠区域时,应根据交互需求设置手势优先级,避免父容器和子组件的手势相互干扰。

11. 动画体系:让界面自然流畅

动画是提升用户体验的重要手段。ArkUI 的动画体系非常丰富,包括属性动画、显式动画、转场动画、关键帧动画、粒子动画和路径动画等。掌握这些动画能力,可以让界面交互更自然、更有品质感。

11.1 属性动画

属性动画是最简单的动画方式。给组件的属性添加 animation 方法后,该属性的任何变化都会以动画形式过渡。

@Component
struct PropertyAnimationDemo {
  @State expanded: boolean = false

  build() {
    Column({ space: 24 }) {
      Text('属性动画')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .animation({
          duration: 300,
          curve: Curve.EaseInOut
        })

      Button(this.expanded ? '收起' : '展开')
        .width(this.expanded ? 200 : 120)
        .height(this.expanded ? 60 : 44)
        .animation({
          duration: 350,
          curve: Curve.EaseOut
        })
        .onClick(() => {
          this.expanded = !this.expanded
        })
    }
  }
}

11.2 显式动画

显式动画通过 animateTo 包裹状态变更代码块,手动指定动画参数。它的优势是可以对多个属性的组合变化做统一控制。

@Component
struct ExplicitAnimationDemo {
  @State rotateAngle: number = 0
  @State scale: number = 1

  build() {
    Column({ space: 40 }) {
      Text('显式动画')
        .fontSize(28)
        .rotate({ angle: this.rotateAngle })
        .scale({ x: this.scale, y: this.scale })

      Button('播放动画')
        .onClick(() => {
          this.getUIContext().animateTo({
            duration: 600,
            curve: Curve.Friction,
            onFinish: () => {
              console.info('动画播放完成')
            }
          }, () => {
            this.rotateAngle = 360
            this.scale = 1.5
          })
        })
    }
  }
}

11.3 转场动画

转场动画用于组件插入和移除时的过渡效果。transition 方法接收 TransitionEffect 参数,可以组合淡入淡出、位移、缩放、旋转等效果。常见场景包括弹窗出现、条件渲染内容的显隐等。

@Component
struct TransitionDemo {
  @State show: boolean = false

  build() {
    Column({ space: 24 }) {
      Button('显示/隐藏')
        .onClick(() => {
          this.getUIContext().animateTo({
            duration: 300
          }, () => {
            this.show = !this.show
          })
        })

      if (this.show) {
        Column() {
          Text('我是转场出现的内容')
            .fontSize(18)
        }
        .width('80%')
        .height(120)
        .justifyContent(FlexAlign.Center)
        .backgroundColor('#FBC4AB')
        .borderRadius(16)
        .transition(
          TransitionEffect.OPACITY
            .combine(TransitionEffect.translate({ y: 40 }))
            .animation({ duration: 300, curve: Curve.EaseOut })
        )
      }
    }
    .width('100%')
  }
}

11.4 关键帧动画

关键帧动画允许在动画过程中定义多个关键节点,每个节点指定不同的属性值和时间点,适合实现复杂的动画序列。通过 keyframeAnimateTo 方法可以配置关键帧数组。

@Component
struct KeyframeDemo {
  @State animatedValue: number = 0

  build() {
    Column({ space: 32 }) {
      Text('关键帧')
        .fontSize(28)
        .translate({ y: this.animatedValue })

      Button('播放关键帧动画')
        .onClick(() => {
          this.getUIContext().keyframeAnimateTo({
            duration: 1200,
            keyframes: [
              { duration: 400, value: -80 },
              { duration: 400, value: 40 },
              { duration: 400, value: 0 }
            ]
          }, () => {
            this.animatedValue = this.animatedValue
          })
        })
    }
  }
}

动画性能优化有几个关键点:优先使用 transform 类属性(如 translate、scale、rotate)而不是布局类属性(如 width、height、margin),因为前者可以跳过布局阶段直接在合成层完成;避免在动画过程中更新大量状态;对于列表项动画,要注意与组件复用的配合。

12. 常用基础组件详解

ArkUI 内置组件数量众多、功能全面。下面选取开发中最常用的几类组件进行详细说明,并给出典型用法。

12.1 文本组件 Text 与 Span

Text 用于显示文本,支持省略号、换行、对齐、装饰线、行高、字体等丰富属性。SpanText 配合可以在一段文本中混排不同样式。

@Component
struct TextDemo {
  build() {
    Column({ space: 16 }) {
      Text('单行超长文本省略示例,后面的内容会被省略号替代')
        .fontSize(16)
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })

      Text() {
        Span('普通文本')
        Span('加粗文本').fontWeight(FontWeight.Bold)
        Span('红色文本').fontColor(Color.Red)
        Span('下划线文本').decoration({
          type: TextDecorationType.Underline,
          color: Color.Blue
        })
      }
      .fontSize(16)
    }
    .padding(16)
  }
}

12.2 图片组件 Image

Image 支持网络图片、本地资源和 Base64 等多种来源,提供多种填充模式和丰富的图像处理能力,如圆角、模糊、灰度等。

@Component
struct ImageDemo {
  build() {
    Column({ space: 16 }) {
      Image('https://example.com/image.png')
        .width(180)
        .height(120)
        .objectFit(ImageFit.Cover)
        .borderRadius(12)

      Image($r('app.media.icon'))
        .width(64)
        .height(64)
        .borderRadius(32)
    }
    .padding(16)
  }
}

12.3 按钮组件 Button

Button 支持多种样式(胶囊、圆形、普通),可通过 ButtonTypestateEffect 等属性定制外观和按压效果。

@Component
struct ButtonDemo {
  build() {
    Column({ space: 16 }) {
      Button('普通按钮')
        .width('80%')
        .height(44)

      Button('胶囊按钮', { type: ButtonType.Capsule })
        .width('80%')
        .height(44)
        .backgroundColor('#4ECDC4')

      Button('圆形按钮', { type: ButtonType.Circle })
        .width(64)
        .height(64)
        .backgroundColor('#FF6B6B')
    }
    .width('100%')
  }
}

12.4 输入组件 TextInput 与 TextArea

TextInput 用于单行输入,TextArea 用于多行输入。二者都支持占位符、输入类型、正则校验、字数统计、密码框等能力。

@Component
struct InputDemo {
  @State username: string = ''
  @State password: string = ''

  build() {
    Column({ space: 16 }) {
      TextInput({ placeholder: '请输入用户名', text: this.username })
        .height(44)
        .onChange((value: string) => {
          this.username = value
        })

      TextInput({ placeholder: '请输入密码', text: this.password })
        .height(44)
        .type(InputType.Password)
        .onChange((value: string) => {
          this.password = value
        })

      TextArea({ placeholder: '请输入详细描述' })
        .height(120)
    }
    .padding(16)
  }
}

12.5 弹窗与提示

ArkUI 提供 DialogAlertDialogActionSheetToastPopup 等多种提示组件。其中 Toast 是轻量提示,AlertDialog 用于需要用户确认的场景,ActionSheet 适合操作菜单。

@Component
struct DialogDemo {
  build() {
    Column({ space: 16 }) {
      Button('显示 Toast')
        .onClick(() => {
          this.getUIContext().getPromptAction().showToast({
            message: '操作成功',
            duration: 2000
          })
        })

      Button('显示确认框')
        .onClick(() => {
          AlertDialog.show({
            title: '提示',
            message: '确定要删除这条记录吗?',
            primaryButton: {
              value: '取消',
              action: () => {
                console.info('用户取消删除')
              }
            },
            secondaryButton: {
              value: '确定',
              fontColor: Color.Red,
              action: () => {
                console.info('用户确认删除')
              }
            }
          })
        })
    }
    .padding(24)
  }
}

13. 列表、网格与滚动容器

数据展示类场景是移动应用的核心。ArkUI 中的 ListGridScrollWaterFlow 提供了高性能的数据展示能力,配合 LazyForEach 可以流畅地承载海量数据。

13.1 List 列表容器

List 支持垂直和水平列表、分组列表、粘性头部、分割线和滚动控制。对于长列表,必须使用 LazyForEach 实现懒加载。

@Component
struct ListDemo {
  private data: string[] = []

  aboutToAppear(): void {
    for (let i = 0; i < 500; i++) {
      this.data.push(`列表项 - ${i}`)
    }
  }

  build() {
    List() {
      ForEach(this.data, (item: string, index: number) => {
        ListItem() {
          Row() {
            Text(item)
              .fontSize(16)
            Blank()
            Text(`#${index}`)
              .fontSize(14)
              .fontColor('#999999')
          }
          .width('100%')
          .padding(16)
        }
        .swipeAction({
          end: this.deleteButton(index)
        })
      }, (item: string, index: number) => `${item}_${index}`)
    }
    .width('100%')
    .height('100%')
    .listDirection(Axis.Vertical)
  }

  @Builder
  deleteButton(index: number) {
    Button('删除')
      .fontSize(14)
      .backgroundColor(Color.Red)
      .onClick(() => {
        this.data.splice(index, 1)
      })
  }
}

上面的例子还展示了 ListItemswipeAction 能力,用于实现侧滑删除操作,这是移动端列表非常常见的交互模式。

13.2 Grid 网格容器

Grid 用于展示网格数据,支持固定列数,配合 GridItem 完成每个格子的内容。它与 GridRow 不同:Grid 侧重数据驱动的网格列表,GridRow 侧重布局栅格。

@Component
struct GridDemo {
  private icons: string[] = ['📱', '💻', '⌚', '🎧', '📷', '🎮', '📺', '🔋']

  build() {
    Grid() {
      ForEach(this.icons, (icon: string, index: number) => {
        GridItem() {
          Column({ space: 8 }) {
            Text(icon)
              .fontSize(32)
            Text(`设备 ${index + 1}`)
              .fontSize(13)
              .fontColor('#666666')
          }
          .width('100%')
          .height('100%')
          .justifyContent(FlexAlign.Center)
        }
        .borderRadius(12)
        .backgroundColor(index % 2 === 0 ? '#F0F7F4' : '#FDF0E7')
      }, (icon: string, index: number) => `${icon}_${index}`)
    }
    .columnsTemplate('1fr 1fr 1fr 1fr')
    .rowsGap(12)
    .columnsGap(12)
    .height(220)
    .padding(16)
  }
}

13.3 Scroll 与 WaterFlow

Scroll 是基础滚动容器,适合内容不固定、需要整体滚动的场景;WaterFlow 是瀑布流容器,适合小红书式的不等高卡片流。

@Component
struct ScrollDemo {
  build() {
    Scroll() {
      Column({ space: 12 }) {
        ForEach([1, 2, 3, 4, 5, 6, 7, 8], (item: number) => {
          Text(`滚动内容块 ${item}`)
            .width('100%')
            .height(80)
            .textAlign(TextAlign.Center)
            .backgroundColor('#E8F1F2')
            .borderRadius(12)
        })
      }
      .padding(16)
    }
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Auto)
    .edgeEffect(EdgeEffect.Spring)
  }
}

列表性能优化有几个关键实践:为 ForEachLazyForEach 提供稳定且唯一的键;列表项避免嵌套过深的布局;图片使用懒加载和按需解码;对复杂列表项开启组件复用。

14. 网络与数据持久化基础

UI 框架本身不提供网络请求和数据库能力,但 ArkUI 应用可以通过 HarmonyOS 的系统能力组件完成这些任务。掌握网络请求与数据持久化的基本方式,才能构建完整可用的应用。

14.1 网络请求

HarmonyOS 提供了 @kit.NetworkKit 中的 HTTP 请求能力,通过 http.createHttp() 发起请求。需要注意的是,网络请求涉及权限声明、异步处理和错误处理。

import { http } from '@kit.NetworkKit'

async function fetchArticles(): Promise<string> {
  const client = http.createHttp()
  try {
    const response = await client.request('https://api.example.com/articles', {
      method: http.RequestMethod.GET,
      header: {
        'Content-Type': 'application/json'
      },
      connectTimeout: 10000,
      readTimeout: 10000
    })
    if (response.responseCode === 200) {
      return response.result as string
    }
    return ''
  } finally {
    client.destroy()
  }
}

14.2 轻量偏好存储

对于简单的键值对数据,可以使用 preferences 能力。对于结构化数据和复杂查询,推荐使用关系型数据库 relationalStore

import { preferences } from '@kit.ArkData'

async function saveSetting(key: string, value: string): Promise<void> {
  const store = await preferences.getPreferences(getContext(), 'app_settings')
  await store.put(key, value)
  await store.flush()
}

async function readSetting(key: string): Promise<string> {
  const store = await preferences.getPreferences(getContext(), 'app_settings')
  const value = await store.get(key, '')
  return value as string
}

在实际项目中,建议将数据访问逻辑封装为独立的 Repository 层,UI 组件通过状态管理装饰器和异步方法获取数据,保持层次清晰、便于测试。

15. 多设备适配与响应式设计

「一次开发、多端部署」是 ArkUI 的核心价值主张,也是它与传统单一设备框架最大的差异。实现多设备适配需要综合运用断点系统、栅格布局、媒体查询和组件多态能力。

15.1 断点系统

断点系统根据屏幕宽度把设备分为几个区间。通过监听断点变化,开发者可以在代码中针对不同宽度区间采用不同的布局策略。

@Entry
@Component
struct ResponsivePage {
  @State currentBreakpoint: string = 'sm'

  aboutToAppear(): void {
    this.updateBreakpoint()
  }

  updateBreakpoint(): void {
    const width = this.getUIContext().getHostContext()?.config?.windowWidth ?? 0
    if (width >= 992) {
      this.currentBreakpoint = 'lg'
    } else if (width >= 768) {
      this.currentBreakpoint = 'md'
    } else {
      this.currentBreakpoint = 'sm'
    }
  }

  build() {
    Column() {
      Text(`当前断点:${this.currentBreakpoint}`)
        .fontSize(16)

      if (this.currentBreakpoint === 'lg') {
        this.wideLayout()
      } else if (this.currentBreakpoint === 'md') {
        this.mediumLayout()
      } else {
        this.narrowLayout()
      }
    }
    .width('100%')
    .height('100%')
    .onAreaChange(() => {
      this.updateBreakpoint()
    })
  }

  @Builder
  wideLayout() {
    Row({ space: 16 }) {
      this.panel('左侧面板')
      this.panel('中间面板')
      this.panel('右侧面板')
    }
  }

  @Builder
  mediumLayout() {
    Row({ space: 16 }) {
      this.panel('左侧面板')
      this.panel('右边内容')
    }
  }

  @Builder
  narrowLayout() {
    Column({ space: 16 }) {
      this.panel('顶部内容')
      this.panel('中间内容')
      this.panel('底部内容')
    }
  }

  @Builder
  panel(label: string) {
    Column() {
      Text(label)
    }
    .layoutWeight(1)
    .height(160)
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#DCE9F0')
    .borderRadius(12)
  }
}

15.2 媒体查询与栅格

除了在代码中判断断点,也可以结合 GridRow 的响应式 span 配置实现声明式适配。对于样式层面的差异,可以使用媒体查询能力。栅格布局的 span 对象可以针对 sm、md、lg、xl、xxl 分别指定列数,框架会根据当前屏幕宽度自动选择。

多设备适配并非简单的「拉伸界面」。真正优质的响应式设计需要重新思考信息架构:手机上是单列流式布局,平板上是双栏或三栏,折叠屏展开后可以利用更大的空间展示更多层级。利用 ArkUI 的分栏组件与断点能力,可以让同一套代码在形态各异的设备上都呈现合理的体验。

16. 渲染管线与性能优化

性能是评价 UI 框架的关键维度。ArkUI 的渲染管线包含构建、布局、绘制和渲染几个阶段,理解每个阶段的成本来源,才能有针对性地做优化。

16.1 渲染流程概述

  1. 构建阶段:状态变化触发组件 build 重新执行,生成新的 UI 描述。
  2. 布局阶段:框架递归测量每个组件的大小并确定位置。
  3. 绘制阶段:将布局结果转换为绘制指令。
  4. 渲染合成:绘图指令提交到 GPU,完成最终显示。

从优化角度看,构建阶段的成本与「状态更新的影响范围」直接相关;布局阶段的成本与「布局树的复杂度和变更频率」相关;绘制阶段则受「绘制面积、层数、特效复杂度」影响。

16.2 常见性能优化手段

  • 缩小状态作用域:把状态放在最小必要组件,避免页面级大范围刷新。
  • 使用稳定键值ForEachLazyForEach 的键生成函数要返回稳定唯一值,不要使用数组下标作为唯一依据(除非数据永远不变)。
  • 长列表懒加载:大数据量列表必须使用 LazyForEach,避免一次性构建全部节点。
  • 组件复用:通过 @Reusable 装饰器和 aboutToReuse 生命周期复用列表项组件,减少创建和销毁开销。
  • 避免深层嵌套:布局嵌套越深,测量和布局成本越高。尽量扁平化结构,减少不必要的包装容器。
  • 动画使用 transform:优先使用 translate、scale、rotate 属性做动画,避免触发重新布局。
  • 图片优化:使用合适尺寸的图片资源,开启图片缓存,避免频繁解码大图。
  • 减少不必要的日志:高频回调(如滚动、拖拽)中的 console 输出会显著影响性能。

16.3 性能分析工具

DevEco Studio 提供了性能分析工具(Profiler),可以查看帧率、CPU、内存、布局耗时等指标。开发者还可以通过 SmartPerf 等工具进行真机性能数据采集。在优化前,应先通过工具定位瓶颈,避免盲目优化。

性能优化的重要原则是「先测量,再优化」。很多直觉上的优化可能收效甚微,而真正的瓶颈往往隐藏在布局复杂度过高、状态更新过于频繁或不恰当的列表实现中。

17. 分布式能力与元服务

ArkUI 与鸿蒙系统的分布式能力深度结合,这是它区别于其他框架的重要特色。分布式能力让应用可以在多设备间流转,元服务让应用以更轻量的形态触达用户。

17.1 分布式数据与跨设备协同

鸿蒙的分布式软总线让设备间通信变得简单。结合分布式数据管理能力,应用可以实现在手机、平板、智慧屏之间的数据同步和任务流转。ArkUI 应用通过系统提供的分布式数据接口,可以在用户无感知的情况下完成跨设备数据一致。

跨端迁移是分布式体验的典型场景:用户在手机上编辑文档,靠近平板后可以将当前任务无缝迁移到平板上继续编辑。这种能力依赖系统的分布式任务调度,ArkUI 的组件状态需要配合系统生命周期完成迁移前后的保存和恢复。

17.2 原子化服务(元服务)

元服务是免安装、轻量化的应用形态,用户无需下载安装即可使用。一个元服务通常对应若干 Entry 形式的页面(称为服务卡片或服务页面),通过碰一碰、搜索、桌面卡片等方式触达。元服务对包体积、启动速度和功能聚焦有更高要求,ArkUI 在构建元服务时同样适用。

元服务与卡片能力密切相关。服务卡片是展示在桌面上的轻量 UI,可以使用 ArkUI 的卡片开发能力(ArkTS 卡片)构建。卡片运行在受限环境中,有独立的生命周期和更新机制,开发时需要注意数据更新频率和内存限制。

18. 与主流声明式框架的对比

将 ArkUI 与 Flutter、SwiftUI、Jetpack Compose、React Native 等框架对比,有助于理解它的技术路线和生态定位。

维度ArkUIFlutterSwiftUIJetpack ComposeReact Native
声明式 UI
开发语言ArkTSDartSwiftKotlinJavaScript/TypeScript
渲染方式自绘引擎自绘引擎原生控件原生控件原生控件
跨平台范围鸿蒙多设备iOS/Android/Web/桌面Apple 生态Android 生态iOS/Android/Web
状态管理装饰器体系StatefulWidget/Provider 等@State 等属性包装器remember/mutableStateuseState/Redux 等
多设备适配断点/栅格/多态,体系完整需自行适配Size ClassWindowSizeClass需自行适配
分布式/系统融合深度整合Apple 生态接力

从对比可以看出,ArkUI 的技术路线与 Flutter 类似(自绘渲染 + 声明式),但在生态定位上聚焦鸿蒙体系,并与系统分布式能力深度绑定。对于已经在鸿蒙生态内做开发的团队,ArkUI 是唯一能完整释放系统能力的 UI 框架。

19. 实战案例:构建一个完整的待办事项应用

下面通过一个相对完整的待办事项应用,把前面介绍的知识串联起来。这个案例涵盖数据模型、状态管理、列表渲染、输入交互、事件处理、弹窗提示和持久化。

19.1 数据模型

@Observed
class TodoItem {
  id: number
  title: string
  done: boolean
  createdAt: number

  constructor(id: number, title: string) {
    this.id = id
    this.title = title
    this.done = false
    this.createdAt = Date.now()
  }
}

19.2 页面主体与状态

@Entry
@Component
struct TodoPage {
  @State todos: TodoItem[] = []
  @State inputText: string = ''
  @State filter: 'all' | 'active' | 'completed' = 'all'
  private nextId: number = 1

  get filteredTodos(): TodoItem[] {
    if (this.filter === 'active') {
      return this.todos.filter(item => !item.done)
    }
    if (this.filter === 'completed') {
      return this.todos.filter(item => item.done)
    }
    return this.todos
  }

  get activeCount(): number {
    return this.todos.filter(item => !item.done).length
  }

  addTodo(): void {
    const title = this.inputText.trim()
    if (title.length === 0) {
      return
    }
    this.todos.push(new TodoItem(this.nextId++, title))
    this.inputText = ''
  }

  toggleTodo(id: number): void {
    const todo = this.todos.find(item => item.id === id)
    if (todo) {
      todo.done = !todo.done
    }
  }

  removeTodo(id: number): void {
    this.todos = this.todos.filter(item => item.id !== id)
  }

  clearCompleted(): void {
    this.todos = this.todos.filter(item => !item.done)
  }

  build() {
    Column({ space: 16 }) {
      Text('待办事项')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)

      Row({ space: 12 }) {
        TextInput({ placeholder: '请输入待办事项', text: this.inputText })
          .layoutWeight(1)
          .height(44)
          .onChange((value: string) => {
            this.inputText = value
          })

        Button('添加')
          .height(44)
          .onClick(() => {
            this.addTodo()
          })
      }
      .width('100%')

      Row({ space: 12 }) {
        Button('全部')
          .height(36)
          .fontSize(14)
          .fontColor(this.filter === 'all' ? Color.White : '#333333')
          .backgroundColor(this.filter === 'all' ? '#4ECDC4' : '#F0F0F0')
          .onClick(() => {
            this.filter = 'all'
          })

        Button('进行中')
          .height(36)
          .fontSize(14)
          .fontColor(this.filter === 'active' ? Color.White : '#333333')
          .backgroundColor(this.filter === 'active' ? '#4ECDC4' : '#F0F0F0')
          .onClick(() => {
            this.filter = 'active'
          })

        Button('已完成')
          .height(36)
          .fontSize(14)
          .fontColor(this.filter === 'completed' ? Color.White : '#333333')
          .backgroundColor(this.filter === 'completed' ? '#4ECDC4' : '#F0F0F0')
          .onClick(() => {
            this.filter = 'completed'
          })
      }
      .width('100%')

      if (this.filteredTodos.length > 0) {
        ForEach(this.filteredTodos, (todo: TodoItem) => {
          Row({ space: 12 }) {
            Text(todo.title)
              .layoutWeight(1)
              .fontSize(16)
              .fontColor(todo.done ? '#999999' : '#333333')
              .decoration({
                type: todo.done ? TextDecorationType.LineThrough : TextDecorationType.None
              })

            Button(todo.done ? '已完成' : '未完成')
              .height(36)
              .fontSize(13)
              .onClick(() => {
                this.toggleTodo(todo.id)
              })

            Button('删除')
              .height(36)
              .fontSize(13)
              .backgroundColor('#FF6B6B')
              .onClick(() => {
                this.removeTodo(todo.id)
              })
          }
          .width('100%')
          .padding(12)
        }, (todo: TodoItem) => todo.id.toString())
      } else {
        Text('暂无待办事项')
          .fontSize(16)
          .fontColor('#999999')
          .margin({ top: 24 })
      }

      Row() {
        Text(`剩余 ${this.activeCount} 项未完成`)
          .fontSize(14)
          .fontColor('#666666')

        Blank()

        Button('清除已完成')
          .height(36)
          .fontSize(14)
          .fontColor('#FF6B6B')
          .backgroundColor('#FFF0F0')
          .onClick(() => {
            this.clearCompleted()
          })
      }
      .width('100%')
    }
    .width('100%')
    .height('100%')
    .padding(16)
  }
}

19.3 页面交互与事件处理

页面主体搭建完成后,接下来要让页面“动起来”。交互的核心是事件处理:用户在页面上的点击、输入、滚动、键盘操作等行为都会触发事件,开发者通过监听这些事件来响应用户意图。理解事件模型,可以让页面主体从静态结构变成可交互的界面。

19.3.1 认识浏览器事件

浏览器事件是用户或浏览器自身发出的信号。常见事件包括:

  • 鼠标事件clickdblclickmouseentermouseleave
  • 键盘事件keydownkeyupkeypress
  • 表单事件inputchangesubmitfocusblur
  • 页面事件loadresizescroll

事件触发后会沿着 DOM 树进行传播,通常分为捕获阶段、目标阶段和冒泡阶段。日常开发中更多使用冒泡阶段,因为可以通过父级容器统一处理子元素的事件。

19.3.2 事件的绑定方式

推荐使用 addEventListener 绑定事件,它支持多次绑定、指定触发阶段,也方便在组件销毁时解绑。

const button = document.getElementById('submitBtn');

function handleClick(event) {
  console.log('按钮被点击', event.target);
}

button.addEventListener('click', handleClick);

// 组件销毁或不再需要时解绑
button.removeEventListener('click', handleClick);

使用 addEventListener 的好处是:同一个元素可以绑定多个处理函数,彼此互不覆盖;同时可以接收事件对象,获取触发目标、按键信息、坐标等关键数据。

19.3.3 事件委托

当页面主体中需要处理大量同级元素的事件时,逐个绑定会带来较多内存开销,动态新增的元素还需要重复绑定。此时可以使用事件委托,把事件监听挂在父级容器上,通过事件冒泡统一处理。

<ul id="todoList">
  <li>学习 JavaScript</li>
  <li>编写示例代码</li>
  <li>整理笔记</li>
</ul>
const list = document.getElementById('todoList');

list.addEventListener('click', function (event) {
  if (event.target.tagName === 'LI') {
    console.log('点击了任务:', event.target.textContent);
  }
});

事件委托不仅减少监听器数量,还能自动覆盖后续动态插入的列表项,适合待办列表、表格行、菜单项等场景。

19.3.4 常见交互场景

结合上一节的状态驱动思路,可以把事件处理与状态更新统一起来:事件处理函数只负责更新状态,主体渲染由 render 完成。

1. 点击按钮进行增删改查
const state = { items: [], inputValue: '' };

function render() {
  const listEl = document.getElementById('list');
  listEl.innerHTML = '';

  state.items.forEach((item, index) => {
    const li = document.createElement('li');
    li.textContent = item;

    const deleteBtn = document.createElement('button');
    deleteBtn.textContent = '删除';
    deleteBtn.addEventListener('click', () => {
      state.items.splice(index, 1);
      render();
    });

    li.appendChild(deleteBtn);
    listEl.appendChild(li);
  });
}

document.getElementById('addBtn').addEventListener('click', () => {
  const input = document.getElementById('input');
  state.items.push(input.value);
  input.value = '';
  render();
});
2. 表单输入与即时校验

通过 input 事件监听输入内容,实时更新状态并给出提示。

const usernameInput = document.getElementById('username');
const tip = document.getElementById('tip');

usernameInput.addEventListener('input', function (event) {
  const value = event.target.value;
  if (value.length < 3) {
    tip.textContent = '用户名至少需要 3 个字符';
  } else {
    tip.textContent = '输入合法';
  }
});
3. 键盘快速操作

例如在待办输入框中按压回车键自动添加任务,减少鼠标操作。

document.getElementById('input').addEventListener('keydown', function (event) {
  if (event.key === 'Enter') {
    document.getElementById('addBtn').click();
  }
});

19.3.5 事件处理中的性能与注意事项

  • 避免重复绑定:在循环内绑定事件时要谨慎,优先使用事件委托。
  • 及时解绑:组件卸载、页面跳转或定时器终止后,记得移除不再需要的事件监听。
  • 防止高频触发的性能问题:滚动、缩放、输入等高频事件可以采用防抖或节流策略,延缓执行频率。
  • 控制事件处理函数体积:处理函数尽量只做状态更新和必要的 DOM 操作,复杂逻辑拆分到独立函数。

19.3.6 小结

页面交互与事件处理是连接用户与页面主体的桥梁。通过合理绑定事件、使用事件委托、配合状态驱动渲染,可以让代码更清晰、扩展性更强。下一节将在此基础上继续讨论表单状态与校验,进一步完善页面交互能力。

19.4 表单状态与校验

表单是页面中最常见的交互模块,几乎所有登录、注册、搜索、资料编辑功能都依赖表单。表单开发的核心是状态管理数据校验:既要准确记录用户输入,又要在提交前及时发现并提示错误,避免无效数据进入业务流程。

19.4.1 表单状态的基本组成

一个表单的状态通常包括:

  • 字段值:各输入框、下拉框、单选框、复选框的当前值。
  • 校验错误:每个字段的错误提示信息。
  • 提交状态:是否正在提交、是否提交成功。
  • 交互状态:某个字段是否被触碰过,用于控制提示时机。
const formState = {
  values: {
    username: '',
    email: '',
    password: ''
  },
  errors: {},
  touched: {},
  submitting: false
};

19.4.2 状态与表单控件同步

为了让输入框显示的内容与状态保持一致,需要在输入事件中更新对应字段。

function handleInput(field, value) {
  formState.values[field] = value;
  formState.touched[field] = true;
  validateField(field, value);
  renderForm();
}

这样状态成为表单的唯一数据来源,输入框只负责展示状态值。无论是程序自动填充、清空表单,还是用户手动输入,都可以通过修改状态完成。

19.4.3 常见校验规则

  • 必填校验:字段不能为空。
  • 长度校验:限制最小长度和最大长度。
  • 格式校验:邮箱、手机号、URL 等是否符合格式。
  • 一致性校验:两次输入的密码是否一致。
  • 业务校验:例如用户名是否已存在,需要与后端交互。
function validateField(field, value) {
  const errors = [];

  if (!value.trim()) {
    errors.push('该字段不能为空');
  }

  if (field === 'email' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
    errors.push('请输入有效的邮箱地址');
  }

  formState.errors[field] = errors;
}

19.4.4 表单提交流程

提交前先校验全部字段,校验通过后再发送请求,避免把无效数据提交给后端。

async function handleSubmit(event) {
  event.preventDefault();

  Object.keys(formState.values).forEach((field) => {
    validateField(field, formState.values[field]);
  });

  const hasError = Object.values(formState.errors).some((errors) => errors.length > 0);
  if (hasError) {
    renderForm();
    return;
  }

  formState.submitting = true;
  renderForm();

  try {
    await saveForm(formState.values);
    alert('提交成功');
  } catch (error) {
    alert('提交失败,请稍后重试');
  } finally {
    formState.submitting = false;
    renderForm();
  }
}

19.4.5 表单开发中的最佳实践

  • 即时提示:在用户输入时校验,但要等字段被触碰后再提示,避免一开始就显示一堆错误。
  • 集中管理状态:把表单字段、错误和提交状态放在同一个对象中,便于调试和扩展。
  • 友好错误提示:明确告诉用户哪里出错、应该如何修改。
  • 防止重复提交:提交过程中禁用按钮或标记提交状态。
  • 保护用户输入:提交失败时保留已填写的内容,不要清空表单。

19.5 异步数据加载与状态同步

真实项目中,页面主体展示的数据大多来自后端接口。加载数据、处理加载状态、展示错误信息,以及将接口数据同步到页面状态,是每个前端开发者必须掌握的技能。

19.5.1 异步请求的基本流程

  1. 设置加载状态。
  2. 发起异步请求。
  3. 请求成功后把数据写入状态。
  4. 更新页面主体。
  5. 请求失败时记录错误信息。

现代浏览器提供 fetch 作为原生请求方法,配合 asyncawait 可以写出清晰易懂的异步代码。

async function loadUsers() {
  const state = { loading: true, users: [], error: '' };

  try {
    const response = await fetch('/api/users');
    if (!response.ok) {
      throw new Error('请求失败');
    }
    state.users = await response.json();
  } catch (error) {
    state.error = error.message || '加载失败';
  } finally {
    state.loading = false;
    renderArticle(state);
  }
}

19.5.2 在页面主体中展示加载、成功和失败状态

function renderArticle(state) {
  const container = document.getElementById('articleBody');

  if (state.loading) {
    container.innerHTML = '<p>加载中,请稍候……</p>';
    return;
  }

  if (state.error) {
    container.innerHTML = '<p class="error">加载失败:' + state.error + '</p>';
    return;
  }

  const html = state.users
    .map((user) => `<li>${user.name}</li>`)
    .join('');
  container.innerHTML = '<ul>' + html + '</ul>';
}

页面主体需要明确区分加载中、加载失败和正常展示三种状态,让用户清楚系统当前发生了什么。

19.5.3 多请求与并发控制

当页面需要同时请求多个接口时,可以使用 Promise.all 并行处理,减少等待时间。

const [users, roles] = await Promise.all([
  fetch('/api/users').then((res) => res.json()),
  fetch('/api/roles').then((res) => res.json())
]);

如果某些请求相互依赖,则需要按顺序执行。对于可独立完成的请求,优先采用并行方式。

19.5.4 避免数据不同步

  • 统一入口更新状态:所有接口返回的数据都通过固定函数写入状态,避免多处直接修改。
  • 及时清理旧数据:切换页面或重新加载时,先重置状态,防止旧数据残留。
  • 处理竞态:多次请求同一资源时,可能出现后发先至,导致页面展示旧数据。可以记录请求序号,只采用最新一次的响应。

19.6 本章小结

本章围绕页面主体与状态展开,先后讲解了页面主体的结构组织、状态驱动的渲染方式、事件处理、表单状态与校验,以及异步数据加载与状态同步。这些内容构成了前端开发的基本功:

  • 页面主体是用户看到的一切,组织好结构才能高效维护。
  • 状态是数据快照,状态改变驱动页面更新。
  • 事件处理负责响应用户操作,事件委托能减少绑定成本。
  • 表单开发需要同时管理字段值、校验错误和提交状态。
  • 异步数据加载后要正确同步到状态,并处理好加载、失败和空数据场景。

建议读者把这些概念放在同一个示例项目中反复练习,例如实现一个完整的待办列表应用,涵盖增删改查、搜索筛选、表单校验和接口保存功能。通过实际编写和调试,可以更好地理解页面主体与状态之间的协作关系。

系。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐