[写在前面: 文章多处用到gif动图,如未自动播放,请点击图片]
1. 初识 ArkTS 语言
ArkTS 是 HarmonyOS 优选主力开发语言。ArkTS 是基于 TypeScript (TS) 扩展的一门语言,继承了 TS 的所有特性,是TS的超集。
主要是扩展了以下几个方面:
-
声明式UI描述和自定义组件:
- ArkTS使用声明式的方式描述用户界面(UI),布局更直观易懂。
- 可以自定义组件
-
多维度的状态管理机制:
ArkTS可以使用@State、@Prop 等装饰器以及 LocalStorage 等管理应用的状态。
主要表现在:
- 支持在组件内使用与UI相关联的数据,实现局部状态管理。
- 支持在不同组件层级间传递数据,包括父子组件和爷孙组件之间的传递。
- 全局状态管理允许在应用的整个范围内传递数据,也可以跨设备传递。
- 提供只读的单向数据传递和可变更的双向数据传递。
-
渲染控制的能力:
ArkTS提供了灵活的渲染控制功能,使开发者能够根据应用的状态动态渲染UI内容。
主要表现在:
- 条件渲染(if-else)允许根据不同的应用状态选择性地渲染UI内容。
- 循环渲染支持从数据源中迭代获取数据,并在每次迭代中创建相应的组件。
- 数据懒加载允许按需迭代数据,并在每次迭代中创建相应的组件,提高应用性能。
2. 基本语法
2.1 ArkTS的基本组成
概念 | 描述 |
---|---|
装饰器 | 用于装饰类、结构、方法以及变量,赋予其特殊含义。例如,@Component 表示自定义组件,@Entry 表示入口组件,@State 表示状态变量。状态变量的变化会触发UI刷新。 |
UI描述 | 使用声明式的方式描述UI的结构,通常体现在 build() 方法中的代码块。 |
自定义组件 | 可复用的UI单元,可组合其他组件。被 @Component 装饰的 struct xxx 就是一个自定义组件。 |
系统组件 | ArkUI框架中默认内置的基础和容器组件,可以直接调用。 常见系统组件:Column 、Text 、Divider 、Button 。 |
属性方法 | 组件可以通过链式调用配置多项属性的方法。例如,fontSize() 、width() 、height() 、backgroundColor() 可以用于设置组件的样式和尺寸等属性。 |
事件方法 | 组件可以通过链式调用设置多个事件的响应逻辑。例如,在 Button 后面的 onClick() 就是一个事件方法,用于处理按钮被点击时的逻辑。 |
经典案例代码示例:
ts
// 1. 按引用传递参数时,传递的参数可为状态变量,且状态变量的改变会引起@Builder方法内的UI刷新
@Builder function MessageBuilder($$:{message:string}) { // 自定义装饰器
Row(){
Text(`Message is ${$$.message}`)
}
}
// 2. 按值传递
@Builder function ValueBuilder(message : string) {
Row() {
Text(`message is ${message}`)
}
}
@Entry // 装饰器
@Component // 装饰器
struct Index { // 使用 struct 关键字定义组件
@State message: string = 'hello' // @State表示组件中的状态变量,状态变量变化会触发UI刷新
@State count: number = 1
build() {
Row() { // 行
Column() { // 列
Text(this.message + this.count)
.fontSize(50) // 属性方法
.fontWeight(FontWeight.Bold)
Button('点我试试')
.onClick(() => {
this.count ++
this.message = '你好'
})
.fontColor('#000')
MessageBuilder({message: this.message}) // 传递参数的引用
ValueBuilder(this.message) // 按值传递,无响应式
}
.width('100%')
}
.height('100%')
}
}
效果如下:
2.2 声明式 UI
ArkTS采用声明方式组合和扩展组件来描述应用程序的UI,同时提供了基本的属性、事件和子组件配置方法
1. 创建组件:
-
无参数创建:对于没有必选构造参数的组件,可以直接在组件后面使用空括号,例如
Divider()
。 -
有参数创建:如果组件包含构造参数,可以在括号内配置相应参数,例如
Text('你好')
。tsColumn() { Text('item 1') Divider() Text('item 2') }
2. 配置属性:
-
使用属性方法通过"."链式调用配置系统组件的样式和其他属性,建议每个属性方法单独写一行,如设置字体大小、宽度、高度等。
tsText('test') .fontSize(12)
3. 配置事件:
-
通过事件方法以"."链式调用配置系统组件支持的事件,可使用lambda表达式、匿名函数表达式或组件的成员函数来定义
tsButton('Click me') .onClick(() => { this.myText = 'ArkUI'; })
2.3 自定义组件
在ArkUI中,UI显示的内容由组件构成,其中框架直接提供的组件称为系统组件 ,而开发者定义的组件则被称为自定义组件。
自定义组件具有以下特点:
-
可组合: 允许开发者组合使用系统组件及其属性和方法。
-
可重用: 自定义组件可以被其他组件重用,作为不同实例在不同的父组件或容器中使用。
-
数据驱动UI更新: 通过状态变量的改变来驱动UI的刷新。
自定义组件的结构:
概念 | 描述 |
---|---|
struct | 基于 struct 实现自定义组件,结构为 struct + 自定义组件名 + {...} 。实例化时可以省略 new 。 |
@Component | 装饰器,仅能装饰使用 struct 关键字声明的数据结构。被装饰后的结构具备组件化能力,需要实现 build 方法描述UI。一个 struct 只能被一个 @Component 装饰。 |
build() 函数 | 用于定义自定义组件的声明式 UI 描述。自定义组件必须实现 build() 函数。 |
@Entry | 装饰的自定义组件将作为UI页面的入口。在单个UI页面中,最多可以使用一个 @Entry 装饰的自定义组件。可以接受一个可选的 LocalStorage 参数。 |
以下示例展示了自定义组件GreetingComponent
的基本用法:
ts
@Component
struct GreetingComponent {
@State greeting: string = 'Hello, World!';
build() {
// GreetingComponent自定义组件组合系统组件Row和Text
Row() {
Text(this.greeting)
.onClick(() => {
// 状态变量greeting的改变驱动UI刷新
this.greeting = 'Hello, ArkUI!';
});
}
}
}
GreetingComponent
可以在其他自定义组件的 build()
函数中多次创建,实现自定义组件的重用。
ts
@Entry
@Component
struct ParentComponent {
build() {
Column() {
Text('ArkUI Greetings');
GreetingComponent({ greeting: 'Hello, World!' });
Divider();
GreetingComponent({ greeting: '你好!' });
}
}
}
成员函数/变量
自定义组件除了必须要实现build()函数外,还可以实现其他成员函数,成员函数具有以下约束:
- 不支持静态函数。
- 成员函数的访问始终是私有的。
自定义组件可以包含成员变量,成员变量具有以下约束:
- 不支持静态成员变量。
- 所有成员变量都是私有的,变量的访问规则与成员函数的访问规则相同。
- 自定义组件的成员变量本地初始化有些是可选的,有些是必选的。具体是否需要本地初始化,是否需要从父组件通过参数传递初始化子组件的成员变量,请参考官方文档:状态管理。
build()函数
@Entry装饰的自定义组件,必须要有且仅有一个 build() 函数,且必须为容器组件,其中ForEach禁止作为根节点,他会产生多节点。
@Component装饰的自定义组件,其build()函数下的根节点唯一且必要,可以为非容器组件,其中ForEach禁止作为根节点。
代码示例:
ts
@Entry
@Component
struct MyComponent {
build() {
// 根节点唯一且必要,必须为容器组件
Row() {
ChildComponent()
}
}
}
@Component
struct ChildComponent {
build() {
// 根节点唯一且必要,可为非容器组件
Image('test.jpg')
}
}
在 build()
函数内,不允许以下几点:
ts
build() {
// 反例:不允许声明本地变量
let a: number = 1;
// 反例:不允许console.info
console.info('print debug log');
// 反例:不允许本地作用域
{
...
}
// 不允许switch语法,如果需要使用条件判断,请使用if。反例如下。
switch (expression) {
case 1:
Text('...')
break;
case 2:
Image('...')
break;
default:
Text('...')
break;
}
// 不允许使用表达式,反例如下。
(this.aVar > 10) ? Text('...') : Image('...')
}
另外:
不允许调用除了被@Builder装饰以外的方法,允许系统组件
的参数是TS方法的返回值
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())
}
}
}
2.4 自定义组件通用样式
自定义组件通过"."
链式调用的形式设置通用样式。
ts
@Component
struct MyComponent2 {
build() {
Button(`Hello World`)
}
}
@Entry
@Component
struct MyComponent {
build() {
Row() {
MyComponent2() // 为自定义组件添加通用样式
.width(200)
.height(300)
.backgroundColor(Color.Red)
}
}
}
2.5. 页面和自定义组件生命周期
可以由一个或者多个自定义组件组成,@Entry装饰的自定义组件为页面的入口组件,即页面的根节点,一个页面有且仅能有一个@Entry。只有被@Entry装饰的组件才可以调用页面的生命周期。
图示:
生命周期接口 | 描述 |
---|---|
onPageShow | 每次页面显示时触发。 |
onPageHide | 每次页面隐藏时触发一次。 |
onBackPress | 当用户点击返回按钮时触发。 |
aboutToAppear | 组件即将出现时 回调,具体时机为在创建新实例后 ,在执行 build() 函数之前执行。 |
aboutToDisappear | 在自定义组件即将销毁时执行。 |
示例代码:
ts
// 包含两个自定义组件,一个是被@Entry装饰的MyComponent,也是页面的入口组件,即页面的根节点;一个是Child,是MyComponent的子组件。
// 只有@Entry装饰的节点才可以生效页面的生命周期方法,所以MyComponent中声明了当前Index页面的页面生命周期函数。MyComponent和其子组件Child也同时也声明了组件的生命周期函数。
import router from '@ohos.router';
@Entry
@Component
struct MyComponent {
@State showChild: boolean = true;
// 只有被@Entry装饰的组件才可以调用页面的生命周期
onPageShow() {
console.info('Index onPageShow');
}
// 只有被@Entry装饰的组件才可以调用页面的生命周期
onPageHide() {
console.info('Index onPageHide');
}
// 只有被@Entry装饰的组件才可以调用页面的生命周期
onBackPress() {
console.info('Index onBackPress');
}
// 组件生命周期
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('create or delete Child').onClick(() => {
this.showChild = false;
})
// push到Page2页面,执行onPageHide
Button('push to next page')
.onClick(() => {
router.pushUrl({ url: 'pages/Page2' });
})
}
}
}
@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).onClick(() => {
this.title = 'Hello ArkUI';
})
}
}
2.6. @Builder装饰器:自定义构建函数
定义的语法:
ts
@Builder MyBuilderFunction({ ... })
使用方法:
ts
this.MyBuilderFunction({ ... })
- 允许在自定义组件内定义一个或多个自定义构建函数,该函数被认为是该组件的私有、特殊类型的成员函数。
- 自定义构建函数可以在所属组件的build方法和其他自定义构建函数中调用,但不允许在组件外调用。
- 在自定义函数体中,this指代当前所属组件,组件的状态变量可以在自定义构建函数内访问。建议通过this访问自定义组件的状态变量而不是参数传递。
示例如下:
ts
@Builder function GlobalBuilder() {
Text('我是全局装饰器')
}
@Component
struct Children1 { // 子组件
@Builder DoNothing(){}
@BuilderParam aBuilder: () => void = this.DoNothing; // 定义局部的装饰器
@BuilderParam bBuilder: () => void = GlobalBuilder; // 定义全局的装饰器
build(){
Column() {
GlobalBuilder()
}
}
}
@Component
struct Children2 {
@BuilderParam paramsBuilder: () => void; // 声明装饰器
build(){
Column() {
this.paramsBuilder()
}
}
}
@Component
@Entry
struct Index {
@Builder componentBuilder() {
Text('我是父组件的 builder')
}
build() {
Column() {
Children1()
Children2({ paramsBuilder: this.componentBuilder}) // 传入一个 builder 给子组件
}.width('100%')
}
}
尾随闭包初始化组件
在初始化自定义组件时,紧跟一个大括号"{}"形成尾随闭包场景,将尾随闭包内的内容看做@Builder装饰的函数传给@BuilderParam
ts
// 尾随闭包初始化组件
@Component
struct CustomContainer {
@Prop header: string;
@BuilderParam closer: () => void // 准备接受闭包参数
build() {
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 Index {
@State text: string = 'header';
build() {
Column() {
// 在创建CustomContainer时,通过其后紧跟一个大括号"{}"形成尾随闭包
// 用内部的 this.text 作为参数
CustomContainer({ header: this.text }) {
Column() {
specificParam('testA', 'testB')
}.backgroundColor(Color.Yellow)
.onClick(() => {
this.text = 'changeHeader';
})
}
}
}
}
效果如下:
2.7 @Styles装饰器:定义组件重用样式
如果每个组件的样式都需要单独设置,在开发过程中会出现大量代码在进行重复样式设置,虽然可以复制粘贴,但为了代码简洁性和后续方便维护,可以使用公共样式装饰器@Styles。
使用方法:
ts
// 反例: @Styles不支持参数
// @Styles function globalFancy (value: number) {
// .width(value)
// }
// @Styles可以定义在组件内或全局,在全局定义时需在方法名前面添加function关键字,
// 组件内定义时则不需要添加function关键字。
// 定义在组件内的@Styles可以通过this访问组件的常量和状态变量,
// 并可以在@Styles里通过事件来改变状态变量的值
// 组件内@Styles的优先级高于全局@Styles。
@Component
@Entry
struct Index {
@State heightValue: number = 100
@Styles fancy() {
.height(this.heightValue)
.backgroundColor(Color.Yellow)
.onClick(() => {
this.heightValue = 200
})
}
build() {
Column(){
Row(){
Text('自定义样式,点我也能改变样式')
.fancy()
}
Divider()
Row(){
Text('点我也一样,也会跟着改变样式')
.fancy()
}
}
}
}
2.8 @Extend装饰器:定义扩展组件样式
使用规则
- 和@Styles不同,@Extend仅支持定义在全局,不支持在组件内部定义。
- 和@Styles不同,@Extend支持封装指定的组件的私有属性和私有事件,也可以预定义相同组件的@Extend的方法
ts
// 和@Styles不同,@Extend仅支持定义在全局,不支持在组件内部定义。
// 和@Styles不同,@Extend支持封装指定的组件的私有属性和私有事件和预定义相同组件的@Extend的方法。
// 和@Styles不同,@Extend装饰的方法支持参数,开发者可以在调用时传递参数,调用遵循TS方法传值调用。
// @Extend的参数可以为状态变量,当状态变量改变时,UI可以正常的被刷新渲染
@Extend(Text) function fancy (fontSize: number) { // 只给 Text 继承了 fancy
.fontColor(Color.Red)
.fontSize(fontSize)
}
// @Extend装饰的方法的参数可以为function,作为Event事件的句柄。
@Extend(Text) function makeMeClick(onClick: () => void) {
.backgroundColor(Color.Blue)
.onClick(onClick)
}
@Entry
@Component
struct Index {
@State label: string = 'Hello World';
@State fontSizeValue: number = 58;
onClickHandler() {
this.label = 'Hello ArkUI';
this.fontSizeValue = 108
}
build(){
Column(){
Row({space: 10}) {
Text('测试')
.fancy(this.fontSizeValue) // 传入可响应数据,后续函数执行,字号也会发生变化
// Span('span无效')
// .fancy() // Property 'fancy' does not exist on type 'SpanAttribute'. <tsCheck>
}
Row({ space: 10 }) {
Text(`${this.label}`)
.makeMeClick(this.onClickHandler.bind(this)) // bind 绑定当前作用域
.fancy(109)
}
}
}
}
2.9 stateStyles:多态样式
ts
// 多态样式
// stateStyles是属性方法,可以根据UI内部状态来设置样式,类似于css伪类,但语法不同。ArkUI提供以下四种状态:
//
// 1. focused:获焦态。
// 2. normal:正常态。
// 3. pressed:按压态。
// 4. disabled:不可用态。
// 基础场景
// 下面的示例展示了stateStyles最基本的使用场景。Button处于第一个组件,默认获焦,
// 生效focused指定的粉色样式。按压时显示为pressed态指定的黑色。
// 如果在Button前再放一个组件,使其不处于获焦态,就会生效normal态的黄色。
// Styles也可和stateStyles联合使用
@Entry
@Component
struct Index {
@Styles normalStyle() {
.backgroundColor(Color.Gray)
}
@Styles pressedStyle() {
.backgroundColor(Color.Red)
}
build() {
Column() {
Button('Click me')
.stateStyles({
focused: {
.backgroundColor(Color.Pink)
},
pressed: {
.backgroundColor(Color.Black)
},
normal: {
.backgroundColor(Color.Yellow)
}
})
Column() {
Text('Text1')
.fontSize(50)
.fontColor(Color.White)
.stateStyles({
normal: this.normalStyle,
pressed: this.pressedStyle,
})
}
}.margin('30%')
}
}