文章目录
- ArkTS
- ArkTS声明式开发范式的基本组成
- 基本语法
- 声明式UI
- 创建组件
- 配置属性
- 配置事件
- 配置子组件
- 自定义组件
- 基本结构
- 成员函数/变量
- build()函数
- 自定义组件通用样式
- 自定义组件的创建和渲染流程
- 自定义组件重新渲染
- 自定义组件的删除
- @Builder装饰器
- 全局自定义构建函数
- 组件内部的私有构建函数
- 参数传递规则--按值传递:
- 参数传递规则--按引用传递:
- @BuilderParam装饰器
- 初始化@BuilderParam
- 使用场景
- @Styles装饰器
- @Extend装饰器
- 多态样式stateStyles
TS是JS的超集,ArkTS则是TS的超集。
ArkTS
ArkTS是HarmonyOS优选的主力应用开发语言。ArkTS是在TS基础上做了拓展。
当前,ArkTS在TS的基础上主要扩展了如下能力:
- 基本语法:ArkTS定义了声明式UI描述、自定义组件和动态扩展UI元素的能力,再配合ArkUI开发框架中的系统组件及其相关的事件方法、属性方法等共同构成了UI开发的主体。
- 状态管理:ArkTS提供了多维度的状态管理机制。在UI开发框架中,与UI相关联的数据可以在组件内使用,也可以在不同组件层级间传递,比如父子组件之间、爷孙组件之间,还可以在应用全局范围内传递或跨设备传递。另外,从数据的传递形式来看,可分为只读的单向传递和可变更的双向传递。开发者可以灵活地利用这些能力来实现数据和UI的联动。
- 渲染控制:ArkTS提供了渲染控制的能力。条件渲染可根据应用的不同状态,渲染对应状态下的UI内容。循环渲染可从数据源中迭代获取数据,并在每次迭代过程中创建相应的组件。数据懒加载从数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。
未来,ArkTS会结合应用开发/运行的需求持续演进,逐步提供并行和并发能力增强、系统类型增强、分布式开发范式等更多特性。
ArkTS声明式开发范式的基本组成
-
装饰器
用来装饰类、结构体、方法以及变量,赋予其特殊的含义,如上述示例中 @Entry 、 @Component 、 @State 都是装饰器。具体而言, @Component 表示这是个自定义组件; @Entry 则表示这是个入口组件; @State 表示组件中的状态变量,此状态变化会引起 UI 变更。 -
自定义组件
可复用的 UI 单元,可组合其它组件,被 @Component 装饰的 struct。 -
UI 描述
声明式的方式来描述 UI 的结构, build() 方法内部的代码块。 -
内置组件
框架中默认内置的基础和布局组件,可直接被开发者调用,比如 Column、Text、Divider、Button。 -
事件方法
用于添加组件对事件的响应逻辑,统一通过事件方法进行设置,Button的onClick()。 -
属性方法
用于组件属性的配置,统一通过属性方法进行设置,如fontSize()、width()、height()、color() 等,可通过链式调用的方式设置多项属性。
除此之外,ArkTS扩展了多种语法范式来使开发更加便捷:
@Builder/@BuilderParam:特殊的封装UI描述的方法,细粒度的封装和复用UI描述。
@Extend/@Styles:扩展内置组件和封装属性样式,更灵活地组合内置组件。
stateStyles:多态样式,可以依据组件的内部状态的不同,设置不同样式。
基本语法
声明式UI
创建组件
Column() {// 有参数形式// string类型的参数Text('test')// $r形式引入应用资源,可应用于多语言场景Text($r('app.string.title_value'))// 无参数形式Text()Divider()
}
配置属性
Image('test.jpg').alt('error.jpg') .width(100) .height(100)Text('test').fontSize(12)
配置事件
// 使用箭头函数
Button('Click me').onClick(() => {this.myText = 'ArkUI';})// 使用匿名函数,要使用bind,以确保函数体中的this指向当前组件。
Button('add counter').onClick(function(){this.counter += 2;}.bind(this))// 使用组件的成员函数配置组件
myClickHandler(): void {this.counter += 2;
}Button('add counter').onClick(this.myClickHandler.bind(this))// 使用声明的箭头函数,可以直接调用,不需要bind this
fn = () => {console.info(`counter: ${this.counter}`)this.counter++
}Button('add counter').onClick(this.fn)
配置子组件
需要在"{…}"中为组件添加子组件
Column() {Row() {Image('test1.jpg').width(100).height(100)Button('click +1').onClick(() => {console.info('+1 clicked!');})}
}
自定义组件
基本结构
@Entry
@Component
struct MyComponent {build() {}
}
@Entry:@Entry装饰的自定义组件将作为UI页面的入口。在单个UI页面中,最多可以使用@Entry装饰一个自定义组件。@Entry可以接受一个可选的LocalStorage的参数。
@Component:@Component装饰器仅能装饰struct关键字声明的数据结构。struct被@Component装饰后具备组件化的能力,需要实现build方法描述UI,一个struct只能被一个@Component装饰。
struct:自定义组件基于struct实现,struct + 自定义组件名 + {…}的组合构成自定义组件,不能有继承关系。对于struct的实例化,可以省略new。
build()函数:build()函数用于定义自定义组件的声明式UI描述,自定义组件必须定义build()函数。
成员函数/变量
自定义组件除了必须要实现build()函数外,还可以实现其他成员函数,成员函数具有以下约束:
- 不支持静态函数。
- 成员函数的访问是私有的。
自定义组件可以包含成员变量,成员变量具有以下约束:
- 不支持静态成员变量。
- 所有成员变量都是私有的,变量的访问规则与成员函数的访问规则相同。
- 自定义组件的成员变量本地初始化有些是可选的,有些是必选的。具体是否需要本地初始化,是否需要从父组件通过参数传递初始化子组件的成员变量,参考状态管理。
build()函数
所有声明在build()函数的语言,我们统称为UI描述,UI描述需要遵循以下规则:
- @Entry装饰的自定义组件,其build()函数下的根节点唯一且必要,且必须为容器组件,其中ForEach禁止作为根节点。
@Component装饰的自定义组件,其build()函数下的根节点唯一且必要,可以为非容器组件,其中ForEach禁止作为根节点。
@Entry
@Component
struct MyComponent {build() {// 根节点唯一且必要,必须为容器组件Row() {ChildComponent() }}
}@Component
struct ChildComponent {build() {// 根节点唯一且必要,可为非容器组件Image('test.jpg')}
}
- 不允许声明本地变量
build() {// 反例:不允许声明本地变量let a: number = 1;
}
- 不允许在UI描述里直接使用console.info,但允许在方法或者函数里使用
build() {// 反例:不允许console.infoconsole.info('print debug log');
}
- 不允许创建本地的作用域
build() {// 反例:不允许本地作用域{...}
}
- 不允许调用没有用@Builder装饰的方法,允许系统组件的参数是TS方法的返回值。
@Component
struct ParentComponent {doSomeCalculations() {}calcTextValue(): string {return 'Hello World';}@Builder doSomeRender() {Text(`Hello World`)}build() {Column() {// 反例:不能调用没有用@Builder装饰的方法this.doSomeCalculations();// 正例:可以调用this.doSomeRender();// 正例:参数可以为调用TS方法的返回值Text(this.calcTextValue())}}
}
- 不允许switch语法,如果需要使用条件判断,请使用if。
build() {Column() {// 反例:不允许使用switch语法switch (expression) {case 1:Text('...')break;case 2:Image('...')break;default:Text('...')break;}}
}
- 不允许使用表达式
build() {Column() {// 反例:不允许使用表达式(this.aVar > 10) ? Text('...') : Image('...')}
}
自定义组件通用样式
@Component
struct MyComponent2 {build() {Button(`Hello World`)}
}@Entry
@Component
struct MyComponent {build() {Row() {MyComponent2().width(200).height(300).backgroundColor(Color.Red)}}
}
自定义组件的创建和渲染流程
- 自定义组件的创建:自定义组件的实例由ArkUI框架创建。
- 初始化自定义组件的成员变量:通过本地默认值或者构造方法传递参数来初始化自定义组件的成员变量,初始化顺序为成员变量的定义顺序。
- 如果开发者定义了aboutToAppear,则执行aboutToAppear方法。
- 在首次渲染的时候,执行build方法渲染系统组件,如果子组件为自定义组件,则创建自定义组件的实例。在执行build()函数的过程中,框架会观察每个状态变量的读取状态,将保存两个map:
- 状态变量 -> UI组件(包括ForEach和if)。
- UI组件 -> 此组件的更新函数,即一个lambda方法,作为build()函数的子集,创建对应的UI组件并执行其属性方法,示意如下。
build() {...this.observeComponentCreation(() => {Button.create();})this.observeComponentCreation(() => {Text.create();})...}
当应用在后台启动时,此时应用进程并没有销毁,所以仅需要执行onPageShow。
自定义组件重新渲染
当事件句柄被触发(比如设置了点击事件,即触发点击事件)改变了状态变量时,或者LocalStorage / AppStorage中的属性更改,并导致绑定的状态变量更改其值时:
- 框架观察到了变化,将启动重新渲染。
- 根据框架持有的两个map(自定义组件的创建和渲染流程中第4步),框架可以知道该状态变量管理了哪些UI组件,以及这些UI组件对应的更新函数。执行这些UI组件的更新函数,实现最小化更新。
自定义组件的删除
如果if组件的分支改变,或者ForEach循环渲染中数组的个数改变,组件将被删除:
- 在删除组件之前,将调用其aboutToDisappear生命周期函数,标记着该节点将要被销毁。ArkUI的节点删除机制是:后端节点直接从组件树上摘下,后端节点被销毁,对前端节点解引用,前端节点已经没有引用时,将被JS虚拟机垃圾回收。
- 自定义组件和它的变量将被删除,如果其有同步的变量,比如@Link、@Prop、@StorageLink,将从同步源上取消注册。
不建议在生命周期aboutToDisappear内使用async await,如果在生命周期的aboutToDisappear使用异步操作(Promise或者回调方法),自定义组件将被保留在Promise的闭包中,直到回调方法被执行完,这个行为阻止了自定义组件的垃圾回收。
@Builder装饰器
@Builder装饰器是个特殊的工具,以模块化和可复用的方式构建用户界面,
@Builder所装饰的函数遵循build()函数语法规则,将重复使用的UI元素抽象成一个方法,在build方法里调用。
举个简单的例子:
全局自定义构建函数
应用内可以直接调用
如果不涉及组件状态变化,建议使用全局的自定义构建方法。
知道是啥了那就整点别的
组件内部的私有构建函数
需要用
this
调用
参数传递规则–按值传递:
传递的参数为状态变量时,状态变量的改变不会引起@Builder方法内的UI刷新。所以当使用状态变量的时候,推荐使用按引用传递。
参数传递规则–按引用传递:
使用
$$
,传递的参数可以是状态变量,且状态变量的改变会引起@Builder方法内的UI刷新。
在按值传递的基础上修改一下就好了额, UI会刷新了
@BuilderParam装饰器
@BuilderParam 装饰器允许你将一个函数作为参数传递给一个自定义构建函数(@Builder 装饰的函数)
初始化@BuilderParam
- 使用所属自定义组件的自定义构建函数或者全局的自定义构建函数,在本地初始化@BuilderParam
@Builder function GlobalBuilder0() {}@Component
struct Child {@Builder doNothingBuilder() {};@BuilderParam aBuilder0: () => void = this.doNothingBuilder;@BuilderParam aBuilder1: () => void = GlobalBuilder0;build(){}
}
- 用父组件自定义构建函数初始化子组件@BuilderParam装饰的方法。
@Component
struct Child {@BuilderParam aBuilder0: () => void;build() {Column() {this.aBuilder0()}}
}@Entry
@Component
struct Parent {@Builder componentBuilder() {Text(`Parent builder `)}build() {Column() {Child({ aBuilder0: this.componentBuilder })}}
}
使用场景
- 参数初始化组件
@BuilderParam装饰的方法可以是有参数和无参数的两种形式,需与指向的@Builder方法类型匹配。@BuilderParam装饰的方法类型需要和@Builder方法类型一致
@Builder function GlobalBuilder1($$ : {label: string }) {Text($$.label).width(400).height(50).backgroundColor(Color.Green)
}@Component
struct Child {label: string = 'Child'// 无参数类,指向的componentBuilder也是无参数类型@BuilderParam aBuilder0: () => void;// 有参数类型,指向的GlobalBuilder1也是有参数类型的方法@BuilderParam aBuilder1: ($$ : { label : string}) => void;build() {Column() {this.aBuilder0()this.aBuilder1({label: 'global Builder label' } )}}
}@Entry
@Component
struct Parent {label: string = 'Parent'@Builder componentBuilder() {Text(`${this.label}`)}build() {Column() {this.componentBuilder()Child({ aBuilder0: this.componentBuilder, aBuilder1: GlobalBuilder1 })}}
}
- 尾随闭包初始化组件
// xxx.ets
@Component
struct CustomContainer {@Prop header: string;@BuilderParam closer: () => voidbuild() {Column() {Text(this.header).fontSize(30)this.closer()}}
}@Builder function specificParam(label1: string, label2: string) {Column() {Text(label1).fontSize(30)Text(label2).fontSize(30)}
}@Entry
@Component
struct CustomContainerUser {@State text: string = 'header';build() {Column() {// 创建CustomContainer,在创建CustomContainer时,通过其后紧跟一个大括号“{}”形成尾随闭包// 作为传递给子组件CustomContainer @BuilderParam closer: () => void的参数CustomContainer({ header: this.text }) {Column() {specificParam('testA', 'testB')}.backgroundColor(Color.Yellow).onClick(() => {this.text = 'changeHeader';})}}}
}
@Styles装饰器
简单说就是公共样式封装
- 当前@Styles仅支持通用属性(比如width、height等等)和通用事件(比如onClick、onTouch等等)。
- @Styles方法不支持参数
- @Styles可以定义在组件内或全局,在全局定义时需在方法名前面添加function关键字,组件内定义时则不需要添加function关键字。
- 定义在组件内的@Styles可以通过this访问组件的常量和状态变量,并可以在@Styles里通过事件来改变状态变量的值
- 组件内@Styles的优先级高于全局@Styles。优先找当前组件内的@Styles,如果找不到,则会全局查找。
// 定义在全局的@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)}}
}
@Extend装饰器
在@Styles的基础上,我们提供了@Extend,用于扩展原生组件样式。
语法: @Extend(UIComponentName) function functionName { … }
- @Extend仅支持定义在
全局
,不支持在组件内部定义。 - @Extend装饰的方法支持参数
- @Extend支持封装指定的组件的私有属性和私有事件和预定义相同组件的@Extend的方法。
- @Extend的参数可以为状态变量,当状态变量改变时,UI可以正常的被刷新渲染
- @Extend装饰的方法的参数可以为function,作为Event事件的句柄。
先玩一下样式继承。
再玩一下事件。
使用场景
以下示例声明了3个Text组件,每个Text组件均设置了fontStyle、fontWeight和backgroundColor样式。
@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%')}
}
多态样式stateStyles
@Styles和@Extend仅仅应用于静态页面的样式复用,stateStyles可以依据组件的内部状态的不同,快速设置不同样式。
stateStyles是属性方法,可以根据UI内部状态来设置样式,类似于css伪类,但语法不同。ArkUI提供以下四种状态:
- focused:获焦态。
- normal:正常态。
- pressed:按压态。
- disabled:不可用态。
可以与@Styles联合使用
@Entry
@Component
struct MyComponent {@Styles normalStyle() {.backgroundColor(Color.Gray)}@Styles pressedStyle() {.backgroundColor(Color.Red)}build() {Column() {Text('Text1').fontSize(50).fontColor(Color.White).stateStyles({normal: this.normalStyle,pressed: this.pressedStyle,})}}
}
stateStyles可以通过this绑定组件内的常规变量和状态变量
@Entry
@Component
struct CompWithInlineStateStyles {@State focusedColor: Color = Color.Red;normalColor: Color = Color.Greenbuild() {Column() {Button('clickMe').height(100).width(100).stateStyles({normal: {.backgroundColor(this.normalColor)},focused: {.backgroundColor(this.focusedColor)}}).onClick(() => {this.focusedColor = Color.Pink}).margin('30%')}}
}