初识 HarmonyOS

437 阅读3分钟

本文使用「署名 4.0 国际 (CC BY 4.0)」 许可协议,欢迎转载、或重新修改使用,但需要注明来源。

概述

HarmonyOS 是一款面向全场景智慧生活方式的分布式操作系统。在传统的单设备系统能力的基础上,HarmonyOS 提出了基于同一套系统能力、适配多种终端形态的分布式理念,能够支持手机、平板、PC、智慧屏、智能穿戴、智能音箱、车机、耳机、AR/VR 眼镜等多种终端设备。

对消费者而言,HarmonyOS 能够将生活场景中的各类终端进行能力整合,形成“One Super Device”,实现不同终端设备之间的极速连接、能力互助、资源共享,匹配合适的设备、提供流畅的全场景体验。

一、HarmonyOS 应用模型概况

  • Stage模型:HarmonyOS 3.1 Developer Preview 版本开始新增的模型,是目前主推且会长期演进的模型。
  • FA(Feature Ability)模型:HarmonyOS 早期版本开始支持的模型,已经不再主推

Stage模型之所以成为主推模型,源于其设计思想。Stage 模型的设计基于如下出发点。

  1. 为复杂应用而设计

  2. 支持多设备和多窗口形态

  3. 应用组件管理和窗口管理在架构层面解耦

    • 在多设备(如桌面设备和移动设备)上,应用组件可使用同一套生命周期
  4. 平衡应用能力和系统管控成本

  5. Stage模型重新定义应用能力的边界,平衡应用能力和系统管控成本

Stage模型工程目录

应用配置文件(Stage模型) developer.huawei.com/consumer/cn…

AppScope
│
├── app.json5                  # 应用的全局配置信息配置应用全局描述信息,例如应用包名、版本号、应用图标、应用名称和依赖的SDK版本号等  
│
├── entry                      # HarmonyOS工程模块,编译构建生成一个HAP包
│
├── src
│   └── main
│       ├── ets                # 用于存放ArkTS源码
│       │   ├── entryability   # 应用/服务的入口
│       │   └── pages          # 应用/服务包含的页面
│       │
│       ├── resources          # 用于存放应用/服务所用到的资源文件,如图形、多媒体、字符串、布局文件等
│       │                      # 详见资源分类与访问 , 该目录由IDE自动生成,名称不可更改
│       └── module.json5       # Stage模型模块配置文件
│                              # 主要包含HAP包的配置信息、应用/服务在具体设备上的配置信息
│                              # 以及应用/服务的全局配置信息
│                              # 详见module.json5配置文件
│
├── build-profile.json5        # 当前的模块信息、编译信息配置项
│                              # 包括buildOption、targets配置等
│                              # 其中targets中可配置当前运行环境,默认为HarmonyOS
│
├── hvigorfile.ts              # 模块级编译构建任务脚本
│                              # 开发者可以自定义相关任务和代码实现
│
├── oh_modules                 # 用于存放三方库依赖信息
│                              # 关于原npm工程适配ohpm操作,请参考历史工程迁移
│
├── build-profile.json5        # 应用级配置信息,包括签名、产品配置等
│
└── hvigorfile.ts              # 应用级编译构建任务脚本

两个核心配置

  1. app.json5配置文件
  2. module.json5配置文件

FA模型

developer.huawei.com/consumer/cn…

二、初识ArkTS语言

ArkTS 是 HarmonyO S优选的主力应用开发语言。ArkTS 围绕应用开发在TypeScript(简称 TS)生态基础上做了进一步扩展,继承了 TS 的所有特性,是 TS 的超集。因此,在学习 ArkTS 语言之前,建议开发者具备TS语言开发能力。

基本语法

1. UI 描述

