鸿蒙开发手记(一):从“傻瓜式”环境配置到首个 TodoList 实战
在上一篇中,我分享了为什么在 2026 年选择从 Cocos 游戏开发转战 HarmonyOS NEXT。今天,我们直接进入实战:聊聊环境搭建,并用 ArkTS 亲手撸一个 TodoList。
1. 环境配置:集成度极高的“傻瓜式”体验
对于习惯了在 VS Code 里折腾各种插件、Webpack 配置和浏览器调试的前端开发者来说,DevEco Studio 的初体验可以用 “省心” 来形容。
- 全家桶式的集成: 你不需要再去纠结模拟器怎么装、预览器怎么配。DevEco Studio 几乎把所有工具都集成在了一个安装包里。
- Previewer 与模拟器: 它的预览器响应速度很快,基本做到了代码改动即刻生效,这对于我们这种追求“所见即所得”的开发者非常友好。
- 内置 AI 的辅助: 虽然它内置的 AI 功能目前还比不上 Cursor 那样惊艳,但在处理一些基础的代码补全和 API 查询时,已经能节省不少翻文档的时间。
但是需要注意的是,在DevEco Studio中预览我们项目,有3种方式:
-
Previewer: Previewer是一种集成在DevEco 中的预览方式,它具备热更新的能力,当我们的代码修改后,previewer能够实时的做出改变
但是它也具有一些不足的地方:比如某些API不支持previewer环境,这一点十分的重要,所以一定在调试代码的时候一定要注意API的适用环境。
- 模拟器:模拟器是在电脑上安装一个模拟器,模拟器是一个单独的APP,相当于在电脑上运行了一个模拟Harmony OS的APP,在使用模拟器之前,我们需要先在设备管理器中安装一个模拟器,然后运行这个模拟器,然后我们就可以运行的时候选择该模拟器作为运行环境了。
- 真机调试:顾名思义,使用真机进行代码调试,对于一些特殊的场景只能使用真机调试的方式,比如意图开发场景中目前只支持真机调试。
2. 核心思维转换:从命令式到 MVVM
作为一个有着 5 年小游戏开发经验的人,我习惯了手动的去设置不同UI的值,但在 ArkUI 中,这种思维需要发生 180 度的转变。
数据驱动 UI(MVVM)
在 ArkTS 中,你只需要关注数据(State) 。当你通过 @State 定义了一个变量,UI 就会像影子的主人一样,主人动(数据变),影子就跟着动(UI 刷新)。这一点与前端的Vue 框架很类似。
前端开发者的“撞墙”点
虽然 ArkTS 看起来和 TypeScript 一模一样,但在父子组件通信上,它有几个非常硬核的“脾气”需要你适应:
- 禁止传递函数: 在 React 或 Vue 中,父组件向子组件传递一个回调函数是常规操作。但在 ArkUI 里,父组件不能直接向子组件传递函数类型的值。这需要我们习惯使用
@Link或自定义事件来处理交互。 - 传值还是传址? 这是一个坑。参数传递方式的不同(值传递 vs 引用传递)会直接决定 UI 能否感应到数据的变化。比如你修改了对象内部的一个属性,如果没处理好引用关系,UI 可能会纹丝不动。
3. 实战:手写一个TodoList
我们来写一个最基础的 TodoList。虽然我以前不爱写 CSS,但 ArkUI 的链式调用确实让布局变得像写逻辑一样清晰。在编写这个例子的时候也让我来讲解一些ArkTS中的语法。
组件的定义,在ArkTS中,如果要定义一个组件需要使用 struct 关键字来定义,而不是继续使用class ,除了使用 struct以外,还需要使用 @Component 装饰器来修饰它。一般来说我们会把跟UI无关的逻辑封装在class中,而struct 则专门负责UI的逻辑。
数据绑定
之前说了ArkTS是一种基于MVVM模型的双向数据绑定。需要UI响应的数据需要使用 @State装饰器修饰,如下面这个简单的例子
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
private flag: boolean = true;
build() {
Row() {
Column() {
Text(this.message)
.fontSize($r('app.float.page_text_font_size'))
.fontWeight(FontWeight.Bold)
.onClick(() => {
this.message = this.flag ? 'Welcome' : "Hello World";
this.flag = !this.flag;
})
}
.width('100%')
}
.height('100%')
}
}
可以看到这种数据绑定的方式十分的方便。
我现在要展示一种比较反前端直觉的案例,比如父子组件传递函数的情况。
在ArkTS中,不允许这样的做法!除了一种情况例外,父子之间传递函数,只能传递@Builder装饰器修饰的函数!
我们来看这样一个例子。在父组件中定义了一个状态msg,然后向子组件中传递一个修改msg 值的函数,让子组件的UI来改变这个值。
@Component
struct Child {
@Prop
updateMessage: (msg: string) => void;
build() {
Button('update parent message').onClick(() => {
this.updateMessage('from child msg');
})
}
}
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
private updateMessage: (msg: string) => void = (msg: string) => {
this.message = msg;
}
private flag: boolean = true;
build() {
Row() {
Column() {
Text(this.message)
.fontSize($r('app.float.page_text_font_size'))
.fontWeight(FontWeight.Bold)
.onClick(() => {
this.message = this.flag ? 'Welcome' : "Hello World";
this.flag = !this.flag;
})
Child({ updateMessage: this.updateMessage })
}
.width('100%')
}
.height('100%')
}
}
编辑页面中并没有提示语法错误,但是一旦运行,编译器还是报错了。以下是报错信息:
Error message:@Component '@Component 'Child'[8]': Illegal variable value error with decorated variable @Prop 'updateMessage': failed validation: 'undefined, null, number, boolean, string, or Object but not function, not V2 @ObservedV2 / @Trace class, and makeObserved return value either, attempt to assign value type: 'function', value: 'undefined'!
大意就是不支持传递函数类型!那么应该如何修改呢?在ArkTS中,允许子组件直接修改父组件的状态!我们需要在子组件中把父组件传递的属性使用 @Link装饰器修饰,这表示这是一个父组件传递进来的状态,并且在子组件中修改它时,父组件中的值也会发生相应的变化!
我们把updateMessage 变为一个基本数据类型,并用 @Link装饰器修饰,然后在父组件中传递父组件的值。
@Component
struct Child {
@Link
parentMsg: string;
build() {
Button('update parent message').onClick(() => {
this.parentMsg = 'from Child Message';
})
}
}
@Entry
@Component
struct Index {
build() {
...
Child({ parentMsg: this.message })
}
}
运行效果如下:
有了以上的基础知识,我们可以开始开发一个基本的TodoList。
下面通过一个 TodoList 案例来展示 ArkTS 的核心语法。在鸿蒙中,我们推荐将数据逻辑封装在 class 中,而 UI 结构定义在 struct 内部。
为什么需要 @Observed 和 @ObjectLink?
在开发 TodoList 这种包含对象数组的列表时,简单的 @State 是不够的。
@State的局限: 它只能观察到变量本身的变化。如果你修改了数组中某个对象成员的内部属性(例如task.isCompleted = true),由于数组的引用地址没变,UI 不会触发刷新。@Observed: 装饰在类定义上,使类成员的属性变化可以被追踪。@ObjectLink: 装饰在子组件的变量上,用来接收父组件传来的Observed对象。这样子组件修改对象属性时,父组件对应的 UI 也会同步更新。
// 1. 定义数据模型,使用 @Observed 确保属性可被追踪
@Observed
class Task {
id: number;
content: string = "";
isCompleted: boolean = false;
constructor(id: number, content: string, isCompleted: boolean = false) {
this.id = id;
this.content = content;
this.isCompleted = isCompleted;
}
}
// 2. 抽离列表项子组件,使用 @ObjectLink 同步状态
@Component
struct TaskView {
@ObjectLink task: Task;
build() {
Flex({ justifyContent: FlexAlign.SpaceAround }) {
Text(this.task.id.toString())
.fontSize(16)
.fontColor('#333333')
.margin({ right: 10 })
Text(this.task.content)
.fontSize(16)
.fontColor('#333333')
.margin({ right: 10 })
.decoration({
type: this.task.isCompleted ? TextDecorationType.LineThrough : TextDecorationType.None
})
.layoutWeight(1)
Text(this.task.isCompleted ? "已完成" : "未完成")
.fontSize(16)
.fontColor(this.task.isCompleted ? '#00AA00' : '#FF0000')
.onClick(() => {
// 修改状态,UI 自动响应
this.task.isCompleted = !this.task.isCompleted
})
}
.width('100%')
.padding(10)
.backgroundColor('#FFFFFF')
.borderRadius(8)
.margin({ bottom: 10 })
}
}
// 3. 主页面容器
@Component
export struct TodoList {
@State list: Task[] = [
new Task(1, "熟悉项目结构"),
new Task(2, "运行模拟器", true),
new Task(3, "编写第一个组件")
];
build() {
Column() {
Text("待办清单")
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin(20)
ForEach(this.list, (item: Task) => {
TaskView({ task: item });
}, (item: Task) => item.id.toString());
}
.width('100%')
.height('100%')
.backgroundColor('#F1F3F5')
}
}
4. 总结
通过 TodoList 的练习,可以发现鸿蒙开发的门槛并不在于语言,而在于对 Stage 模型和状态装饰器的理解。
对于前端开发者,ArkUI 的组件化思维是互通的,但需要额外关注父子组件间关于函数传递的限制。对于游戏开发者,从手动更新 UI 转向声明式的数据驱动,能够显著减少在界面维护上的逻辑代码量。