【鸿蒙开发】第七章 ArkTS语言UI范式-基础语法

2023-12-23 18:00:31

1 前言

通过前面的章节,我们基本清楚鸿蒙应用开发用到的语言和项目基本结构,在【鸿蒙开发】第四章 Stage应用模型及项目结构也提到过ArkTS的UI范式的基本语法状态管理渲染控制等能力,简要介绍如下:

  1. 基本语法ArkTS定义了声明式UI描述自定义组件动态扩展UI元素的能力,再配合ArkUI开发框架中的系统组件及其相关的事件方法属性方法等共同构成了UI开发的主体。
  2. 状态管理ArkTS提供了多维度的状态管理机制。在UI开发框架中,与UI相关联的数据可以在组件内使用,也可以在不同组件层级间传递,比如父子组件之间、爷孙组件之间,还可以在应用全局范围内传递或跨设备传递。另外,从数据的传递形式来看,可分为只读的单向传递和可变更的双向传递。开发者可以灵活的利用这些能力来实现数据和UI的联动。
  3. 渲染控制ArkTS提供了渲染控制的能力。条件渲染可根据应用的不同状态,渲染对应状态下的UI内容。循环渲染可从数据源中迭代获取数据,并在每次迭代过程中创建相应的组件。数据懒加载从数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。

本章节我们先来展开学习一下基本语法,后续文章会介绍到状态管理渲染控制另外两方面的详细知识。

2 基本语法

我们先来看看前面章节也出现过的这张图片,下面是新建项目后,页面的示范代码。
在这里插入图片描述

  1. 装饰器: 用于装饰类、结构、方法以及变量,并赋予其特殊的含义。如上述示例中@Entry、@Component和@State都是装饰器,@Component表示自定义组件,@Entry表示该自定义组件为入口组件,@State表示组件中的状态变量,状态变量变化会触发UI刷新。
  2. UI描述:以声明式的方式来描述UI的结构,例如build()方法中的代码块。
  3. 自定义组件:可复用的UI单元,可组合其他组件,如上述被@Component装饰的struct Hello。
  4. 系统组件:ArkUI框架中默认内置的基础和容器组件,可直接被开发者调用,比如示例中的Column、Text、Divider、Button。
  5. 属性方法:组件可以通过链式调用配置多项属性,如fontSize()、width()、height()、backgroundColor()等。
  6. 事件方法:组件可以通过链式调用设置多个事件的响应逻辑,如跟随在Button后面的onClick()。
    系统组件、属性方法、事件方法具体使用可参考基于ArkTS的声明式开发范式。
  7. @Builder/@BuilderParam:特殊的封装UI描述的方法,细粒度的封装和复用UI描述。
  8. @Extend/@Style:扩展内置组件和封装属性样式,更灵活地组合内置组件。
  9. stateStyles:多态样式,可以依据组件的内部状态的不同,设置不同样式。

2.1 声明式UI

通过前面章节,我们知道ArkTS声明方式组合和扩展组件来描述应用程序的UI,同时还提供了基本的属性事件子组件配置方法,帮助开发者实现应用交互逻辑。

2.1.1 组件创建

根据组件构造方法的不同,创建组件包含有参数无参数两种方式。创建组件时不需要new运算符。

如果组件的接口定义没有包含必选构造参数,则组件后面的“()”不需要配置任何内容。如果组件的接口定义包含构造参数,则在组件后面的“()”配置相应参数。

// Image的参数为必选参数
Image('https://xyz/test.jpg')
// Text为可选参数,有无参数都可
Text($r('app.string.title_value'))
Text()

2.1.2 配置属性

属性方法“.”链式调用的方式配置系统组件的样式和其他属性,建议每个属性方法单独写一行。

属性方法中参数根据定义,传递常量参数变量参数枚举类型表达式

Text('hello')
  .width(this.count % 2 === 0 ? 100 : 200)    
  .height(this.offset + 100)
  .fontSize(20)
  .fontColor(Color.Red)
  .fontWeight(FontWeight.Bold)

