ArkUI框架介绍
从演进脉络看,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 环境准备
- 安装 DevEco Studio,建议使用与目标 HarmonyOS SDK 版本匹配的稳定版本。
- 在 DevEco Studio 中下载 HarmonyOS SDK、OpenHarmony SDK 与相关模拟器镜像。
- 配置签名信息,否则真机运行和部分调试能力会受到限制。
- 创建工程时选择 Empty Ability 模板,语言选择 ArkTS,即可得到一个最小的 ArkUI 工程。
工程结构遵循 Stage 模型:entry/src/main/ets 目录存放 ArkTS 源码,pages 目录存放页面组件,resources 目录存放字符串、图片、颜色等资源,module.json5 描述模块基本信息与页面路由配置。
3.2 最小可运行示例
下面是一个最经典的 Hello World 页面,展示了 @Entry、@Component、build() 和 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 支持基础类型 number、string、boolean、null、undefined,也支持 enum、union 类型、interface 和 class。在组件开发中,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。通过组合 layoutWeight、justifyContent 和 alignItems,可以实现大多数常规布局需求。
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 可以统一控制所有子组件的对齐方式,也可以通过单个子组件的 position、offset 或 align 属性做个性化定位。
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(如 push、splice),而对整个数组重新赋值总是安全的。
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.json 或 route_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 提供了一套完整的事件处理机制,包括点击、触摸、拖拽、手势、按键等。事件绑定以链式方法的形式出现在组件上,如 onClick、onTouch、onChange、onAppear、onDisAppear 等。此外,通用手势系统支持单击、双击、长按、滑动、拖拽、捏合和旋转手势。
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 用于显示文本,支持省略号、换行、对齐、装饰线、行高、字体等丰富属性。Span 和 Text 配合可以在一段文本中混排不同样式。
@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 支持多种样式(胶囊、圆形、普通),可通过 ButtonType 和 stateEffect 等属性定制外观和按压效果。
@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 提供 Dialog、AlertDialog、ActionSheet、Toast、Popup 等多种提示组件。其中 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 中的 List、Grid、Scroll 和 WaterFlow 提供了高性能的数据展示能力,配合 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)
})
}
}
上面的例子还展示了 ListItem 的 swipeAction 能力,用于实现侧滑删除操作,这是移动端列表非常常见的交互模式。
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)
}
}
列表性能优化有几个关键实践:为 ForEach 和 LazyForEach 提供稳定且唯一的键;列表项避免嵌套过深的布局;图片使用懒加载和按需解码;对复杂列表项开启组件复用。
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 渲染流程概述
- 构建阶段:状态变化触发组件 build 重新执行,生成新的 UI 描述。
- 布局阶段:框架递归测量每个组件的大小并确定位置。
- 绘制阶段:将布局结果转换为绘制指令。
- 渲染合成:绘图指令提交到 GPU,完成最终显示。
从优化角度看,构建阶段的成本与「状态更新的影响范围」直接相关;布局阶段的成本与「布局树的复杂度和变更频率」相关;绘制阶段则受「绘制面积、层数、特效复杂度」影响。
16.2 常见性能优化手段
- 缩小状态作用域:把状态放在最小必要组件,避免页面级大范围刷新。
- 使用稳定键值:
ForEach和LazyForEach的键生成函数要返回稳定唯一值,不要使用数组下标作为唯一依据(除非数据永远不变)。 - 长列表懒加载:大数据量列表必须使用
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 等框架对比,有助于理解它的技术路线和生态定位。
| 维度 | ArkUI | Flutter | SwiftUI | Jetpack Compose | React Native |
|---|---|---|---|---|---|
| 声明式 UI | 是 | 是 | 是 | 是 | 是 |
| 开发语言 | ArkTS | Dart | Swift | Kotlin | JavaScript/TypeScript |
| 渲染方式 | 自绘引擎 | 自绘引擎 | 原生控件 | 原生控件 | 原生控件 |
| 跨平台范围 | 鸿蒙多设备 | iOS/Android/Web/桌面 | Apple 生态 | Android 生态 | iOS/Android/Web |
| 状态管理 | 装饰器体系 | StatefulWidget/Provider 等 | @State 等属性包装器 | remember/mutableState | useState/Redux 等 |
| 多设备适配 | 断点/栅格/多态,体系完整 | 需自行适配 | Size Class | WindowSizeClass | 需自行适配 |
| 分布式/系统融合 | 深度整合 | 无 | 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 认识浏览器事件
浏览器事件是用户或浏览器自身发出的信号。常见事件包括:
- 鼠标事件:
click、dblclick、mouseenter、mouseleave。 - 键盘事件:
keydown、keyup、keypress。 - 表单事件:
input、change、submit、focus、blur。 - 页面事件:
load、resize、scroll。
事件触发后会沿着 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 异步请求的基本流程
- 设置加载状态。
- 发起异步请求。
- 请求成功后把数据写入状态。
- 更新页面主体。
- 请求失败时记录错误信息。
现代浏览器提供 fetch 作为原生请求方法,配合 async 和 await 可以写出清晰易懂的异步代码。
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 本章小结
本章围绕页面主体与状态展开,先后讲解了页面主体的结构组织、状态驱动的渲染方式、事件处理、表单状态与校验,以及异步数据加载与状态同步。这些内容构成了前端开发的基本功:
- 页面主体是用户看到的一切,组织好结构才能高效维护。
- 状态是数据快照,状态改变驱动页面更新。
- 事件处理负责响应用户操作,事件委托能减少绑定成本。
- 表单开发需要同时管理字段值、校验错误和提交状态。
- 异步数据加载后要正确同步到状态,并处理好加载、失败和空数据场景。
建议读者把这些概念放在同一个示例项目中反复练习,例如实现一个完整的待办列表应用,涵盖增删改查、搜索筛选、表单校验和接口保存功能。通过实际编写和调试,可以更好地理解页面主体与状态之间的协作关系。
系。
更多推荐



所有评论(0)