鸿蒙开发手记(一):从“傻瓜式”环境配置到首个 TodoList 实战

76 阅读7分钟

鸿蒙开发手记(一):从“傻瓜式”环境配置到首个 TodoList 实战

在上一篇中,我分享了为什么在 2026 年选择从 Cocos 游戏开发转战 HarmonyOS NEXT。今天,我们直接进入实战:聊聊环境搭建,并用 ArkTS 亲手撸一个 TodoList。

1. 环境配置:集成度极高的“傻瓜式”体验

对于习惯了在 VS Code 里折腾各种插件、Webpack 配置和浏览器调试的前端开发者来说,DevEco Studio 的初体验可以用 “省心” 来形容。

  • 全家桶式的集成: 你不需要再去纠结模拟器怎么装、预览器怎么配。DevEco Studio 几乎把所有工具都集成在了一个安装包里。
  • Previewer 与模拟器: 它的预览器响应速度很快,基本做到了代码改动即刻生效,这对于我们这种追求“所见即所得”的开发者非常友好。
  • 内置 AI 的辅助: 虽然它内置的 AI 功能目前还比不上 Cursor 那样惊艳,但在处理一些基础的代码补全和 API 查询时,已经能节省不少翻文档的时间。

但是需要注意的是,在DevEco Studio中预览我们项目,有3种方式:

  1. Previewer: Previewer是一种集成在DevEco 中的预览方式,它具备热更新的能力,当我们的代码修改后,previewer能够实时的做出改变

    但是它也具有一些不足的地方:比如某些API不支持previewer环境,这一点十分的重要,所以一定在调试代码的时候一定要注意API的适用环境。

image-20260201153159100

  1. 模拟器:模拟器是在电脑上安装一个模拟器,模拟器是一个单独的APP,相当于在电脑上运行了一个模拟Harmony OS的APP,在使用模拟器之前,我们需要先在设备管理器中安装一个模拟器,然后运行这个模拟器,然后我们就可以运行的时候选择该模拟器作为运行环境了。
  1. 真机调试:顾名思义,使用真机进行代码调试,对于一些特殊的场景只能使用真机调试的方式,比如意图开发场景中目前只支持真机调试。

2. 核心思维转换:从命令式到 MVVM

作为一个有着 5 年小游戏开发经验的人,我习惯了手动的去设置不同UI的值,但在 ArkUI 中,这种思维需要发生 180 度的转变。

数据驱动 UI(MVVM)

在 ArkTS 中,你只需要关注数据(State) 。当你通过 @State 定义了一个变量,UI 就会像影子的主人一样,主人动(数据变),影子就跟着动(UI 刷新)。这一点与前端的Vue 框架很类似。

前端开发者的“撞墙”点

虽然 ArkTS 看起来和 TypeScript 一模一样,但在父子组件通信上,它有几个非常硬核的“脾气”需要你适应:

  1. 禁止传递函数: 在 React 或 Vue 中,父组件向子组件传递一个回调函数是常规操作。但在 ArkUI 里,父组件不能直接向子组件传递函数类型的值。这需要我们习惯使用 @Link 或自定义事件来处理交互。
  2. 传值还是传址? 这是一个坑。参数传递方式的不同(值传递 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%')
  }
}

iShot_2026-02-01_15.42.29

可以看到这种数据绑定的方式十分的方便。

我现在要展示一种比较反前端直觉的案例,比如父子组件传递函数的情况。

在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 })
  } 
}

运行效果如下:

iShot_2026-02-01_15.58.24

有了以上的基础知识,我们可以开始开发一个基本的TodoList。

下面通过一个 TodoList 案例来展示 ArkTS 的核心语法。在鸿蒙中,我们推荐将数据逻辑封装在 class 中,而 UI 结构定义在 struct 内部。

为什么需要 @Observed@ObjectLink

在开发 TodoList 这种包含对象数组的列表时,简单的 @State 是不够的。

  1. @State 的局限: 它只能观察到变量本身的变化。如果你修改了数组中某个对象成员的内部属性(例如 task.isCompleted = true),由于数组的引用地址没变,UI 不会触发刷新。
  2. @Observed 装饰在类定义上,使类成员的属性变化可以被追踪。
  3. @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 转向声明式的数据驱动,能够显著减少在界面维护上的逻辑代码量。