2.1.3 配置事件

事件方法也以“.”链式调用的方式配置系统组件支持的事件,建议每个事件方法单独写一行。

Button('add counter')
  .onClick(() => {
    this.counter += 2;
  })

// 使用声明的箭头函数,可以直接调用,不需要bind this。
fn = () => {
  console.info(`counter: ${this.counter}`)
  this.counter++
}
...
Button('add counter')
  .onClick(this.fn)

2.1.4 配置子组件

如果组件支持子组件配置,则需在尾随闭包"{…}"中为组件添加子组件的UI描述。ColumnRowStackGridList等组件都是容器组件

Column() {
  Row() {
    Image('test1.jpg')
      .width(100)
      .height(100)
    Button('click +1')
      .onClick(() => {
        console.info('+1 clicked!');
      })
  }
}

2.2 自定义组件

自定义组件即开发者根据自己的业务要求定义组件,代码可复用性、业务逻辑与UI分离,后续版本演进等因素。因此,将UI和部分业务逻辑封装成自定义组件是不可或缺的能力。

自定义组件具有以下特点:

  1. 可组合:允许开发者组合使用系统组件、及其属性和方法。
  2. 可重用:自定义组件可以被其他组件重用,并作为不同的实例在不同的父组件或容器中使用。
  3. 数据驱动UI更新:通过状态变量的改变,来驱动UI的刷新。

下面我们以一个简单的自定义组件案例来学习:

@Entry
@Component
export struct HelloComponent {
  @State message: string = 'Hello, World!';

  build() {
    // HelloComponent自定义组件组合系统组件Row和Text
    Row() {
      Text(this.message)
        .onClick(() => {
          // 状态变量message的改变驱动UI刷新,UI从'Hello, World!'刷新为'Hello, ArkUI!'
          this.message = 'Hello, ArkUI!';
        })
    }
  }
}

@Entry
@Component
struct ParentComponent {

  build() {
    Column() {
      // 创建HelloComponent实例,并初始化成员变量message
      MyComponent({ message: 'Hello,World!My name is Yvan' })
    }
  }
}
  1. export:如果在另外的文件中引用该自定义组件,需要使用export关键字导出,并在使用的页面import该自定义组件。
  2. struct:自定义组件基于struct实现,struct + 自定义组件名 + {…}的组合构成自定义组件,不能有继承关系。对于struct的实例化,可以省略new。
  3. @Component:仅能装饰struct关键字声明的数据结构。struct被@Component装饰后具备组件化的能力,需要实现build方法描述UI,一个struct只能被一个@Component装饰。
  4. build():build()函数用于定义自定义组件的声明式UI描述,自定义组件必须定义build()函数。
  5. @Entry:@Entry装饰的自定义组件将作为UI页面的入口。在单个UI页面中,最多可以使用@Entry装饰一个自定义组件。@Entry可以接受一个可选的LocalStorage的参数。(从API version 10开始,@Entry可以接受一个可选的LocalStorage的参数或者一个可选的EntryOptions参数。)
名称类型必填说明
routeNamestring表示作为命名路由页面的名字。
storageLocalStorage页面级的UI状态存储。
@Entry({ routeName : 'myPage' })
@Component
struct MyComponent {
}

  1. @Reusable:@Reusable装饰的自定义组件具备可复用能力
@Reusable
@Component
struct MyComponent {
}

2.2.1 成员函数/变量

自定义组件中函数和变量,都具备以下约束:

  1. 不支持静态
  2. 访问私有化

而变量具体是否需要本地初始化,是否需要从父组件通过参数传递初始化子组件的成员变量,具体后续文章状态管理的内容会介绍。

2.2.2 build()函数

所有声明在build()函数的语言,我们统称为UI描述。
我们先来看看这个案例:

@Entry
@Component
struct MyComponent {
  @State count: number = 1;

