前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >CPU都被干冒烟了,拥抱HarmonyOS第二天,自定义组件

CPU都被干冒烟了,拥抱HarmonyOS第二天,自定义组件

作者头像
用户6901603
发布2023-12-12 21:55:39
1850
发布2023-12-12 21:55:39
举报
文章被收录于专栏:不知非攻不知非攻

自从经历了第一天惨痛的踩坑经历之后,我机智的拉了一个 HarmonyOS app 学习讨论群。虽然目前只有寥寥数人,但个个都是大佬,有点什么问题随便探讨一下我就有灵感了,比我自己一个人蒙头研究确实进度快了许多

在这两天的学习过程中,我发现了一个巨诡异的事情,那就是明明第一次在文档中找到的内容,结果第二次想要通过搜索找到他,打死都找不到,这种情况出现了三次。arkUI 的文档是真神奇!

那种诡异感甚至差点让我以为自己穿越了...

第二天没有什么实际的产出,但是学习进展非常明显,直接把自定义组件相关的知识 ~

学!完!了!

我把主要学习精力放在了区别 arkUI 和 React/vue 的差异、以及在封装一些比较有难度的基础组件的实现方式上。

比如我想要封装一个体验良好的表单组件,那么我的学习思路是

  • T、学习思路
  • 1、复杂UI布局应该怎么做 ?
  • 2、基础的动画细节应该如何实现 ?
  • 3、数据管理应该怎么做 ?
  • 4、表单验证应该怎么做 ?
  • 5、数据量复杂的时候有没有可能存在性能问题,还没有验证过,若在实践中遇到问题,针对性的优化即可

如果目的一样,当我攻克了和 React 的差异,在 HarmonyOS 上基于 arkUI 实现一套大厂可商用的基础组件就比较简单了。

这意味着什么呢?

意味着我已经可以开始撸大型实战项目啦!剩下还有很多不熟悉的组件和系统能力,只需要在用到的时候,阅读文档拿来使用,在后续的开发过程中慢慢熟练即可。所以呢,有想要组建 HarmonyOS 开发团队的老板们可以开始联系我了,我给你们当技术指导,哈哈。

我今天学习的主要内容包括

系统组件生命周期

onAppear:组件挂载到组件树之后调用

onDisAppear:组件卸载时调用

页面组件生命周期

onPageShow:页面每次显示时触发

onPageHide:页面每次隐藏时触发

onBackPress:用户点击返回按钮时触发

自定义组件生命周期

aboutToAppear:组件即将出现时触发,在 build 之前

aboutToDisappear:组件即将销毁时触发

组件级别状态管理

@State

@Prop

@Link

@Provide/@Consume

@Observed

@ObjectLink

应用级别状态管理

@StorageProp

@StorageLink

@LocalStorageProp

@LocalStorageLink

其他状态管理

@Watch

$$ 运算符

T. 状态交互

1. 传参与接受参数

2. 子组件控制父组件

3. 父组件控制子组件

练习了大量的交互 Demo,并成功封装了两个可商用的基础组件

Icon

Lottie

字体图标组件

Lottie 动画组件

在 React 里,一个 state 一个 props 就完事了,万万没想到在 arkUI 里知识点这么多。在研究组件交互的过程中,我的 CPU 直接被干冒烟了,根据我的学习感受,这必定会成为一个比较难掌握的点,要吃透他不太容易

01

组件分类

在 arkUI 里,组件主要分为三个大类

  • 系统组件 主要是指系统原生提供的一系列组件,例如 Text、Button、CheckBox
  • 页面组件 被装饰器 @Entry 装饰的组件为页面组件,他表示一个页面的入口,该组件为页面的根节点
  • 自定义组件 我们可以通过装饰器 @Component 定义新的组件

02

组件基础语法

一个页面组件中最基础的写法如下

代码语言:javascript
复制
@Entry
@Component
struct MyComponent {
  build() {
    Text('hello world!')
  }
}

页面组件必须被 @Entry 装饰。build 方法中包含所有的页面元素。在上面这个例子中,Text 组件执行实际上是一个初始化的过程,但是由于关键字 struct 的作用,因此省略了 new.

系统提供的基础组件以 . 链式调用的方式设置当前组件的样式

例如,设置 Text 组件的字体大小

代码语言:javascript
复制
Text('test')
  .fontSize(12)

