HarmonyOS 从完全不懂到 TODO LIST 开发

前段时间我有幸被掘金邀请参加了鸿蒙的培训。作为一个 Web 前端开发者,在参加培训前对鸿蒙一无所知。然而,经过培训后我意外地发现鸿蒙 Next 原生开发如此容易上手,对于前端开发者学习成本非常低,需要学习的新东西非常少,特写一篇速通文章分享我的所学。

本文将从一个前端开发者的角度出发,速通鸿蒙 Next 开发的方方面面,并最终开发一个 Todo List 小实例。

简介

首先,鸿蒙使用 ArkTS 作为原生开发语言。如果你熟悉 TypeScript 编程语言,那基本上可以无缝过渡,因为 ArkTS 并不是一门全新的语言,而是在 TypeScript 基础上进行扩展的。这意味着我们可以跳过学习新语言的阶段,只需学习鸿蒙的 UI 框架 ArkUI

类似于 React,ArkUI 框架采用声明式的开发范式来描述 UI,并且内置了完善的状态管理机制,无需像 React 那样面对各种混乱的第三方状态库。

鸿蒙内置组件的布局设计基于 CSS 的概念,包括 marginpaddingflexgrid、栅格系统和媒体查询等,这些都是前端开发者所熟悉的。

鸿蒙 Next 提供了完整的开发、调试、测试和发布一站式 IDE。如果你之前使用过 IntelliJ IDEA,那么对你来说将是无缝衔接的,因为鸿蒙的 DevEco 是基于 IntelliJ IDEA Community 进行深度定制的。但如果你跟我一样是 Neovim 或者 VSCode 的死忠粉的话,也没有关系 DevEco 其实是很好上手的。

IDE 介绍

工欲善其事,必先利其器,我们先看看鸿蒙的 IDE 怎么用

DevEco-Studio 安装

目前提供3个版本可供下载

  • Windows(64-bit)
  • Mac(X86)
  • Mac(ARM)

注意目前版本需要 Node 18.x环境,前端开发者建议安装 nvm,事先切换好 Node 版本。

安装好 nvm 后

plaintext

nvm install 18.14.1
nvm use 18.14.1
node -v

这样你在安装 DevEco 的时候就可以选择 Local 的 Node 环境了,如果你本地没有 Node,也可以选择全新安装

node version

接下来如果一切顺利,在 Diagnose 界面就会看到满屏的对号,表示安装成功了。

DevEco setup

我相信你能摸索着创建一个 Hello Wrold 项目

DevEco hello

接下来会看到树形列表中默认创建了大量的文件,跟我刚开始一样,不知如何下手,经过研究发现很多文件是有两份的,一个是模块级的,一个是整个应用级的。

为了清楚起见,我画了一张图,图中左侧列出的文件,都是应用和模块的两个版本的。

DevEco hello

整个 entry 目录叫做一个 Module,该目录会编译为一个 .hap 为后缀的文件,HAP 包。

等应用最终上架时,会打包成 .app 为扩展名的文件,上传到华为商店。

Module 不只有 entry 类型的,还有其他类型的,简单起见,我还是总结一张图:

DevEco hello

初看是有点头大,其实作为入门,只需要关注的 entry 模块,其他 Module 类型可等到深入开发时再去文档了解。

entry 目录下有一个 src/main/ets/pages/index.ets 文件,这是用户看到的第一个页面,我们先把该文件打开看个大概,后边编程语言的部分会详细介绍对应语法。

ts

@Entry
@Component
struct Index {
  @State message: string = 'Hello World';
  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
    }
    .height('100%')
  }
}
 

第一行的 @Entry 装饰器表明这是应用的入口,中间 build() 方法返回的就是整个声明式UI的页面结构。

翻译过来就是一个满高的 Row 组件之中放了一个满宽的 Column 组件,里边放了一个 50 大小文本组件。

右上角选中 previewer,点击 run 按钮即可运行,并预览效果。

run

输出效果

输出效果

这就是整个开发流程了,刚才的代码怎么看着这么眼熟,是 TypeScript ?

编程语言

鸿蒙的主要开发语言是 ArkTS(.ets 文件)。看着眼熟是因为它是 TypeScript 的超集,基本上就是 TS 代码,最大的区别或者说特点,就是在编译时对静态类型的检查和分析做了增强,并对一些动态特性做了少量的限制。这使得程序在运行时类型都是已知的,减少了运行时 bug 并提升了程序性能。