  build() {
    // 1.根节点唯一且必要,必须为容器组件
    Row() {
      ChildComponent() 
    }
  	// 2.不允许声明本地变量
  	// let a: number = 1;
  	
  	// 3.不允许console.info
  	// console.info('print debug log');

  	// 4.不允许本地作用域
  	//{
    //	...
  	//}
  	
    // 5.不能调用没有用@Builder装饰的方法
    // this.doSomeCalculations();
    // 可以调用
    // this.doSomeRender();
    // 参数可以为调用TS方法的返回值
    // Text(this.calcTextValue())

    // 6.不允许使用switch语法
    //switch (expression) {
    //  case 1:
    //    Text('...')
    //    break;
    //  default:
    //    Text('...')
    //    break;
    //}
    
 	// 7.不允许使用表达式
    // (this.aVar > 10) ? Text('...') : Image('...')

    // 8. 应避免直接在Text组件内改变count的值
    // Text(`${this.count++}`)
  }
  
  doSomeCalculations() {
  }

  calcTextValue(): string {
    return 'Hello World';
  }

  @Builder doSomeRender() {
    Text(`Hello World`)
  }
  
}

@Component
struct ChildComponent {
  build() {
    // 根节点唯一且必要,可为非容器组件
    Image('test.jpg')
  }
}

UI描述需要遵循以下规则:

  1. @Entry装饰的自定义组件,其build()函数下的根节点唯一且必要,且必须为容器组件,其中ForEach禁止作为根节点。 @Component装饰的自定义组件,其build()函数下的根节点唯一且必要,可以为非容器组件,其中ForEach禁止作为根节点。
  2. 不允许声明本地变量
  3. 不允许在UI描述里直接使用console.info,但允许在方法或者函数里使用
  4. 不允许创建本地的作用域
  5. 不允许调用没有用@Builder装饰的方法,允许系统组件的参数是TS方法的返回值
  6. 不允许switch语法,如果需要使用条件判断,请使用if
  7. 不允许使用表达式
  8. 不允许直接改变状态变量

其中,第8点需要注意。在ArkUI状态管理中,状态驱动UI更新。所以,不能在自定义组件的build()@Builder方法里直接改变状态变量,这可能会造成循环渲染的风险。Text(‘${this.count++}’)全量更新最小化更新会产生不同的影响。
build函数中更改应用状态的行为可能会比上面的示例更加隐蔽,如:

  1. 在@Builder,@Extend或@Styles方法内改变状态变量 。
  2. 在计算参数时调用函数中改变应用状态变量,例如 Text(‘${this.calcLabel()}’)。
  3. 对当前数组做出修改,sort()改变了数组this.arr,随后的filter方法会返回一个新的数组。
// 反例
@State arr : Array<...> = [ ... ];
ForEach(this.arr.sort().filter(...), 
  item => { 
  ...
})
// 正确的执行方式为:filter返回一个新数组,后面的sort方法才不会改变原数组this.arr
ForEach(this.arr.filter(...).sort(), 
  item => { 
  ...
})

2.3 页面和自定义组件生命周期

在开始之前,我们先明确自定义组件页面的关系:

  1. 自定义组件@Component装饰的UI单元,可以组合多个系统组件实现UI的复用,可以调用组件的生命周期。

  2. 页面:即应用的UI页面。可以由一个或者多个自定义组件组成,@Entry装饰的自定义组件为页面的入口组件,即页面的根节点,一个页面有且仅能有一个@Entry。只有被@Entry装饰的组件才可以调用页面的生命周期。

页面生命周期,即被@Entry装饰的组件生命周期,提供以下生命周期接口:

onPageShow页面每次显示时触发一次,包括路由过程、应用进入前台等场景,仅@Entry装饰的自定义组件生效。
onPageHide页面每次隐藏时触发一次,包括路由过程、应用进入后台等场景,仅@Entry装饰的自定义组件生效。
onBackPress:当用户点击返回按钮时触发,仅@Entry装饰的自定义组件生效。