也可以同时配置多个属性

代码语言:javascript
复制
Image('test.jpg')
  .alt('error.jpg')    
  .width(100)    
  .height(100)

除了直接传递常量参数之外,还可以传递变量和表达式

代码语言:javascript
复制
Text('hello')
  .fontSize(this.size)
Image('test.jpg')
  .width(this.count % 2 === 0 ? 100 : 200)    
  .height(this.offset + 100)

arkUI 系统提供了一些全局的枚举类型,可以作为参数传递

代码语言:javascript
复制
Text('hello')
  .fontSize(20)
  .fontColor(Color.Red)
  .fontWeight(FontWeight.Bold)

如果某个元素想要设置事件监听,同样以 . 链式调用的方式使用

代码语言:javascript
复制
Button('Click me')
  .onClick(() => {
    this.myText = 'ArkUI';
  })

this

习惯了箭头函数和函数式组件的 React 开发者就要注意了,在面向对象的语境下,需要随时确保 this 的引用发生变化,因此如下两种情况都需要使用 bind 绑定 this

代码语言:javascript
复制
Button('add counter')
  .onClick(function(){
    this.counter += 2;
  }.bind(this))

这是个坑啊,折磨了多少开发者,他又来了!

代码语言:javascript
复制
myClickHandler(): void {
  this.counter += 2;
}
...
Button('add counter')
  .onClick(this.myClickHandler.bind(this))

组件嵌套

组件嵌套的方式,就是在父组件后面添加 {},所有的容器组件都支持嵌套子组件

代码语言:javascript
复制
Column() {
  Text('Hello')
    .fontSize(100)
  Divider()
  Text(this.myText)
    .fontSize(100)
    .fontColor(Color.Red)
}

@Builder

有的时候我们希望把一段组件的逻辑单独抽离出来,可以使用 @Builder 装饰器来声明一个自定义构建函数

代码语言:javascript
复制
@Entry
@Component
struct MyComponent {
  @Builder helloHarmonyOS() {
    Text('Hello HarmonyOS')
  }
  build() {
    Row() {
      Text('hello world!')
      this.helloHarmonyOS()
    }
  }
}

但是这个 @Builder 装饰器在传参数的时候,有特别的规则,我们后面在学习状态管理的时候一起分享

我们可以在一个组件里定义多个 @Builder 声明的函数,也可以在全局定义

代码语言:javascript
复制
@Builder 
function MyGlobalBuilderFunction() {
}

@Styles

我们可以使用 @Styles 装饰器来解决样式复用的问题。

代码语言:javascript
复制
@Entry
@Component
struct MyComponent {
  @Builder helloHarmonyOS() {
    Text('Hello HarmonyOS')
      .fancy()
  }
  @Styles fancy() {
    .width('100%')
    .height(40)
    .backgroundColor(Color.Pink)
  }
  
  build() {
    Row() {
      Text('hello world!')
        .fancy()
      this.helloHarmonyOS()
    }
  }
}

需要注意的是,@Styles 目前仅支持通用属性和通用事件。

具体属性有哪些可以去官方文档查看,或者根据 DevEco 的代码提示来编写

@Styles 也不支持传入参数

代码语言:javascript
复制
// bad
@Styles 
- function globalFancy (value: number) {
-  .width(value)
}

@Styles 可以定义在组件内,也可以定义在全局,在全局定义时需要在方法名之前添加 function 关键字

代码语言:javascript
复制
// 全局
@Styles function functionName() { ... }

// 在组件内
@Component
struct FancyUse {
  @Styles fancy() {
    .height(100)
  }
}

组件内 @Styles 的优先级高于全局 @Styles。 框架优先找当前组件内的 @Styles,如果找不到,则会全局查找。

代码语言:javascript
复制
// 定义在全局的@Styles封装的样式
@Styles function globalFancy  () {
  .width(150)
  .height(100)
  .backgroundColor(Color.Pink)
}

@Entry
@Component
struct FancyUse {
  @State heightValue: number = 100
  // 定义在组件内的@Styles封装的样式
  @Styles fancy() {
    .width(200)
    .height(this.heightValue)
    .backgroundColor(Color.Yellow)
    .onClick(() => {
      this.heightValue = 200
    })
  }