import router from '@ohos.router'
@Entry
@Component
struct Index {
  @State message: string = 'Hello World'

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
        Button(){
          Text('Next')
            .fontSize(30)
        }
        .type(ButtonType.Capsule)
        .margin(20)
        .backgroundColor('#0D9FFB')
        .width('40%')
        .height('5%')
        .onClick(() => {
          console.info(`Succeeded in clicking the 'Next' button.`)
          // 跳转到第二页
          router.pushUrl({ url: 'pages/Second' }).then(() => {
            console.info('Succeeded in jumping to the second page.')
          }).catch((err) => {
            console.error(`Failed to jump to the second page.Code is ${err.code}, message is ${err.message}`)
          })
        })
      }
      .width('100%')
    }
    .height('100%')
  }
}

2.渲染控制

  • if/else:条件渲染

  • ForEach:循环渲染

       ForEach(
         arr: Array,
         itemGenerator: (item: any, index: number) => void,
         keyGenerator?: (item: any, index: number) => string
       )

       ......
       @Entry
       @Component
       struct ArticleList {
         @State simpleList: Array<number> = [1, 2, 3, 4, 5];

         build() {
           Column() {
             ForEach(this.simpleList, (item: string) => {
               ArticleSkeletonView({ num: item })
                 .margin({ top: 20 })
             }, (item: string) => item)
           }
           .padding(20)
           .width('100%')
           .height('100%')
         }
       }

LazyForEach:数据懒加载,仅有 List、Grid、Swiper 以及 WaterFlow 组件支持数据懒加载

3.像素单位

developer.huawei.com/consumer/cn…

ArkTS 的基本组成

自定义组件结构

  1. @Entry:@Entry装饰的自定义组件将作为UI页面的入口。在单个UI页面中,最多可以使用@Entry装饰一个自定义组件。

  2. @Component:定义一个 struct 组件名,同是需要 build() 返回 UI 视图

  3. struct:自定义组件名、类名、函数名不能和系统组件名相同

  4. build()函数:build()函数用于定义自定义组件的声明式UI描述,自定义组件必须定义build()函数。

    • 不允许switch语法,如果需要使用条件判断,请使用if
    • 不允许使用表达式
// 不允许switch语法
build() {
  Column() {
    // 反例:不允许使用switch语法
    switch (expression) {
      case 1:
        Text('...')
        break;
      case 2:
        Image('...')
        break;
      default:
        Text('...')
        break;
    }
  }
}

// 不允许使用表达式
build() {
  Column() {
    // 反例:不允许使用表达式
    (this.aVar > 10) ? Text('...') : Image('...')
  }
}

生命周期

  1. 页面生命周期:即被 @Entry 装饰的组件
  • onPageShow:页面每次显示时触发一次,包括路由过程、应用进入前台等场景。
  • onPageHide:页面每次隐藏时触发一次,包括路由过程、应用进入后台等场景。
  • onBackPress:当用户点击返回按钮时触发。
  1. 组件的生命周期:即一般用 @Component 装饰的自定义组件的生命周期
  • aboutToAppear:组件即将出现时回调该接口,具体时机为在创建自定义组件的新实例后,在执行其 build() 函数之前执行。
  • aboutToDisappear:在自定义组件析构销毁之前执行。不允许在 aboutToDisappear 函数中改变状态变量,特别是 @Link 变量的修改可能会导致应用程序行为不稳定。

生命周期图

装饰器使用说明

@Builder 装饰器

简单来说就是定义一个函数

  1. 函数的定义

    • 组件内自定义构建函数
       @Builder MyBuilderFunction(){ ... }
      
       // 使用
       this.MyBuilderFunction()
      
    • 全局自定义构建函数
      @Builder function MyGlobalBuilderFunction(){ ... }
      
      // 使用
      MyGlobalBuilderFunction()
      
  2. 参数传递规则

    • 按引用传递参数
      @Builder function overBuilder($$: { paramA1: string }) {
        Row() {
          Text(`UseStateVarByReference: ${$$.paramA1} `)
        }
      }
      
      overBuilder( $$ : { paramA1: string, paramB1 : string } );    
      
    • 值传递
       @Builder function overBuilder(paramA1: string) {
         Row() {
           Text(`UseStateVarByValue: ${paramA1} `)
         }
       }
      
       overBuilder('overBuilder')
      

