赞
踩
本文是结合HarmonyOS3.1/4.0的开发文档,结合一些代码示例而成的,如有不妥,请告知。
ArkTs是HarmonyOS主推的应用开发语言。ArkTs是基于Typescript进一步扩展出来的产物,继承了Typescript的所有特性,是typescript的超集。
不过,ArkTs还扩展了如下的能力:
因为ArkTs是基于Typescript扩展的,所以语法基本跟Typescript一致的,需要先熟悉一下Typescript,再来学习ArkTs,如果没有了解过Typescript的同学,可以去Typescript的官网看看。
了解ArkTs的基本语法,我们将从下面一段代码去分析一下
@Entry // 装饰器 @Component // 装饰器 // struct Index:定义自定义组件 struct Index { // @State 装饰器 @State message: string = 'Hello World' // 整个build 方法里的内容属于UI描述,Row、Column等属于内置组件 build() { Row() { Column() { Text(this.message) // 事件方法 .onClick(() => { console.log('---'); }) // 下面两个是属性方法 .fontSize(50) .fontWeight(FontWeight.Bold) } .width('100%') // DemoComponent() } .height('100%') } }
具体描述:
其它更多详细的系统组件、属性方法等介绍,可以参考 ArkTs的声明式开发范式
Harmony主推的ArkTs,为了帮助开发者实现应用交互逻辑,以声明方式组合和扩展组件来描述应用程序的UI,同时提供了可链式调用的基本属性、事件方法等,极大程度地帮助开发者提高开发效率。
根据ArkTs提供的组件构造方法不同,有两种方式创建组件:有参和无参
注意:因为struct 关键字的原因,创建组件的时候不需要通过new运算符去创建
当组件的接口定义了无参构造函数,则组件后面的"()"不需要传递任何参数,内置组件Divider就是如此。
Row() {
Button("确定").onClick(() => {
this.btnText = "删除";
console.log("======", this.btnText);
}).width(120)
.height(40)
.backgroundColor(Color.White)
.fontColor("#ff2064")
// 分割线
Divider()
// 文本显示
Text(this.btnText)
}
当组件的接口定义了有参构造函数(参数分必填和可选),则在组件后面的"()"传递对应的参数。
button组件源码中构造参数定义,如下图:
// 无参形式
Button()
.width(120)
.height(40)
// 有参形式:string类型参数以及非必填参数options
Button("确定", { type: ButtonType.Capsule })
.width(120)
.height(40)
.backgroundColor(Color.Orange)
.fontColor("#fff")
// $r形式引入应用资源,可用于多语言环境 title_value为自定义属性,其配置如下图
Button($r('app.string.title_value'))
.width(120)
.height(40)
// 网络图片在真机或虚拟机上需要配置网络请求权限才可以
Image("https://test/test.png")
组件属性方法以"."链式调用方式进行配置内置组件的样式和其他属性
Button("按钮")
.fontColor("#fff")
Button("确定", { type: ButtonType.Capsule })
.width(120)
.height(this.height ? this.height : 20)
.fontColor("#fff")
Button("确定", { type: ButtonType.Capsule })
Text("Hello ArkTs")
.fontColor(Color.Orange)
.fontWeight(FontWeight.Bold)
事件方法跟其他属性一样,也是以"."链式调用的方式使用,下面介绍一下几种事件方法。
Button($r('app.string.title_value'))
.onClick(() => {
console.log("这是一个箭头函数");
})
Button($r('app.string.title_value'))
.onClick(function(){
this.btnText = "测试";
console.log("===this====", this.btnText); // 输出:测试
}.bind(this))
// 定义成员函数跟定义变量同级
handleClick(): number {
return 100;
}
...
Button($r('app.string.title_value'))
.onClick(this.handleClick.bind(this))
struct DemoComponent {
@State btnText: string = 'Hello World'
handleFn = (): number => {
return 100;
}
build() {
Row() {
Button($r('app.string.title_value'))
.onClick(this.handleFn)
}
}
}
组件如果支持子组件配置,则需在闭包"{…}"中为组件添加子组件。
Row() {
Text("这是一行文本!")
Divider()
Button("删除按钮")
}
Row() { Column() { Row() { Text("这是一行文本!") .fontSize(14) .fontColor(Color.Red) } Row() { Button("删除") .onClick(() => { console.log("删除操作"); }) } .justifyContent(FlexAlign.End) } }
要实现一个复杂的功能,基本都不会在同一个组件中实现整个业务逻辑,这个时候需要对整个功能页面进行拆解成不同的小块,这时候自定义组件就派上用场了。
自定义组件具有以下特点:
说明:如果该组件需要给到其它组件调用,则需要使用关键字export导出,使用需要使用关键字import导入
@Component export struct Child { @State count: number = 0; build() { Row() { Text(`the count is ${this.count}`) Button("点击+1") .onClick(() => { this.count += 1; }) } .width("100%") .height("100%") } }
在其它组件中使用child
import { Child } from './child' @Component struct Father { build() { Column() { Text("这是父组件!") // 在子组件定义的count变量会编程可选参数 Child({ count: 20 }) Divider() // 多次重复使用 Child() } } }
上面简单讲了自定义组件的基本使用,这里我们将对自定义组件做进一步的了解。
自定义组件的基本结构
注意:自定义组件组件名、类名、函数名都能和内置组件名相同。
说明:从API 9 开始,@Component装饰器支持在ArkTs卡片中使用。
@Component
struct Demo1 {
build() {
...
}
}
说明:该装饰器也是从API 9 开始支持在ArkTs 卡片中使用
成员函数和变量
自定义组件除了必须实现的build函数,还有其他一些可选成员函数,比如onPageShow、aboutToDisappear等,具体可自查其源码。成员函数具有以下约束:
自定义组件可以有成员变量,但是成员变量具有以下约束:
所有声明在build函数的语言,在ArkTs中统称为UI描述,而UI描述有着相对严格的规则约束。
import { DemoComponent } from './demo/Demo1' @Entry @Component struct Index { @State message: string = 'Hello World' build() { // 根节点是容器组件且唯一的 Row() { DemoComponent() Text("测试") } .height('100%') } } @Component export struct Child { build() { Text("这是子组件") } }
...
build() {
let a: string = '错误示例';
}
@Component
export struct Child {
build() {
console.log("打印信息"); // 报错提示: console.log' does not comply with the UI component syntax. <etsLint>
Text("这是子组件")
.onClick(() => {
console.log("正确使用");
})
}
}
...
build() {
{
...
}
}
@Component export struct Child { @State count: number = 0; @Builder doSomethings() { Text("111111") } getTextVal(): string { return 'value'; } hasNothing() { } build() { // 错误示例 this.hasNothing() // 正确使用 this.doSomethings() Text(this.getTextVal()) } }
@Component export struct Child { @State count: number = 0; build() { // 错误示例 switch(count) { case 1: Text("1") break; case 2: Button("2") break; } // 错误示例 this.count > 10 ? Text("111") : Button("222") // 正确示例 if (this.count == 0) { Text("正确操作") } else { Image("https://这是一张图片") } } }
ArkTs的生命周期,分为页面和自定义组件的生命周期,有相同之处也有区别的地方
二者的关系:
下面我们简单了解一下页面生命周期:
组件的生命周期函数声明在源码的common.d.ts文件下,可以自行去查看
// Index.ets import { DemoComponent } from './demo/Demo1' @Entry @Component struct Index { // 该钩子函数触发了 onPageShow() { console.log("=====Index pageShow====="); } build() { Row() { DemoComponent() } .height('100%') } } // DemoComponent.ets @Component export struct DemoComponent { @State btnText: string = 'Hello World' // 该钩子函数没有触发 onPageShow() { console.log("======demo1 pageShow======"); } build() { Row() { Button("按钮") .width(120) .height(40) } } }
这里使用IDE的预览模拟了一下,生效
组件生命周期,即一般使用@Component装饰的自定义组件的生命周期,如下:
上述生命周期流程如下所示,展示的是被@Entry装饰的组件或者页面生命周期。
根据上面的流程图,从自定义组件的初始创建、重新渲染和删除来简单解释一下。
自定义组件的创建和渲染流程
build() {
...
this.observeComponentCreation(() => {
Button.create();
})
this.observeComponentCreation(() => {
Text.create();
})
...
}
当应用在后台启动时,此时应用并没有被销毁,所以仅需要执行onPageShow钩子函数。
自定义组件重新渲染
当组件中的状态变量发生改变的时,或者localStorage/AppStorage中的属性发生变更而导致绑定的状态变量改变时:
自定义组件的删除
如果if组件的分支发生改变或者ForEach循环渲染中数组的个数发生改变,组件将被删除
自定义组件的生命周期函数中还有几个不算太常用(个人意见,勿喷)的
笔者目前也还没搞清楚onLayout和onMeasure两个的具体使用,社区上有人说是跟安卓的layout类似,笔者不会安卓,安卓同学方便可以告知一下。
下面是简单的示例:
// 官方示例 // 定义一个CustomLayout组件 @Component export struct CustomLayout { @BuilderParam builder: () => void // onLayout(children: Array<LayoutChild>, constraint: ConstraintSizeOptions) { let pos = 0; children.forEach((child) => { child.layout({ position: { x: pos, y: pos }, constraint: constraint }) child.borderInfo.borderWidth = { top: 2, left: 2, right: 2, bottom: 2 } pos += 100; }) } onMeasure(children: Array<LayoutChild>, constraint: ConstraintSizeOptions) { let size = 100; children.forEach((child) => { child.measure({ minHeight: size, minWidth: size, maxWidth: size, maxHeight: size }) size += 50; }) } build() { this.builder() } } // DemoIndex.ets 中使用 import { CustomLayout } from './CustomLayout' @Entry @Component struct DemoIndex { build() { Row() { Column() { Button("按钮") .width("100%") .height(50) .backgroundColor(Color.Blue) CustomLayout() { ForEach([1,2,3], index => { Text("ArkTs " + index) .fontSize(30) .fontColor(Color.Red) .backgroundColor(Color.Black) }) } } } .height("100%") .width("100%") } }
预览效果图:看结构剖析,显示内容跟结构节点(虚拟框框)不在同一位置
前面我们了解了自定义组件的创建及其基本结构的理解。除了一些固定的结构之外,ArkUI还提供了一种更轻量的UI元素复用机制@Builder,被@Builder修饰的函数遵循build()函数语法规则,我们可以把一些重复的UI结构抽象到一个方法,方便在build()函数中调用。
@Builder的简单使用,如下:
语法:
@Builder myFnc(){...}
@Component
struct DemoBuilder {
// 自定义构建函数
@Builder showRepeatCode() {
Text("重复UI调用")
.fontColor(Color.Red)
.fontSize(12)
}
build() {
this.showRepeatCode()
}
}
语法:
@Builder
function myGlobalBuilder(){...}
下面用一个简单的例子说明一下:
// 全局定义自定义构建函数 builder.ets @Builder export function myGlobalBuilder() { Column(){ Text("全局Builder") .fontColor(Color.Blue) .fontSize(30) } .height(100) .width(300) .border({ width: 2, color: Color.Black }) } // 调用组件 DemoBuilder.ets import { myGlobalBuilder } from '../../utils/builder' @Component export struct DemoBuilder { // 自定义构建函数 @Builder showRepeatCode() { Text("重复UI调用") .fontColor(Color.Red) .fontSize(30) } build() { Column() { this.showRepeatCode() myGlobalBuilder() // 全局自定义构建函数 } } }
由上述例子我们可以得出以下两点:
参数传递规则
自定义构建函数的参数传递有两种:按值传递和按引用传递,均需要遵守以下规则:
1. 按引用传递参数
按引用类型传递参数时,传递参数可以是状态变量,变量值的变化也会引起@Builder方法内的UI更新。ArkUI提供$$作为按引用传递参数的范式。
语法方式:
BuilderDemo($$: {param1: string, param2: string});
示例1:官方范式
@Builder function ABuilder($$: { paramA1: string }) { Row() { Text(`UseStateVarByReference: ${$$.paramA1} `) } } @Entry @Component struct Parent { @State label: string = 'Hello'; build() { Column() { // 在Parent组件中调用ABuilder的时候,将this.label引用传递给ABuilder ABuilder({ paramA1: this.label }) Button('Click me').onClick(() => { // 点击“Click me”后,UI从“Hello”刷新为“ArkUI” this.label = 'ArkUI'; }) } } }
示例2:自定义类型
interface ItemList { id: number, name: string, value?: string } @Builder function ItemCard(item: ItemList) { Row() { Text(item.name) .fontColor(Color.White) .width(120) if (item.value) { Text(item.value) .fontColor(Color.Blue) .fontSize(30) } } .justifyContent(FlexAlign.SpaceBetween) } @Component export struct ListBuilder { @State list: Array<ItemList> = [ { id: 1, name: '测试1', value: 'hello' }, { id: 2, name: '发送接口' }, { id: 5, name: '数据库的费' } ] build() { Column() { List({ space: 20 }) { ForEach( this.list, item => { ListItem() { ItemCard(item) } .width("100%") .height(60) .padding(10) .backgroundColor("#ff6602") } ) } Button("添加一行") .onClick(() => { const item: ItemList = this.list[this.list.length - 1]; this.list.push({ id: item.id + 1, name: "增加一行" + item.id + 1, value: item.id % 3 === 0 ? "add row" + item.id : "" }) }) .margin({ top: 30 }) .width("100%") .height(45) .fontSize(22) .fontColor(Color.White) .backgroundColor(Color.Blue) } .width("100%") } }
2. 按值传递参数
调用@Builder装饰的函数默认按值传递,当传递的参数为状态变量的时,变量值的改变并没有触发组件的更新。所以传递参数使用状态变量的时候,推荐使用按引用传递。
示例:
@Builder function updateByValue(value: number) { Row() { Text("the number is " + value) .fontSize(20) .fontColor(Color.White) }.width("100%").height(30).backgroundColor("#ff6602") } @Component export struct ValueBuilder { @State num: number = 10; build() { Column() { updateByValue(this.num) Button("添加一行") .onClick(() => { this.num = 100; }) .margin({ top: 30 }) .width("100%") .height(45) .fontSize(22) .fontColor(Color.White) .backgroundColor(Color.Blue) } .width("100%") } }
作为开发者的我们,在创建一个自定义组件的时候,而该组件承载的是公共的适应多页面使用的,当需要在该组件内设置某个页面需要的特定功能时,我们不能直接在组件内部去设置,这样不仅会导致组件更加臃肿,而且会导致其它使用该组件的地方增加了本来不需要的功能。
为了解决这种问题,ArkUI引入了@BuilderParam装饰器,用来装饰执行@Builder方法的变量,这样开发者就可以根据需要,在自定义组件初始化前传递自己需要的属性,设置特定的功能点。这个装饰器的作用就类似前端开发中的占位符slot。
装饰器使用说明
根据官方文档,@BuilderParam装饰的方法只能被自定义构建函数 (@Builder装饰的方法) 初始化。
下面通过代码展示一下:
// 编写BuilderParamDemo.ets @Builder function WholeApp1() { Text("===WholeApp1===") } @Component export struct BuilderParamDemo { @Builder doNothing() {} @BuilderParam builderParam1: () => void = WholeApp1; @BuilderParam builderParam2: () => void = this.doNothing; build() { Column() { this.builderParam1() Row().margin({ top: 20, bottom: 20 }) this.builderParam2() } } } // 在Index.ets中使用 import { BuilderParamDemo } from './demo/BuilderParamDemo'; @Entry @Component struct Index { @Builder updateBuilderParam() { Button("测试") .width("100%") .height(60) .backgroundColor(Color.Brown) } build() { Row() { // 无参 // BuilderParamDemo() // 有参--父组件自定义构建函数初始化子组件自定义@BuilderParam装饰的方法 BuilderParamDemo({ builderParam2: this.updateBuilderParam }) } .height('100%') } }
下面来个综合的例子,演示一下@BuilderParam装饰的自定义构建函数在以下两个场景中的使用:
@Builder function WholeBuilder($$: { value: string }) { Text($$.value) .fontSize(30) .fontColor(Color.Red) } @Entry @Component struct BuilderIndex { childVal: string = "parent component" @Builder ParentBuilder($$: { value: string }) { Text($$.value) .fontColor(Color.Black) .fontSize(30) } build() { Column(){ // 场景1:通过参数初始化 ChildIndex({ showItem: WholeBuilder }) Divider() ChildIndex({ showItem: WholeBuilder, title: "BuilderIndex" }) // 场景2:尾随闭包初始化组件 // 该场景下自定义组件内有且只有一个使用@BuilderParam装饰的属性 Divider() ChildIndex() {} Divider() ChildIndex({ title: "测试场景2-2" }) {} Divider() ChildIndex({ title: "测试场景2-3" }) { this.ParentBuilder({ value: "the parent Builder" }) } Divider() ChildIndex({ title: "测试场景2-4" }) { Button("Parent button") .width(300) .height(60) .fontSize(26) .backgroundColor("#ff6602") } } .width("100%") .height("100%") } } @Component struct ChildIndex { @State title: string = "ChildIndex" @BuilderParam showItem: ($$: { value: string | number }) => void build() { Row() { Column() { Text(this.title) .fontColor(Color.Blue) .fontSize(30) .margin({ bottom: 20 }) this.showItem({ value: "default value" }) } .width("100%") } .margin({ top: 10, bottom: 10 }) } }
当我们把ChildIndex中@BuilderParam装饰的自定义构建函数注释掉,不在build()函数执行,在parent组件中使用的效果如下图,发现slot效果消失了,所以定义之后的@BuilderParam构建函数一定要在build中执行才有效果。
样式是应用开发中重要的一环,几乎每个组件都必须要设置,也不可避免的出现大量重复的样式代码,这样不仅造成单个文件内代码量过多,而且不容易维护,不容易扩展等问题,所以ArkUI给开发者提供了相应的装饰器解决问题。
@Styles装饰器可以把多条样式写在一个方法,可以在组件需要的地方调用即可。下面是该装饰器支持的与限制的用法
通过下面的代码,我们熟悉一下@Styles
// 全局样式 @Styles function GlobalStyles() { .width("100%") .height("100%") .backgroundColor("#f8f8f8") } @Entry @Component struct SytlesIndex { @State tColor: string = "#FF164FFA" // 组件内定义 @Styles txt_cls() { .width("100%") .height(60) .backgroundColor("#ff6602") .onClick(() => { this.tColor = "#ff6602" }) } @Styles txt() { .width("100%") .height(50) .backgroundColor(this.tColor) } build() { Column() { Text("这是一行文本") .txt() // 使用组件内定义样式 .fontSize(30) .fontColor(Color.White) .margin({ bottom: 20 }) Button("change color") .txt_cls() // 使用组件内定义样式 .fontSize(30) } .GlobalStyles() // 定义的全局样式 } }
总结:@Styles可以提取大部分的样式,但是发现文本类型的样式没办法提取(希望我没有用错),总体上还是很方便的,有点可惜就是不能传参。
@Extend装饰器是在@Styles的基础上,为开发者提供扩展原生(内置)组件样式的能力。
@Extend语法:
@Extend(UI组件名) function fncName() {...}
使用@Extend装饰器需要遵循的规则:
// 只能扩展内置组件,不能扩展自定义组件 // @Extend(Demo) function demoFnc() {} @Extend(Row) function rowCls() { .width("100%") .height(60) .margin({ bottom: 10 }) .backgroundColor("#ff6602") } @Extend(Row) function oddCls() { .rowCls() // 调用预定义的rowCls .backgroundColor(Color.Blue) } // 支持文本css属性 @Extend(Text) function txtCls($$: { color: string | Color }) { .fontSize(30) .fontColor($$.color) } @Entry @Component struct ExtendIndex { @State newColor: string | Color = "#000" changeColor = () => { this.newColor = Color.Red } build() { Column() { Row() { Text("first row") .txtCls({ color: "#fff" }) } .rowCls() Row() { Text("odd row") .txtCls({ color: this.newColor }) // 传递参数:状态变量 } .oddCls() Row() { Button("change color") .fontSize(30) .onClick(this.changeColor) } } } } @Component struct Demo { build() { Column() { Text("demo text") .fontSize(30) } } }
注意:目前ArkTs并不支持@Extend、@Styles的跨文件使用,不能使用export 导出,如果有其他跨文件使用,可以告知一下。
@Styles和@Extend作用于静态页面的样式复用,而stateStyles可以依据组件的内部状态的不同,快速设置不同的样式。
stateStyles是属性方法,可以根据UI内部状态来变更样式,类似于css伪类,不过语法不一样。ArkUI提供以下四种状态:
下面是示例:
@Entry @Component struct StateStylesIndex { @State inputTxt: string = ""; @Styles normalCls() { .width("100%") .height(60) .backgroundColor(Color.Orange) } build() { Column() { Button("按钮") .onClick(() => { this.inputTxt = "点击了按钮"; }) .margin({ bottom: 30, top:5 }) .fontSize(30) .stateStyles({ normal: this.normalCls, pressed: { .backgroundColor(Color.Blue) .fontColor(Color.White) } }) TextInput({ placeholder: "请输入内容", text: this.inputTxt }) .width("90%") .height(50) .fontSize(25) .stateStyles({ normal: { .fontColor("#999999") .borderWidth(2) .borderColor(Color.Gray) }, focused: { .borderWidth(2) .borderColor(Color.Blue) } }) } .width("100%") } }
总结:以上文章的介绍,让我们对ArkTs有了基本的了解,知道了如何去构建页面和自定义组件,后续,我们将去学习它的状态管理等知识点。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。