  build() {
    Column({ space: 10 }) {
      // 使用全局的@Styles封装的样式
      Text('FancyA')
        .globalFancy ()
        .fontSize(30)
      // 使用组件内的@Styles封装的样式
      Text('FancyB')
        .fancy()
        .fontSize(30)
    }
  }
}

很坑爹的是,目前 @Styles 还不支持跨文件引入,这里的全局只能是同一个文件的全局 ~

@Extend

我们可以使用 @Extend 扩展原生组件样式

代码语言:javascript
复制
@Extend(UIComponentName) 
function functionName {
  ...
}
  • @Extend 仅支持全局定义
  • @Extend 支持封装指定原生组件的私有属性和方法,以及相同指定组件的 @Extend 方法
代码语言:javascript
复制
// 支持Text的私有属性fontColor
@Extend(Text) function fancy () {
  .fontColor(Color.Red)
}
// superFancyText可以调用预定义的fancy
@Extend(Text) 
function superFancyText(size:number) {
  .fontSize(size)
  .fancy()
}
  • @Extend 支持参数传入
代码语言:javascript
复制
// xxx.ets
@Extend(Text) 
function fancy (fontSize: number) {
  .fontColor(Color.Red)
  .fontSize(fontSize)
}

@Entry
@Component
struct FancyUse {
  build() {
    Row({ space: 10 }) {
      Text('Fancy')
        .fancy(16)
      Text('Fancy')
        .fancy(24)
    }
  }
}

传入的参数可以是 function

代码语言:javascript
复制
@Extend(Text) 
function makeMeClick(onClick: () => void) {
  .backgroundColor(Color.Blue)
  .onClick(onClick)
}

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

  onClickHandler() {
    this.label = 'Hello ArkUI';
  }

  build() {
    Row({ space: 10 }) {
      Text(`${this.label}`)
        .makeMeClick(this.onClickHandler.bind(this))
    }
  }
}

也可以传入状态变量作为参数,当状态改变时,UI 可以正常刷新渲染

代码语言:javascript
复制
@Extend(Text) 
function fancy(fontSize: number) {
  .fontColor(Color.Red)
  .fontSize(fontSize)
}

@Entry
@Component
struct FancyUse {
  @State fontSizeValue: number = 20
  build() {
    Row({ space: 10 }) {
      Text('Fancy')
        .fancy(this.fontSizeValue)
        .onClick(() => {
          this.fontSizeValue = 30
        })
    }
  }
}

我们可以使用 @Extend 对下面的代码进行简化

代码语言:javascript
复制
@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 的样式

代码语言:javascript
复制
@Extend(Text) 
function fancy(w: number, color: Color) {
  .fontStyle(FontStyle.Italic)
  .fontWeight(w)
  .backgroundColor(color)
}

然后在组件中使用他

代码语言:javascript
复制
@Entry
@Component
struct FancyUse {
  @State label: string = 'Hello World'

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

stateStyles

类似于 css 的伪类,可以设置组件在不同状态时的样式,arkUI 提供了如下四种状态

  • normal 正常状态
  • focused 获得焦点
  • pressed 按下状态
  • disabled 禁用状态

使用方式如下

代码语言:javascript
复制
@Entry
@Component
struct StateStylesSample {
  build() {
    Column() {
      Button('Click me')
        .stateStyles({
          focused: {
            .backgroundColor(Color.Pink)
          },
          pressed: {
            .backgroundColor(Color.Black)
          },
          normal: {
            .backgroundColor(Color.Yellow)
          }
        })
    }.margin('30%')
  }
}

02

自定义组件

自定义组件是逻辑复用的重要手段。最基本结构如下

代码语言:javascript
复制
@Component
struct MyComponent {
  build() {
    Text('hello world!')
  }
}

组件封装好之后,使用时只能用如下方式传参

代码语言:javascript
复制
MyComponent({ name: 'world' })

传入的参数中,key 值 name 会覆盖在组件内部定义的同名属性

代码语言:javascript
复制
@Component
struct MyComponent {
  private name = 'china'
  build() {
    Text(`hello ${this.name}!`)
  }
}

自定义组件的导出和引用,与 TS 模块的语法是一致的,这里不在扩展冗余介绍

03

状态

和 React/Vue 一样,arkUI 也是基于数据驱动 UI 的核心思想来设计。不过 arkUI 中的数据状态非常不一样,它有更复杂的机制和逻辑

arkUI 中将会影响 UI 的数据称之为状态,他们常常需要特定的装饰器来声明

@State

先来实现一个经典的 count 案例

代码语言:javascript
复制
@Entry
@Component
struct MyComponent {
  @State
  private count: number = 0