@Styles装饰器

定义组件重用样式

  1. 当前 @Styles 仅支持通用属性通用事件
  2. 不支持参数,反例如下。
// 反例: @Styles不支持参数
@Styles function globalFancy (value: number) {
  .width(value)
  .fontSize(50)
   .fontWeight(FontWeight.Bold) // 采用链式调用
}

@Extend 装饰器

定义扩展组件样式,用于扩展原生组件样式。

  1. 语法
@Extend(UIComponentName) function functionName { ... }
  1. 使用规则

    • 仅支持定义在全局,不支持在组件内部定义。只能在当前文件内使用,不支持 export。
    • 支持参数,参数可以为 function,作为 Event 事件的句柄。
      @Extend(Text) function makeMeClick(onClick: () => void) {
        .backgroundColor(Color.Blue)
        .onClick(onClick)
      }
      
      //
      Text('Hello World')
         .makeMeClick(this.onClickHandler.bind(this))
      
    • 参数可以为状态变量,当状态变量改变时,UI 可以正常的被刷新渲染。
  2. 使用场景

@Entry
@Component
struct FancyUse {
  @State label: string = 'Hello World'

  build() {
    Row({ space: 10 }) {
      Text(`${this.label}`)
        .fontStyle(FontStyle.Italic)
        .fontWeight(100)
        .backgroundColor(Color.Blue)
      Text(`${this.label}`)
        .fontStyle(FontStyle.Italic)
        .fontWeight(200)
        .backgroundColor(Color.Pink)
      Text(`${this.label}`)
        .fontStyle(FontStyle.Italic)
        .fontWeight(300)
        .backgroundColor(Color.Orange)
    }.margin('20%')
  }
}
@Extend(Text) function fancyText(weightValue: number, color: Color) {
  .fontStyle(FontStyle.Italic)
  .fontWeight(weightValue)
  .backgroundColor(color)
}

//
@Entry
@Component
struct FancyUse {
  @State label: string = 'Hello World'

  build() {
    Row({ space: 10 }) {
      Text(`${this.label}`)
        .fancyText(100, Color.Blue)
      Text(`${this.label}`)
        .fancyText(200, Color.Pink)
      Text(`${this.label}`)
        .fancyText(300, Color.Orange)
    }.margin('20%')
  }
}

三、资源分类与访问

应用开发过程中,经常需要用到颜色、字体、间距、图片等资源,在不同的设备或配置中,这些资源的值可能不同。

  1. 应用资源:借助资源文件能力,开发者在应用中自定义资源,自行管理这些资源在不同的设备或配置中的表现。
  2. 系统资源:开发者直接使用系统预置的资源定义(即分层参数,同一资源ID在设备类型、深浅色等不同配置下有不同的取值)。

资源分类

应用开发中使用的各类资源文件,需要放入特定子目录中存储管理。资源目录的示例如下所示,base 目录、限定词目录、rawfile 目录称为资源目录,element、media、profile 称为资源组目录。

资源目录示例:

resources
|---base // base目录是默认存在的目录
|   |---element // element用于存放字符串、颜色、布尔值等基础元素
|   |   |---string.json
|   |---media
|   |   |---icon.png
|   |---profile
|   |   |---test_profile.json
|---en_US  // 默认存在的目录,设备语言环境是美式英文时,优先匹配此目录下资源
|   |---element
|   |   |---string.json
|   |---media
|   |   |---icon.png
|   |---profile
|   |   |---test_profile.json
|---zh_CN  // 默认存在的目录,设备语言环境是简体中文时,优先匹配此目录下资源
|   |---element
|   |   |---string.json
|   |---media
|   |   |---icon.png
|   |---profile
|   |   |---test_profile.json
|---en_GB-vertical-car-mdpi // 自定义限定词目录示例,由开发者创建
|   |---element
|   |   |---string.json
|   |---media
|   |   |---icon.png
|   |---profile
|   |   |---test_profile.json
|---rawfile // 其他类型文件,原始文件形式保存,不会被集成到resources.index文件中。文件名可自定义。

