HarmonyOS用 ArkTS 描述界面,让数据驱动渲染
HarmonyOS用 ArkTS 描述界面,让数据驱动渲染
声明式到底"声明"了什么
命令式编程里,你要告诉程序"怎么做"——先创建按钮,再设置它的位置,再绑定点击回调。每一步都是具体的操作指令。
声明式编程里,你只告诉程序"要什么"——这里有个按钮,文字是"登录",点它触发这个动作。至于按钮怎么绘制、怎么布局、怎么响应,统统交给框架。
用一个计数器的例子对比,差异一目了然:
// 命令式思路(伪代码,鸿蒙不这么写)
let count = 0;
let text = createText(count.toString());
let btn = createButton("+1");
btn.onClick(() => {
count++;
text.setText(count.toString()); // 手动更新 UI
});
// 声明式思路(ArkTS)
@Entry
@Component
struct Counter {
@State count: number = 0; // 声明一个状态变量
build() {
Column() {
Text(this.count.toString()) // UI 直接引用状态
.fontSize(40)
Button('+1')
.onClick(() => {
this.count++; // 只改数据,不改 UI
})
}
}
}
关键区别:声明式里 count++ 之后你没有碰任何 UI 代码,但界面会自动更新。这就是"数据驱动 UI",开发者只写引起界面变化的数据,界面怎么变交给框架。
ArkTS 的三个扩展
ArkTS 在 TypeScript 基础上做了扩展,扩展方向正好对应声明式 UI 的核心需求:
| 扩展 | 作用 | 典型语法 |
|---|---|---|
| 声明式 UI 描述 | 用接近自然语言的方式描述界面结构 | Column { Text(...) } |
| 自定义组件 | 把界面拆成可复用的独立单元 | @Component struct MyComp {} |
| 动态扩展 UI 元素 | 运行时动态增删界面元素 | @BuilderParams、if/forEach |
| 状态管理 | 声明数据与 UI 的绑定关系 | @State @Prop @Link |
| 渲染控制 | 根据条件/循环控制渲染 | if ForEach LazyForEach |
最小可运行结构
一个声明式页面,最小结构长这样:
// 页面入口
@Entry // 标记为页面入口,可被路由加载
@Component // 标记为自定义组件
struct HomePage {
build() { // 唯一入口,描述 UI 结构
Text('Hello ArkUI')
}
}
@Entry 和 @Component 是两个装饰器:
@Entry:表示这是一个页面,可以独立加载@Component:表示这是一个自定义组件
build() 是组件的唯一渲染入口,里面写 UI 结构。
UI 结构的组织
声明式 UI 通过组件嵌套组织。容器组件(如 Column)包住子组件,形成树状结构:
build() {
Column { // 容器:纵向排列
Text('标题')
.fontSize(24) // 链式调用设置属性
.fontWeight(FontWeight.Bold)
Row { // 容器:横向排列
Button('确定')
Button('取消')
}
}
.padding(16) // 容器自身的属性
.width('100%')
}
组件属性用链式调用设置,一个组件一行或几行,读起来接近自然语义。
状态管理让 UI 活起来
静态结构写好了,要让界面"活",靠状态管理。最常用的是 @State:
@Entry
@Component
struct LoginPage {
@State username: string = ''; // 输入框内容
@State remember: boolean = false; // 是否记住密码
build() {
Column({ space: 12 }) {
TextInput({ placeholder: '请输入用户名' })
.onChange((value: string) => {
this.username = value; // 输入变化,同步到状态
})
Text(`当前输入:${this.username}`) // 引用状态,自动刷新
Row() {
Checkbox()
.select(this.remember)
.onChange((checked: boolean) => {
this.remember = checked;
})
Text('记住密码')
}
Button('登录')
.enabled(this.username.length > 0) // 状态驱动可用性
.onClick(() => {
this.doLogin();
})
}
.padding(16)
}
doLogin(): void {
// 登录逻辑
}
}
这个例子里,username 变化会自动刷新两个地方:提示文本和登录按钮的可用状态。你只改了数据,框架自动定位到引用它的组件做更新。
渲染控制:条件与循环
列表和条件渲染是界面开发的高频需求,ArkTS 有专门语法:
@Entry
@Component
struct TaskList {
@State tasks: Task[] = []; // 任务列表
build() {
Column() {
// 条件渲染
if (this.tasks.length === 0) {
Text('暂无任务') // 空态
} else {
// 循环渲染
ForEach(this.tasks, (task: Task) => {
TaskItem({ task: task })
}, (task: Task) => task.id.toString()) // 第三个参数是 key,必须唯一
}
}
}
}
@Component
struct TaskItem {
task: Task; // 父组件传入的数据
build() {
Row() {
Text(this.task.title)
Button('完成')
}
}
}
两个要点:
ForEach的第三个参数(key 生成器)必须提供且返回唯一值,否则列表更新会错乱- 大数据量用
LazyForEach替代ForEach,只渲染可见项,性能更好
技术架构落到代码上
之前讲的后端引擎 C++ 架构,在代码层的体现是:你写的声明式描述,会被编译和运行时转换成对 C++ 组件的调用。开发时不用关心这个转换,但要知道两点:
-
UI 前端和后端分层:前端是你写的 ArkTS 结构,后端是 C++ 组件和渲染管线。数据在两边间传递,状态变化会触发后端重新计算。
-
编译器优化:统一字节码、AOT 编译、高效 FFI(跨语言调用)、引擎极小化。这些让 ArkTS 代码即使有跨语言调用,性能也能保证。
状态的两个层次
状态管理分两层,配合使用能构建完整应用:
应用级状态 AppStorage / LocalStorage / PersistentStorage
(跨页面共享) ↑ 全局数据、持久化配置
─────────────────────────────
组件级状态 @State / @Prop / @Link / @Watch
(组件内部/父子间) ↑ 单个组件的 UI 状态
先用组件级状态把单页逻辑写对,再用应用级状态处理跨页共享,这是清晰的状态分层思路。具体每个装饰器的用法,后面状态管理专题展开。
一条容易忽略的通则
声明式范式里,长度数字默认单位是 vp(虚拟像素)。写 .width(200) 就是 200vp,会自动适配不同屏幕密度。如果用 px 需要显式写 .width('200px')。
另一个注意点是异常值处理:传 undefined、null 或无效值时,有默认值的参数走默认值,没有默认值的参数直接不生效。这意味着不会因为传错值而崩溃,但也可能让你误以为设置成功了——调试时留意属性是否真的生效。
小小建议
-
先写结构再写状态。把
build()里的静态结构搭好,跑起来看到界面,再逐步加@State让界面动起来。一步到位容易乱。 -
链式调用分行写。一个属性一行,别挤在一行里,可读性差很多,团队协作尤其明显。
-
ForEach的 key 别省略。省略 key 列表更新会出 bug,这是列表开发最容易出问题的地方。 -
容器嵌套别太深。
Column套Row套Column套三层以上就该拆自定义组件了,深嵌套既难读也影响性能。
更多推荐
所有评论(0)