  build() {
    Column() {
      Text(`hello ${this.count}!`)

      Button('++++')
      .onClick(() => this.count++)
    }
  }
}

@State 支持如下强类型的按值和按引用类型,及这些强类型构成的数组

  • class 、 Array<class>
  • number 、Array<number>
  • boolean 、Array<boolean>
  • string 、Array<string>
  • object 、Array<object>

不支持 any,不支持简单类型和复杂类型的联合类型,不允许使用 ?developer/article/2369955/undefined 和 null

建议不要装饰 Date 类型,应用可能会产生异常行为。不支持 Length、ResourceStr、ResourceColor 类型,Length、ResourceStr、ResourceColor 为简单类型和复杂类型的联合类型。

@State 装饰的属性只能在组件内部访问,子组件也不能访问

讲道理,规则有点多,用的时候再说吧,如果用错了,也会报提示,也不用刻意去记

这里需要特别注意的是,@State 只能观察监听到数据的浅层「第一层」。无法观测到更深层次的数据变化,因此层级结构复杂的数据类型的变化无法使用 @State 监听到完整的数据变化

嵌套类对象的属性变化需要使用 @Observed 与 @ObjectLink 来观测数据的变化,具体的使用我们后面介绍

@prop

如果我们将父组件中,@State 定义的状态传递给子组件,默认情况下,父组件只会将当前的值传递子组件用于初始化,后续父组件的变化则与子组件无关

例如我们定义这样一个子组件

代码语言:javascript
复制
@Component
struct ChildComponent {
  private count: number

  build() {
    Text(`Child Count: ${this.count}}`)
  }
}

然后再父组件中,将 @State count 传递给子组件

代码语言:javascript
复制
@Entry
@Component
struct MyComponent {
  @State
  private count: number = 0

  build() {
    Column() {
      Text(`hello world ${this.count}!`)
      ChildComponent({ count: this.count })
      Button('++++')
      .onClick(() => this.count++)

    }
  }
}

当 count 发生变化时,子组件不会跟着变化。如果我们想要子组件的状态与父组件建立绑定关系,则可以在子组件中,使用 @Prop 装饰 count,这样一个单向的绑定关系就建立成功了

  • 单向关系表现为:
  • 父组件中修改 count,子组件会同步更新
  • 子组件中修改 count,父组件不会有反应
  • 子组件更新后,父组件再更新,子组件中的状态会被父组件最新的值覆盖

因此,在子组件中,给 count 字段添加一个 @Prop 装饰即可

代码语言:javascript
复制
@Component
struct ChildComponent {
  @Prop
  private count: number

  build() {
    Text(`Child Count: ${this.count}}`)
  }
}

当作为子组件时,@Prop 可以被父组件中的其他任意装饰器状态初始化。

当作为父组件时,@Prop 可以初始化子组件的常规变量、@State、@Link、@Prop、Provide

@Prop 装饰的变量是私有的,只能在组件内部访问

@Link

如果你想要和子组件建立双向绑定的关系,则需要使用 @Link

  • 双向关系表现为:
  • 父组件中修改 count,子组件会同步更新
  • 子组件中修改 count,父组件会同步更新
  • 子组件不能初始化,只能接收父组件的参数初始化
  • 父组件必须以按引用传递的方式传参

子组件代码,使用 @Link 装饰状态

代码语言:javascript
复制
@Component
struct ChildComponent {
  @Link
  private count: number

  build() {
    Column() {
      Text(`Child Count: ${this.count}}`)
      Button('ChildCount')
        .onClick(() => this.count++)
    }
  }
}

父组件代码,按引用传参

代码语言:javascript
复制
@Entry
@Component
struct MyComponent {
  @State
  private count: number = 0