资源目录

  1. base目录

base 目录是默认存在的目录,二级子目录element用于存放字符串、颜色、布尔值等基础元素,media、profile 存放媒体、动画、布局等资源文件。

目录中的资源文件会被编译成二进制文件,并赋予资源文件 ID。通过指定资源类型(type)和资源名称(name)引用。

  1. 限定词目录

限定词目录的命名要求 developer.huawei.com/consumer/cn…

en_US 和 zh_CN 是默认存在的两个限定词目录,其余限定词目录需要开发者根据开发需要自行创建。二级子目录 element、media、profile 用于存放字符串、颜色、布尔值等基础元素,以及媒体、动画、布局等资源文件。

同样,目录中的资源文件会被编译成二进制文件,并赋予资源文件ID。通过指定资源类型(type)和资源名称(name)来引用。

  1. rawfile目录(资源目录)
    • 支持创建多层子目录,目录名称可以自定义,文件夹内可以自由放置各类资源文件。
    • 目录中的资源文件会被直接打包进应用,不经过编译,也不会被赋予资源文件ID。通过指定文件路径和文件名引用。

资源访问

  1. 对于应用资源,在工程中,通过"$r('app.type.name')"形式引用。
Text($r('app.string.string_hello'))
  .fontColor($r('app.color.color_hello'))
  .fontSize($r('app.float.font_hello'))
  1. 对于 rawfile 目录资源,通过"$rawfile('filename')"形式引用。其中,filename 为rawfile目录下文件的相对路径,文件名需要包含后缀,路径开头不可以以"/"开头。
Image($rawfile('test.png')) 
  1. 系统资源
Text('Hello')
  .fontColor($r('sys.color.ohos_id_color_emphasize'))
  .fontSize($r('sys.float.ohos_id_text_size_headline1'))
  .fontFamily($r('sys.string.ohos_id_text_font_family_medium'))
  .backgroundColor($r('sys.color.ohos_id_color_palette_aux1'))

资源匹配

限定词目录 > base 目录。

rawfile是原始文件目录,不会根据设备状态去匹配不同的资源。

四、状态管理

组件状态

  1. @State:组件内状态。

        @Entry
        @Component
        struct MyComponent {
          @State count: number = 0;
    
          build() {
            Button(`click times: ${this.count}`)
              .onClick(() => {
                this.count += 1;
              })
          }
        }
    
  2. @Prop:父子单向同步:对父组件状态变量值的修改,将同步给子组件 @Prop 装饰的变量,子组件 @Prop 变量的修改不会同步到父组件的状态变量上。限制条件: @Prop 装饰器不能在 @Entry 装饰的自定义组件中使用。

        @Component
        struct CountDownComponent {
          @Prop count: number;
    
          build() {
            Column() {
                Text(`You have ${this.count} Nuggets left`)
            }
          }
        }
    
    
    
        @Entry
        @Component
        struct ParentComponent {
          @State countDownStartValue: number = 10;
    
          build() {
            Column() {
              Text(`Grant ${this.countDownStartValue} nuggets to play.`)
    
              Button(`+1 - Nuggets in New Game`).onClick(() => {
                this.countDownStartValue += 1;
              })
              CountDownComponent({ count: this.countDownStartValue })
            }
          }
        }
    
  3. @Link:父子双向同步

    • 不支持any,不支持简单类型和复杂类型的联合类型,不允许使用undefined和null
    • 禁止本地初始化