组件生命周期,即一般用@Component装饰的自定义组件的生命周期,提供以下生命周期接口:

aboutToAppear:组件即将出现时回调该接口,具体时机为在创建自定义组件的新实例后,在执行其build()函数之前执行
aboutToDisappear:aboutToDisappear函数在自定义组件析构销毁之前执行。不允许在aboutToDisappear函数中改变状态变量,特别是@Link变量的修改可能会导致应用程序行为不稳定。

生命周期流程如下图所示,下图展示的是被@Entry装饰的组件(首页)生命周期
在这里插入图片描述

2.3.1 自定义组件的创建和渲染流程

  1. 自定义组件的创建:自定义组件的实例由ArkUI框架创建。

  2. 初始化自定义组件的成员变量:通过本地默认值或者构造方法传递参数来初始化自定义组件的成员变量,初始化顺序为成员变量的定义顺序。

  3. 如果开发者定义了aboutToAppear,则执行aboutToAppear方法。

  4. 在首次渲染的时候,执行build方法渲染系统组件,如果子组件为自定义组件,则创建自定义组件的实例。在执行build()函数的过程中,框架会观察每个状态变量的读取状态,将保存两个map:

    1. 状态变量 -> UI组件(包括ForEach和if)
    2. UI组件 -> 此组件的更新函数。即一个lambda方法,作为build()函数的子集,创建对应的UI组件并执行其属性方法,示意如下。
build() {
  ...
  this.observeComponentCreation(() => {
    Button.create();
  })

  this.observeComponentCreation(() => {
    Text.create();
  })
  ...
}

当应用在后台再次启动时,此时应用进程并没有销毁,所以仅需要执行onPageShow

2.3.2 自定义组件重新渲染

当事件句柄被触发(比如设置了点击事件,即触发点击事件)改变了状态变量时,或者LocalStorage / AppStorage中的属性更改,并导致绑定的状态变量更改其值时:

  1. 框架观察到了变化,将启动重新渲染。

  2. 根据框架持有的两个map(自定义组件的创建和渲染流程
    中第4步),框架可以知道该状态变量管理了哪些UI组件,以及这些UI组件对应的更新函数。执行这些UI组件的更新函数,实现最小化更新。

2.3.3 自定义组件的删除

如果if组件的分支改变,或者ForEach循环渲染中数组的个数改变,组件将被删除:

  1. 在删除组件之前,将调用其aboutToDisappear生命周期函数,标记着该节点将要被销毁。ArkUI的节点删除机制是:后端节点直接从组件树上摘下,后端节点被销毁,对前端节点解引用,前端节点已经没有引用时,将被JS虚拟机垃圾回收。

  2. 自定义组件和它的变量将被删除,如果其有同步的变量,比如@Link、@Prop、@StorageLink,将从同步源上取消注册。

不建议在生命周期aboutToDisappear内使用async await,如果在生命周期的aboutToDisappear使用异步操作(Promise或者回调方法),自定义组件将被保留在Promise的闭包中,直到回调方法被执行完,这个行为阻止了自定义组件的垃圾回收。

以下示例展示了生命周期的调用时机:

// Index.ets
import router from '@ohos.router';

@Entry
@Component
struct MyComponent {
  @State showChild: boolean = true;
  @State btnColor:string = "#FF007DFF"

  // 只有被@Entry装饰的组件才可以调用页面的生命周期
  onPageShow() {
    console.info('Index onPageShow');
  }
  // 只有被@Entry装饰的组件才可以调用页面的生命周期
  onPageHide() {
    console.info('Index onPageHide');
  }

  // 只有被@Entry装饰的组件才可以调用页面的生命周期
  onBackPress() {
    console.info('Index onBackPress');
    this.btnColor ="#FFEE0606"
    return true // 返回true表示页面自己处理返回逻辑,不进行页面路由;返回false表示使用默认的路由返回逻辑,不设置返回值按照false处理
  }

