HarmonyOS组件基础 原创
1.组件-什么是ArkTS
ArkTS是HarmonyOS优选的主力应用开发语言。ArkTS围绕应用开发在TypeScript(简称TS)生态基础上做了进一步扩展,继承了TS的所有特性,是TS的超集。
说明: 也就是前端开发过程中所有的js/ts语法大部分支持的,比如es6中的箭头函数-模板字符串-promise-async/await-数组对象方法。
注意: 根据Next版本的内部沟通,下一版本的ArkTs对类型最了更一步的限制。
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/typescript-to-arkts-migration-V5

这里注意:ArtTS绝不是TS。
①TS在前端中虽然有类型约束,但是他会编译成js去运行。
②ArtTS编译后直接映射字节码-编译过程带类型。
扩展能力如下:
①基本语法
- 定义声明式UI、自定义组件、动态扩展UI元素;
 - 提供ArkUI系统组件,提供组件事件、方法、属性;
 - 共同构成 UI 开发主体。
 
②状态管理
- 组件状态、组件数据共享、应用数据共享、设备共享;
 
③渲染控制
- 条件渲染、循环渲染、数据懒加载;
 
声明式UI

总结:
①AktTS提供原有前端范畴内的一切TypeScript和JavaScript的类型及方法支持。
②不是所有都支持- 比如解构不支持(Next版本)。
③ArkTS采用声明式UI的方法来绘制页面,设置属性,绑定事件。
2.组件结构
接下来,我们来解析我们的UI的结构。
ArkTS通过装饰器 @Component 和 @Entry 装饰 struct 关键字声明的数据结构,构成一个自定义组件。
自定义组件中提供了一个 build 函数,开发者需在该函数内以链式调用的方式进行基本的 UI 描述,UI 描述的方法请参考 UI 描述规范。
(1)struct-自定义组件基于struct
实现
要想实现一段UI的描述,必须使用struct关键字来声明- 注意不能有继承关系-组件名不能系统组件名重名。
语法: struct 组件名 {}。
@Component
struct Index {
}
@CustomDialog 
struct Index2 {
}struct关键字声明的UI描述-必须被@Component或者@CustomDialog修饰。
(2)Component修饰符
Component装饰器只能修饰struct关键字声明的结构,被修饰后的struct具备组件的描述(渲染)能力。
(3)build函数
用于定义组件的UI描述,一个struct结构必须实现build函数。
@Component
struct MyComponent {
  build() {
  }
}build函数是组件(Component)必须提供以及实现的一个函数,build函数可以没有内容,如果有的话,必须有且只有一个容器组件(可以放置子组件的组件)- 只有entry里面有限制- component里面没有限制。
常见容器组件- Flex-Column-Row-List-Grid-Panel。
(4)entry修饰符
entry将自定义组件定义为UI页面的入口,也就是我们原来前端常说的一个页面,最多可以使用entry装饰一个自定义组件(在一个ets文件中)-如下面的代码就是不被允许的。
// 错误示例
@Entry
@Component
struct Index {
  build() {
  }
}
@Entry
@Component
struct Index2 {
  build() {
  }
}entry修饰的组件,最终会被注册,具体文件位置-main/resources/base/profile/main_pages.json。
①自动注册-新建组件时,采用新建Page的方式。
②手动注册-新建一个ets文件,自己在main_pages.json中手动添加路径。
注意:
如果手动删除了某一个带entry的组件,你需要手动去main_page中去删除该路径,否则编译会报错。
(5)组件复用
在很多情况下,由于业务的复杂度,经常会将一个大的业务拆成若干个组件,进行组装,这里我们非常灵活的复用组件,比如:

可以把上图抽象成三个组件- Header- Main- Footer。
@Entry
@Component
struct Index {
  @State message: string = 'Hello
World'
  build() {
    //
它的子组件都会以纵向方式排列
    Column()
{
      Header()
      Main()
      Footer()
    }
  }
}
@Component
struct Header {
  build() {
    Row(){
      Text("Header")
    }
      .width('100%')
      .height(50)
      .backgroundColor(Color.Pink)
    .justifyContent(FlexAlign.Center)
  }
}
@Component
struct Main {
  build() {
    Row(){
      Text("Main")
        .fontColor(Color.White)
    }
    .width('100%')
    .height(200)
    .backgroundColor(Color.Blue)
    .justifyContent(FlexAlign.Center)
  }
}
@Component
struct Footer {
  build() {
    Row(){
      Text("Footer")
        .fontColor(Color.White)
    }
    .width('100%')
    .height(50)
    .backgroundColor(Color.Pink)
    .justifyContent(FlexAlign.Center)
  }
}运行效果,如图所示:

总结:
· 一个UI描述必须使用struct来声明,不能继承。
· struct必须被Component或者CustomDialog修饰。
· struct必须实现build方法,build方法可以没有元素,但是有的话有且只有一个可容纳子组件的容器组件(entry修饰的组件)。
· entry修饰符表示该组件是页面级组件,一个文件中只允许修饰一个struct组件。
· 采用分拆组件的形式可以有效解解耦我们的业务。
3.系统组件(ArkUI)
常用系统组件:
Button Text Column Row Flex Stack Scroll List TextInput Image 更多组件。
组件使用:
· Text 文本组件-(Span子组件)
· Column 列组件,纵向排列,Flex布局主轴是Y (任意子组件)
· Row 行组件,横向向排列,Flex布局主轴是X (任意子组件)
· Flex 以弹性方式布局子组件的容器组件。(
存在二次布局,官方推荐有性能要求,使用Column和Row代替) (任意子组件)
· Button 按钮组件 (单子组件)
· TextInput 输入框组件 (无子组件)
· Image (无子组件)
· Button (单个子组件)
· List (限制ListItem子组件)
· Scroll (限制单个子组件)
组件使用语法:
· 使用组件采用组件名() 的语法
· 有构造参数采用组件名(参数)的语法
· 组件里放置子组件采用组件名() {子组件的语法 } 的链式语法
· 组件设置属性采用组件名().width().height() 的语法
· 组件又有属性又有子组件采用组件名(){ ...子组件 }.width().height() 的语法
接下来,我们来实现一个布局的小例子测试一下我们的能力。
(1)横向布局
实现效果:

代码实现:
@Entry
@Component
struct Index {
  @State message: string = 'Hello
World'
  build() {
    Row(){
      Column(){
        Row({
space:15 }) {
          Column()
            .width(100)
            .height(200)
            .backgroundColor(Color.Pink)
          Column()
            .width(100)
            .height(200)
            .backgroundColor(Color.Red)
          Column()
            .width(100)
            .height(200)
            .backgroundColor(Color.Blue)
        }
        .width('100%')
        .justifyContent(FlexAlign.Center)
      }
      .width('100%')
    }
    .height('100%')
  }
}(2)纵向布局
实现效果:

代码实现:
@Entry
@Component
struct Index {
  build() {
    Column(){
      RowCase()
      ColumnCase()
    }.height('100%').backgroundColor(Color.Grey)
  }
}
@Component
struct RowCase {
  build() {
    Row(){
      Column().height(150).width(100).backgroundColor(Color.Pink)
      Column().height(150).width(100).backgroundColor(Color.Red)
      Column().height(150).width(100).backgroundColor(Color.Blue)
    }
    .width('100%')
    .alignItems(VerticalAlign.Top)
    .justifyContent(FlexAlign.SpaceAround)
  }
}
@Component
struct ColumnCase {
  build() {
    Column(){
      Column().height(100).width(150).backgroundColor(Color.Pink)
      Column().height(100).width(150).backgroundColor(Color.Red)
      Column().height(100).width(150).backgroundColor(Color.Blue)
    }.height(400).width('100%').justifyContent(FlexAlign.SpaceEvenly)
  }
}(3)Flex横纵向
实现效果:

代码实现:
@Entry
@Component
struct Index {
  @State message: string = 'Hello
World'
  build() {
    Scroll(){
      Row(){
        Column(){
          Flex({
direction: FlexDirection.Row,
justifyContent: FlexAlign.SpaceAround }) {
            Column()
              .width(100)
              .height(200)
              .backgroundColor(Color.Pink)
            Column()
              .width(100)
              .height(200)
              .backgroundColor(Color.Red)
            Column()
              .width(100)
              .height(200)
              .backgroundColor(Color.Blue)
          }
          .margin({
            top: 200
          })
        }
        .width('100%')
      }
    }
  }
}在arkUI中,我们的内容如果超过了屏幕显示,则不会显示滚动条,需要使用Scroll来包裹。
需要注意的是:
该组件滚动的前提是主轴方向大小小于内容大小。子组件不要设置高度,否则不能滚动。
4.组件事件
监听原生组件的事件和设置属性的方式是一样的都是链式调用,值得注意的是,我们注册事件都要使用箭头函数的写法,HarmonyOS 5.0版本会有对于匿名函数function的限制。
尝试给一个TextInput和一个按钮注册一个值改变事件和点击事件。
(1)事件定义
实现效果:

点击登录后的效果:

代码实现:
@Entry
@Component
struct Index {
  build() {
    Column({
space: 10 }) {
      TextInput({
placeholder: '请输入用户名' })
        .onChange((value) => {
          AlertDialog.show({
message: value })
        })
      Button('登录').width('100%').onClick((event: ClickEvent) => {
        AlertDialog.show({
message: '点击登录' })
      })
    }
    .width('100%')
    .height('100%')
  }
}请注意:在注册事件中的逻辑务必使用箭头函数() => {},极不推荐function() {}。
①因为function中this指向为undefind
②HarmonyOS 5.0不再支持funtion匿名函数声明
③箭头函数中的this指向当前struct实例,可以方便的调用方法和获取属性
当我们事件处理逻辑比较复杂,写在UI描述中无法抽提的时候,我们可以在struct结构体中定义。
(2)属性定义
当我们需要在组件中记录一些状态时,变量应该显示的在struct中声明,并注明类型,比如-登录账户和密码。
struct Event {
  loginName: string = ""
  password: string = ""
}我们看代码示例时,会发现 public和private关键字,
如果不写或者写public 表示该属性可被外界即父组件赋值,
如果写private表示该属性只会被该组件的this获取。
下面实现一个简单的登录过程:
示例代码:
@Entry
@Component
struct Index {
  username: string = ''
  pwd: string = ''
  login(){
    AlertDialog.show({
			message: `username:${this.username}-pwd: ${this.pwd}` })
  }
  build() {
    Column({space: 10 }) {
      TextInput({placeholder: '请输入用户名' })
        .onChange((value) => {
          this.username = value
          console.log('i-log', value)
        })
      TextInput({placeholder: '请输入密码' })
        .type(InputType.Password)
        .onChange((value) => {
          this.pwd= value
          console.log('i-log', value)
        })
      Button('登录').width('100%').onClick((event: ClickEvent) => {
       this.login()
      })
    }
    .width('100%')
    .height('100%')
  }
}说明
promptAction和AlertDailog都可以弹出提示 promptAction需要引入包,AlertDialog不需要引入就可以直接使用。
再加一个小需求,当用户名和密码为空时,不让用户点按钮。
代码如下:
@Entry
@Component
struct Index {
  username: string = ''
  pwd: string = ''
  login(){
    AlertDialog.show({message: `username:${this.username}-pwd: ${this.pwd}` })
  }
  isSubmitFn(): boolean{
    return this.username!= '' && this.pwd!= ''
  }
  build() {
    Column({space: 10 }) {
      TextInput({placeholder: '请输入用户名' })
        .onChange((value) => {
          this.username = value
          console.log('i-log', value)
        })
      TextInput({placeholder: '请输入密码' })
        .type(InputType.Password)
        .onChange((value) => {
          this.pwd = value
          console.log('i-log', value)
        })
      Button('登录')
        .enabled(this.isSubmitFn())
        .width('100%').onClick((event: ClickEvent) => {
       this.login()
      })
    }
    .width('100%')
    .height('100%')
  }
}我们发现,好像没有变化!!!为什么? 因为我们定义的变量是非响应式数据,值的变化无法导致build函数重新执行, 这就需要引出State修饰符了。
响应式数据- 数据驱动视图更新。
5.组件状态
@State装饰的变量,或称为状态变量,一旦变量拥有了状态属性,就和自定义组件的渲染绑定起来。当状态改变时,UI会发生对应的渲染改变。
如何使用 @State 定义一个状态变量?
(1)组件变量,不具备驱动UI更新能力。
  @State
  username: string = ''
  @State
  pwd: string = ''加上该修饰符后,你惊奇的发现按钮随着数据的变化在变化,因为我们在值改变的时候赋值,造成了build的重新执行,isSubmitFn 函数会重新执行,来保证我们状态的变化。