@Component
struct YellowButton {
  @Link yellowButtonState: number;

  build() {
    Button(`Yellow Button:${this.yellowButtonState}`)
      .width(this.yellowButtonState)
      .height(40)
      .backgroundColor('#f7ce00')
      .fontColor('#FFFFFF,90%')
      .onClick(() => {
        // 子组件的简单类型可以同步回父组件
        this.yellowButtonState += 100.0;
      })
  }
}



@Entry
@Component
struct ShufflingContainer {
  @State yellowButtonProp: number = 180;

  build() {
    Column() {
      Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center }) {
        // 简单类型从父组件@State向子组件@Link数据同步
        Button(`Parent View: Set yellowButto: ${this.yellowButtonProp}`)
          .width(312)
          .height(40)
          .margin(12)
          .fontColor('#FFFFFF,90%')
          .onClick(() => {
            this.yellowButtonProp = (this.yellowButtonProp < 300) ? this.yellowButtonProp + 100 : 100;
          })
        // 简单类型初始化@Link
        YellowButton({ yellowButtonState: $yellowButtonProp }).margin(12)
      }
    }
  }
}
  1. @Provide和@Consume: 后代组件双向同步

    • 实现跨层级传递

    • @Provide和@Consume可以通过相同的变量名或者相同的变量别名绑定,变量类型必须相同

      // 通过相同的变量名绑定
      @Provide a: number = 0;
      @Consume a: number;
      
      // 通过相同的变量别名绑定
      @Provide('a') b: number = 0;
      @Consume('a') c: number;
      
    • 初始值:@Provide,必须指定。@Consume:禁止本地初始化

          @Component
          struct CompC {
            // @Consume装饰的变量通过相同的属性名绑定其祖先组件CompA内的@Provide装饰的变量
            @Consume reviewNum: number;
            build() {
              Column() {
                Text(`reviewNum(${this.reviewNum})`)
                Button(`reviewNum(${this.reviewNum}), give +1`)
                  .onClick(() => this.reviewNum += 1)
              }
              .width('50%')
            }
          }
      
          @Component
          struct CompB {
            build() {
              CompC()
            }
          }
      
          @Entry
          @Component
          struct CompA {
            // @Provide装饰的变量reviewVotes由入口组件CompA提供其后代组件
            @Provide reviewNum: number = 0;
            @Provide reviewAGE: number = 0;
      
      
            build() {
              Column() {
                Button(`reviewNum(${this.reviewNum}${this.reviewAGE}), give +1`)
                  .onClick(() => this.reviewNum += 1)
                CompB()
              }
            }
          }
      
  2. @Observed装饰器和@ObjectLink装饰器: 嵌套类对象属性变化(比如二维数组,或者数组项 class,或者 class 的属性是 class)

@ObjectLink 和 @Observed 类装饰器用于在涉及嵌套对象或数组的场景中进行双向数据同步

@ObjectLink

  • 不能在 @Entry 装饰的自定义组件中使用
  • 不支持简单类型
  • 不允许初始值
  • 不允许直接修改值
// 允许@ObjectLink装饰的数据属性赋值
this.objLink.a= ...
// 不允许@ObjectLink装饰的数据自身赋值
this.objLink= ...

@Observed

装饰的类,比如 class、Objec t或者数组,也需要被 @Observed 装饰,否则将观察不到其属性的变化。

class ClassA {
  public c: number;

  constructor(c: number) {
    this.c = c;
  }
}

@Observed
class ClassB {
  public a: ClassA;
  public b: number;

  constructor(a: ClassA, b: number) {
    this.a = a;
    this.b = b;
  }
}

以上示例中,ClassB 被 @Observed 装饰,其成员变量的赋值的变化是可以被观察到的,但对于ClassA,没有被 @Observed 装饰,其属性的修改不能被观察到

@ObjectLink b: ClassB