  build() {
    Column() {
      Text(`hello world ${this.count}!`)
      ChildComponent({ count: $count })
      Button('++++').onClick(() => this.count++)
    }
  }
}

其实学习到这里,我已经逐渐有点裂开了。这规则也太多了吧 ~ 别急,还有一点,@Link 只能与父组件的 @State Link StorageLink 建立双向绑定关系

@Provide 与 @Consume

类似于 React 中的 context,用于跨组件层级传递参数。其中 @Provide 作用于约定范围内的根节点,@Consume 作用于后代组件中,他们之间的关系是双向绑定

这两个知识点的使用反而简单,看如下案例即可

代码语言:javascript
复制
@Entry
@Component
struct MyComponent {
  @Provide
  private count: number = 0

  build() {
    Column() {
      Text(`hello world ${this.count}!`)
      ChildComponent({ count: $count })
      Button('++++')
        .onClick(() => this.count++)
    }
  }
}

@Component
struct ChildComponent {
  @Link
  private count: number

  build() {
    Column() {
      Text(`Child Count: ${this.count}}`)
      D()
    }
  }
}

@Component
struct D {
  @Consume
  private count: number

  build() {
    Button('我在深层子组件')
      .onClick(() => this.count++)
  }
}

@Observed 与 @ObjectLink

上文所述的装饰器仅能观察到第一层的变化,但是在实际应用开发中,应用会根据开发需要,封装自己的数据模型。对于多层嵌套的情况,比如二维数组,或者数组项class,或者class的属性是class,他们的第二层的属性变化是无法观察到的。这就引出了@Observed @ObjectLink装饰器

对他们使用主要步骤如下

  • 父组件中,使用 @Observed 装饰的 class 对象初始化 @State 变量
  • 子组件中,使用 @ObjectLink 接收父组件传递过来的参数

示例如下,首先使用 @Observed 定义复杂数据结构的对象

代码语言:javascript
复制
// objectLinkNestedObjects.ets
let NextID: number = 1;

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

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

@Observed
class ClassB {
  public a: ClassA;

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

然后在父组件中,使用刚才定义的复杂对象初始化 @State

代码语言:javascript
复制
@State b: ClassB = new ClassB(new ClassA(0));

然后在子组件中,使用 @ObjectLink 接收参数

代码语言:javascript
复制
@Component
struct ViewA {
  label: string = 'ViewA1';
  @ObjectLink a: ClassA;

  build() {
    Row() {
      Button(`ViewA [${this.label}] this.a.c=${this.a.c} +1`)
        .onClick(() => {
          this.a.c += 1;
        })
    }
  }
}

父组件完整代码如下,包括状态初始化,参数传递

代码语言:javascript
复制
@Entry
@Component
struct ViewB {
  @State b: ClassB = new ClassB(new ClassA(0));

  build() {
    Column() {
      ViewA({ label: 'ViewA #1', a: this.b.a })
      ViewA({ label: 'ViewA #2', a: this.b.a })

      Button(`ViewB: this.b.a.c+= 1`)
        .onClick(() => {
          this.b.a.c += 1;
        })
      Button(`ViewB: this.b.a = new ClassA(0)`)
        .onClick(() => {
          this.b.a = new ClassA(0);
        })
      Button(`ViewB: this.b = new ClassB(ClassA(0))`)
        .onClick(() => {
          this.b = new ClassB(new ClassA(0));
        })
    }
  }
}

04

总结

学习相关内容只用了一天,但是写这篇文章就用了三天时间 ~ ~ 因为官方文档的内容有点零散,关于自定义组件的内容分布在了几个不同的地方,因此为了确保每一个表达的准确性,反复翻阅文档和写代码验证花费了不少时间,真不是一个轻松的过程

不过写这篇文章本身也是一个总结的过程,让我对自定义组件的相关内容有了更深刻的理解。整体感受下来就是 arkUI 对于状态的区分更为细化,因此在实践中要结合具体情况选择合适的状态,就不得不对这些状态的基本情况有比较详细的了解

除了能够熟练使用之外,官方文档对于内部逻辑的运行机制都分别做了介绍,这可以作为一个进阶内容在后续的过程中学习,不过如果你理解 React 和 Vue 的底层原理的话,大概也能猜到他是如何实现的

虽然我学得挺快的,不过可以预想,对于零基础的同学来说,arkUI 的学习成本非常高,要掌握的概念和细节很多,写这篇文章我感觉自己都被干冒烟了,本来预计还有很大一部分内容要写,放到下一篇文章里来说吧

本文参与?腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-12-10,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 这波能反杀 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 01
  • 02
  • 02
  • 03
  • 04
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
http://www.vxiaotou.com