需要注意的是,State修饰的类型包括:
Object、class、string、number、boolean、enum类型,以及这些类型的数组。类型必须被指定。不支持any,不支持简单类型和复杂类型的联合类型,不允许使用undefined和null。
接下来,我们完成一个数据修改的案例,来看下state的特性。

步骤1:先声明一个User类,interface是ArkTS主推的定义对象类型的形式。
interface Address{
  province: string
  city: string
  area: string
}
interface  User {
  name: string
  age: number
  address: Address
}完整代码如下:
interface Address{
  province: string
  city: string
  area: string
}
interface  User {
  name: string
  age: number
  address: Address
}
@Entry
@Component
struct StateCase {
  @State user: Partial<User>  = {
    name: '张三',
    age: 20,
    address: {
      province: '广东省',
      city: '深圳市',
      area: '南山区'
    }
  }
  build() {
    Row()
{
      Column()
{
        Text(JSON.stringify(this.user))
        Row({
space: 15 }) {
          Text("姓名:")
          TextInput({
text: this.user.name
}).layoutWeight(1)
        }.padding(10)
        Row({
space: 15 }) {
          Text("年龄:")
          TextInput({
text: this.user.age?.toString()
}).layoutWeight(1)
        }.padding(10)
        Row({
space: 15 }) {
          Text("地址:")
          TextInput({
text: this.user.address?.province
}).layoutWeight(1)
          TextInput({
text: this.user.address?.city
}).layoutWeight(1)
          TextInput({
text: this.user.address?.area
}).layoutWeight(1)
        }.padding(10)
      }
      .width('100%')
    }
    .height('100%')
  }
}点击修改数据按钮,设置新数据
运行效果:

点击修改数据,可以修改用户名

代码如下:
    