  // 组件生命周期
  aboutToAppear() {
    console.info('MyComponent aboutToAppear');
  }

  // 组件生命周期
  aboutToDisappear() {
    console.info('MyComponent aboutToDisappear');
  }

  build() {
    Column() {
      // this.showChild为true,创建Child子组件,执行Child aboutToAppear
      if (this.showChild) {
        Child()
      }
      // this.showChild为false,删除Child子组件,执行Child aboutToDisappear
      Button('delete Child')
      .margin(20)
      .backgroundColor(this.btnColor)
      .onClick(() => {
        this.showChild = false;
      })
      // push到page页面,执行onPageHide
      Button('push to next page')
        .onClick(() => {
          router.pushUrl({ url: 'pages/page' });
        })
    }

  }
}

@Component
struct Child {
  @State title: string = 'Hello World';
  // 组件生命周期
  aboutToDisappear() {
    console.info('[lifeCycle] Child aboutToDisappear')
  }
  // 组件生命周期
  aboutToAppear() {
    console.info('[lifeCycle] Child aboutToAppear')
  }

  build() {
    Text(this.title).fontSize(50).margin(20).onClick(() => {
      this.title = 'Hello ArkUI';
    })
  }
}

// page.ets
@Entry
@Component
struct page {
  @State textColor: Color = Color.Black;
  @State num: number = 0

  onPageShow() {
    this.num = 5
  }

  onPageHide() {
    console.log("page onPageHide");
  }

  onBackPress() { // 不设置返回值按照false处理
    this.textColor = Color.Grey
    this.num = 0
  }

  aboutToAppear() {
    this.textColor = Color.Blue
  }

  build() {
    Column() {
      Text(`num 的值为:${this.num}`)
        .fontSize(30)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.textColor)
        .margin(20)
        .onClick(() => {
          this.num += 5
        })
    }
    .width('100%')
  }
}

以上示例中,Index页面包含两个自定义组件,一个是被@Entry装饰的MyComponent,也是页面的入口组件,即页面的根节点;一个是Child,是MyComponent的子组件。只有@Entry装饰的节点才可以使页面级别的生命周期方法生效,所以MyComponent中声明了当前Index页面的页面生命周期函数。MyComponent和其子组件Child也同时声明了组件的生命周期函数。

  1. 应用冷启动的初始化流程为:MyComponent aboutToAppear --> MyComponent build --> Child aboutToAppear --> Child build --> Child build执行完毕 --> MyComponent build执行完毕 --> Index onPageShow

  2. 点击“delete Child”,if绑定的this.showChild变成false,删除Child组件,会执行Child aboutToDisappear方法。

  3. 点击“push to next page”,调用router.pushUrl接口,跳转到另外一个页面,当前Index页面隐藏,执行页面生命周期Index onPageHide。此处调用的是router.pushUrl接口,Index页面被隐藏,并没有销毁,所以只调用onPageHide。跳转到新页面后,执行初始化新页面的生命周期的流程。

  4. 如果调用的是router.replaceUrl,则当前Index页面被销毁,执行的生命周期流程将变为:Index onPageHide --> MyComponent aboutToDisappear --> Child aboutToDisappear。上文已经提到,组件的销毁是从组件树上直接摘下子树,所以先调用父组件的aboutToDisappear,再调用子组件的aboutToDisappear,然后执行初始化新页面的生命周期流程。

  5. 点击返回按钮,触发页面生命周期Index onBackPress,且触发返回一个页面后会导致当前Index页面被销毁

  6. 最小化应用或者应用进入后台,触发Index onPageHide。当前Index页面没有被销毁,所以并不会执行组件的aboutToDisappear。应用回到前台,执行Index onPageShow

  7. 退出应用,执行Index onPageHide --> MyComponent aboutToDisappear --> Child aboutToDisappear

文章来源:https://blog.csdn.net/u010687761/article/details/135038425
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。