// 赋值变化可以被观察到
this.b.a = new ClassA(5)
this.b.b = 5

// ClassA没有被@Observed装饰,其属性的变化观察不到
this.b.a.c = 5

使用场景,对象数组

import { Driver } from '@ohos.UiTest';
let NextID: number = 1;

@Observed
class ClassA {
  public id: number;
  public c: number;

  constructor(c: number) {
    this.id = NextID++;
    this.c = c;
  }
}

@Component
struct ViewA {
  // 子组件ViewA的@ObjectLink的类型是ClassA
  @ObjectLink a: ClassA;
  label: string = 'ViewA1';

  build() {
    Row() {
      Button(`ViewA [${this.label}] this.a.c = ${this.a ? this.a.c : "undefined"}`)
        .width(320)
        .margin(10)
        .onClick(() => {
          this.a.c += 1;
        })
    }
  }
}

@Entry
@Component
struct ViewB {
  // ViewB中有@State装饰的ClassA[]
  @State arrA: ClassA[] = [new ClassA(0), new ClassA(1)];

  build() {
    Column() {
      ForEach(this.arrA,
        (item: ClassA) => {
          ViewA({ label: `#${item.id}`, a: item })
        },
        (item: ClassA): string => item.id.toString()
      )
      Text()
        .border({ width: 1, color: 'red' })
        .height(1)
        .margin(40)
        .width('80%')
      Button(`ViewB: reset array`)
        .width(320)
        .margin(10)
        .onClick(() => {
          this.arrA = [new ClassA(0), new ClassA(1)];
        })
      Button(`ViewB: push`)
        .width(320)
        .margin(10)
        .onClick(() => {
          const len = this.arrA.length;
          this.arrA.push(new ClassA(this.arrA[len - 1].c + 1))
        })
    }
  }
}

管理应用拥有的状态

developer.huawei.com/consumer/cn…

  • LocalStorage

    • 页面级UI状态存储,通常用于 UIAbility 内、页面间的状态共享。
    • LocalStorage 是页面级的 UI 状态存储,通过 @Entry 装饰器接收的参数可以在页面内共享同一个 LocalStorage 实例。
  • AppStorage

    • 特殊的单例 LocalStorage 对象
    • 应用全局的UI状态存储,是和应用的进程绑定的,由UI框架在应用程序启动时创建,为应用程序UI状态属性提供中央存储
  • PersistentStorage

    • 持久化存储UI状态,通常和 AppStorage 配合使用,选择 AppStorage 存储的数据写入磁盘,以确保这些属性在应用程序重新启动时的值与应用程序关闭时的值相同;
  • Environment

    • 应用程序运行的设备的环境参数,环境参数会同步到 AppStorage 中,可以和 AppStorage 搭配使用。
    • 开发者如果需要应用程序运行的设备的环境参数,以此来作出不同的场景判断,比如多语言,暗黑模式等,需要用到 Environment 设备环境查询。

其他状态管理

  1. @Watch装饰器: 状态变量更改通知

    1. 可监听所有装饰器装饰的状态变量。不允许监听常规变量
    2. 不建议在 @Watc h函数中调用 async await,异步行为可能会导致重新渲染速度的性能问题。
    3. 在第一次初始化的时候, @Watch 装饰的方法不会被调用。
    @Component
    struct TotalView {
      @Prop @Watch('onCountUpdated') count: number;
      @State total: number = 0;
      // @Watch 回调
      onCountUpdated(propName: string): void {
        this.total += this.count;
      }
    
      build() {
        Text(`Total: ${this.total}`)
      }
    }
    
  2. $$ 语法:内置组件双向同步

  • 支持基础类型变量,以及 @State、@Link 和 @Prop 装饰的变量。
  • 仅支持 Refresh 组件的refreshing参数。
  • ($$)绑定的变量变化时,会触发 UI 的同步刷新。

五、进阶路径

developer.huawei.com/consumer/cn…