interface IAddress{
  province: string
  city: string
  area: string
}
interface IUserInfo{
  username: string
  age: number
  address: IAddress
}
export class IAddressModel implements IAddress {
  province: string = ''
  city: string = ''
  area: string = ''
  constructor(model: IAddress) {
    this.province= model.province
    this.city= model.city
    this.area= model.area
  }
}
export class IUserInfoModel implements IUserInfo {
  username: string = ''
  age: number = 0
  address: IAddress = new IAddressModel({} as IAddress)
  constructor(model: IUserInfo) {
    this.username= model.username
    this.age= model.age
    this.address= model.address
  }
}
@Entry
@Component
struct StateCase2 {
  @State
  userInfo: IUserInfoModel = new IUserInfoModel({
    username: '李四',
    age: 21,
    address: new IAddressModel({
      province: '广东',
      city: '广州',
      area: '黄埔'
    })
  })
  build() {
    Column({space: 10}) {
      Row(){
        Text('用户名:')
          .width(80)
        Text(this.userInfo.username).layoutWeight(1)
          .borderRadius(40)
          .backgroundColor('#ccc')
          .height(40)
          .padding({left: 10,right: 10 })
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)
      Row(){
        Text('年龄:')
          .width(80)
        Text(this.userInfo.age.toString()).layoutWeight(1)
          .borderRadius(40)
          .backgroundColor('#ccc')
          .height(40)
          .padding({left: 10,right: 10 })
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)
      Row(){
        Text('地址:')
          .width(80)
        Text(this.userInfo.address.province)
          .borderRadius(40)
          .backgroundColor('#ccc')
          .height(40)
          .padding({left: 10,right: 10 })
        Text(this.userInfo.address.city)
          .borderRadius(40)
          .backgroundColor('#ccc')
          .height(40)
          .padding({left: 10, right: 10})
        Text(this.userInfo.address.area)
          .borderRadius(40)
          .backgroundColor('#ccc')
          .height(40)
          .padding({left: 10,right: 10 })
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)
      Button('修改数据').width('100%').onClick((event: ClickEvent) => {
        this.userInfo.username= Math.random().toFixed(2)
      })
    }
    .padding(20)
    .width('100%')
    .height('100%')
  }
}大家发现,如果state是一个对象,我们改第一层的数据没问题,但是第二层的数据不触发更新,怎么办?
我们依然可以改第一层的数据。如:
this.userInfo.address.area = '天河'代码如下:
interface IAddress{
  province: string
  city: string
  area: string
}
 
interface IUserInfo{
  username: string
  age: number
  address: IAddress
}
 
 
export class IAddressModel implements IAddress {
  province: string = ''
  city: string = ''
  area: string = ''
 
  constructor(model: IAddress) {
    this.province = model.province
    this.city = model.city
    this.area = model.area
  }
}
export class IUserInfoModel implements IUserInfo {
  username: string = ''
  age: number = 0
  address: IAddress = new IAddressModel({} as IAddress)
 
  constructor(model: IUserInfo) {
    this.username = model.username
    this.age = model.age
    this.address = model.address
  }
}
 
 
@Entry
@Component
struct StateCase3 {
 
  @State
  userInfo: IUserInfoModel = new IUserInfoModel({
    username: 'mark',
    age: 21,
    address: new IAddressModel({
      province: '广东',
      city: '广州',
      area: '黄埔'
    })
  })
 
  build() {
    Column({space: 10}) {
      Row() {
        Text('用户名:')
          .width(80)
        Text(this.userInfo.username).layoutWeight(1)
          .borderRadius(40)
          .backgroundColor('#ccc')
          .height(40)
          .padding({ left: 10, right: 10 })
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)
 
      Row() {
        Text('年龄:')
          .width(80)
        Text(this.userInfo.age.toString()).layoutWeight(1)
          .borderRadius(40)
          .backgroundColor('#ccc')
          .height(40)
          .padding({ left: 10, right: 10 })
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)
 
 
      Row() {
        Text('地址:')
          .width(80)
        Text(this.userInfo.address.province)
          .borderRadius(40)
          .backgroundColor('#ccc')
          .height(40)
          .padding({ left: 10, right: 10 })
        Text(this.userInfo.address.city)
          .borderRadius(40)
          .backgroundColor('#ccc')
          .height(40)
          .padding({ left: 10, right: 10 })
        Text(this.userInfo.address.area)
          .borderRadius(40)
          .backgroundColor('#ccc')
          .height(40)
          .padding({ left: 10, right: 10 })
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)
 
      Button('修改数据').width('100%').onClick((event: ClickEvent) => {
        this.userInfo.username = Math.random().toFixed(2)
        this.userInfo.address.area = '天河'
      })
    }
    .padding(20)
    .width('100%')
    .height('100%')
  }
}




