比如,强制类型不允许使用 anyunknown 类型。也不能使用 obj as any 之类的语法动态给对象添加属性和方法,更不能在运行时使用 delete 删除属性或方法。在某些需要 any 类型的情况下,建议使用 Record\<string, Object> 类型。

编译器默认打开了 TypeScript 的一些严格模式,比如 strictPropertyInitialization,要求强制给定初始值。strictNullChecks 强制进行空值安全检查等。

总之,你能想到的运行时的动态类型特性都会被限制,都尽量不要使用。更多详细的语法规则可以参考官方文档中关于《从 TypeScript 到 ArkTS 的适配规则》的文章,内容非常详细。实际上,在开发过程中,DevEco Studio 会提供很好的错误报告信息,因此你可以先不必了解这些语法规则,等到遇到问题时再去查询。

UI框架

ArkUI 框架是基于 ArkTS 的UI 框架,使用声明式的开发范式,数据驱动 UI 更新,并且提供了页面级路由导航等。

页面布局方面提供了多种媒体查询,比如

  • 设备类型
  • 窗口宽高监听
  • 折叠屏状态
  • 横竖屏查询

统一了单位,比如

  • vp 虚拟像素
  • fp 字体像素,用户端的设置会乘以系数 1fp = 1vp * scale

并且提供了多种栅格系统,窗口栅格会根据容器宽度,自动匹配栅格数量

栅格size宽度设备
4格small360~600手机竖屏
8格medium600~840手机横屏,pad 竖屏,折叠屏
12格large840~1440pad 横屏,2in1
12格x-large1440~全屏

需要注意的是,目前稳定主推的应用模型叫做Stage模型,如果你在学习的过程中看到FA模型,那就是就是旧版教程,可以不用看了。

光说不练是假把式,下边我们在代码层面了解一下如何声明 UI

声明式UI 描述

声明式 UI 的描述方式如下

js

Column() {
  Text('item 1')
  Divider()
  Text('item 2')
}

Column 是容器组件,所以后边带{},里边包含子组件,非容器组件则无需{}

给组件配置属性通常用链式调用的方法

js

Text("hello").fontSize(20).fontColor(Color.Red).fontWeight(FontWeight.Bold);

为了更加清晰,通常会写成这种格式

ts

Text("hello")
    .fontSize(20)
    .fontColor(Color.Red)
    .fontWeight(FontWeight.Bold);

添加事件处理

ts

Button("Click me").onClick(() => {
  this.myText = "ArkUI";
});

装饰器

ArkUI 中大量的使用的装饰器,包括我们之前看到的 @entry 表示入口,再看一个最简单的自定义组件:

ArkUI 中用 @Component 装饰的 struct 结构代表自定义组件。

ts

@Component
struct MyComponent {
  @State message: string = 'Hello, World!';
  build() {
    // 在 build 函数里返回 UI 描述
  }
}

目前流行数据驱动 UI 的编程范式,所谓 UI = f(State), 少不了状态,

ArkUI 中用 @State 装饰器来声明状态,Parent 可以直接覆盖 Child State

ts

@Component
struct Parent {
  build() {
    Column() {
      // 父组件覆盖State
      MyComponent({ message:"hi" })
    }
  }
}
 

和 React 中的 State 类似,状态的变化可以引起 UI 更新,但注意这个状态不是 immutable 的,UI 是否可以观察到状态的变化要看数据类型,具体要参考详细的文档。

下边是典型的事件处理中修改状态,引起 UI 刷新的例子:

ts

@Component
export struct HelloComponent {
  @State message: string = 'Hello, World!';
  build() {
    Row() {
      Text(this.message)
        .onClick(() => {
          // 基础类型的状态变量 message 改变驱动 UI 刷新,
          // UI从'Hello, World!'刷新为'Hello, ArkUI!'
          this.message = 'Hello, ArkUI!';
        })
    }
  }
}

@State 只是组件内部的状态。如果想在状态更改时引发 Child 组件的更新,就要了解 @Props 装饰器了

@Props 用于接收 Parent 组件的更新,给接收端属性前加上 @Props 装饰器后,就很类似 React 中的 props 传递了,不同的是 @Props 属性自身改变也会引起 UI 更新,类似 React 的 props 和 state 的结合体,看以下例子:

ts

@Component
export struct ChildComponent {
  @Prop message: string;
  build() {
    Row() {
      Text(this.message)
        .onClick(()=>{
          // 本身修改 Props 也会引起重绘
          this.message += " !"
        })
    }
  }
}

除了单向数据流,ArkUI 还提供了双向数据绑定的装饰器 @Link

跨层级共享数据的 @Provide@Consume 装饰器有些类似 react 中的 useContext

画了个图总结一下,可以根据你的需要用的时候再去查文档。

ArkUI State decrators

一个实例

今天做一个学习新语言最常见的简易 ToDo List,请看下边代码,包含了详细注释

ts

// 引入其他组件
import Item from '../ui/Item'
 
@Entry
@Component
struct Index {
  // 当前文本输入框输入的文字
  @State todo: string = '';
  // 整个列表的状态数据
  @State list: Array<string> = [
    "学习鸿蒙开发",
    "沟通外包",
    "看剧放松"
  ];
  build() {
      // Flex 布局,纵向
      Flex({ direction: FlexDirection.Column, }) {
 
        // 增加一个标题文本
        Text("待办事项")// .border({ width: 1 })
          .width("100%")
          .fontSize(28)
          .fontWeight(FontWeight.Bold)
          .textAlign(TextAlign.Start)
          .margin({ bottom: 20 })
 
        // 文本输入框,用于添加新待办事项
        TextInput({ text: this.todo, placeholder: "请输入" })
          .width("100%")
          .height(45)
          .enterKeyType(EnterKeyType.Done)
          .placeholderColor(Color.Gray)
          .margin({ bottom: 20 })
          .onChange((value) => {
            // 文字修改则更新状态,并绑定状态,使之变为受控组件
            this.todo = value;
          })
          .onSubmit((value) => {
            // 回车时触发,填进列表
            console.log(String(value))
            this.list.push(this.todo)
            this.todo = ''
          })
 
        // 列表组件, 内部要使用 ForEach 渲染 ListItem 项
        List({ space: 20 }) {
          ForEach(this.list, (item: string) => {
            ListItem() {
              // ListItem 中包装了自定义组件 Item,见 ui/Item.ets
              Item({ content: item })
            }.width('100%')
          }, (item: string, i) => i + item)
        }
        .flexGrow(1)
        // 列表拖拽默认会有弹性特效,取消掉
        .edgeEffect(EdgeEffect.None)
      }
      .padding(18)
      .width('100%')
      .height('100%')
      .backgroundColor('#dddddd')
    }
}

自定义列表项放入另一个目录 src/main/ets/ui/Item.ets

ts

@Component
export default struct Item {
  private content?: string;
  @State isComplete: boolean = false;
 
  @Builder
  labelIcon(icon: Resource) {
    Image(icon)
      .objectFit(ImageFit.Contain)
      .width(28)
      .height(28)
      .margin(20)
  }
 
  build() {
    Row() {
      // 条件渲染
      if (this.isComplete) {
        // 图标路径 entry/src/main/resources/base/media/ic_ok.png
        this.labelIcon($r('app.media.ic_ok'));
      } else {
        // 图标路径 entry/src/main/resources/base/media/ic_default.png
        this.labelIcon($r('app.media.ic_default'));
      }
      Text(this.content)
        .fontSize(20)
        .fontWeight(500)
        .opacity(this.isComplete ? 0.4 : 1.0)
        .decoration({ type: this.isComplete ? TextDecorationType.LineThrough : TextDecorationType.None })
    }
    .borderRadius(12)
    .backgroundColor("#FFFFFF")
    .width("100%")
    .height("64vp")
    .onClick(() => {
      this.isComplete = !this.isComplete;
    })
  }
}
 

在模拟器中的显示效果

结语

通过上边的学习,可以看出相比于安卓的苹果两大系统,鸿蒙的开发相对于前端开发者来说友好得多。

在如此低迷的经济环境下,衷心希望鸿蒙系统在市场上可以取得大成功,给我们前端开发仔开辟一条新的赛道。

最后,非常感谢掘金和华为此次的邀请,期待下次有机会再聚。

photo 1 photo 2

全文完,如有帮助请支持我