Skip to content

鸿蒙开发入门

序言

最近部门接到一个鸿蒙开发的项目,我们部门没有会鸿蒙开发的人员,公司又恰巧这时候降薪,别说招新人了,旧人都保不住。部门领导就想让我突击学一下鸿蒙顶上,没办法只能答应(极品牛马)。吐槽归吐槽,但是博主自己也计划学习一下鸿蒙开发,因为国家在大力推动鸿蒙开发的生态,提前储备一下也很好。最后花了两天时间入门了鸿蒙开发,达到了可以参与项目开发的程度。

不得不说一句鸿蒙开发对前端开发人员真的极其友好,所以想以前端开发的视角将鸿蒙开发中涉及到的要点记录下来,并非从0记录,主要是针对已有前端开发经验(例如VueReactCss)的人员。

如果要让我来总结鸿蒙开发的语法,我理解的是鸿蒙融合了各家语言的一些特点,鸿蒙用的语法叫做ArkTs,在Typescript的基础上进行扩展,例如Java中的注解,React的状态管理、模板语法,Vue的一些Api特性,样式上完全采用Css,所以对于前端开发人员来说学习鸿蒙真的So Easy。

模板和样式

模板和样式基础

React模板渲染采用的是jsx语法,我理解成都是一种声明式的语法,鸿蒙也可以理解成类似的语法,有React开发经验的同学一眼就能看出。先看下整体结构:

js
@Component
export struct HelloHarmony {
  @State count: number = 0
  submit: () => void = () => {
    this.count++;
  }

  build() {
    Column(){
      Text("学鸿蒙,就来黑马程序员").fontSize(25).fontWeight(FontWeight.Medium).textAlign(TextAlign.Start).width("100%")
      Flex({direction: FlexDirection.Row,alignItems: ItemAlign.Center}){
        Text("置顶").fontSize(15).fontWeight(FontWeight.Normal).fontColor("red")
        Text("新华社").fontSize(15).fontColor('#666666').margin({left: 6})
        Text(this.count + "评论").fontSize(15).fontColor("#666666").margin({left: 6})
      }.margin({top: 10})

      Row(){
        Button("点赞",{type: ButtonType.Normal,stateEffect: true}).width(80).borderRadius(8).onClick(() => {
          this.submit()
        })
      }.margin({top:70})
    }.padding(20)
  }
}

总结几个关键点:

  • @Component注解标注该struct结果提为一个组件,这个组件可以通过添加export关键字导出,鸿蒙的组件引用是基于前端的ES规范的,这里面定义的变量或者函数,在模板或其他函数中都需要用this关键字访问。
  • 鸿蒙中采用的是Css盒子模型,不是标准盒子模型,是我们开发中常用的IE或者叫怪异盒子模型,这点好评。组件的样式使用的是Css,所以Css80%的属性在鸿蒙中都可以使用,名字大部分是一致的。
  • 每个函数或者叫组件都会有一个build函数,等同于react中的render函数,这里是组件抛出的实际内容。
  • @State定义的变量可以理解为是组件中的内置变量,@State修饰的变量值每次改变都会引发页面的重新渲染,所以需要在页面上渲染并且会改变的值可以用@State定义,注意在struct中定义的一切变量或函数在模板中都需要使用this关键字访问。

这里RowColumn是鸿蒙开发中常用的标签,推荐去官网好好看下,可以理解为是Flex布局的增强版,鸿蒙开发中应该都是块级标签,没有行内标签和块级标签之分。

js
@Component
export struct LoginForm {
  build() {
    Column({space: 20}){
      Row(){
        Column(){
          Image($r('app.media.app_icon')).width(60).height(60).borderRadius(30)
        }.width('100%').margin({top: "15%"})
      }.justifyContent(FlexAlign.Center)
      Row(){
        TextInput({placeholder: "请输入用户名"})
      }.width("100%").margin({top:20})
      Row(){
        TextInput({placeholder: "请输入密码"}).type(InputType.Password)
      }.width("100%")
      Button("登录").width("100%")
      Row(){
        Text("前往注册")
        Text("忘记密码").margin({left:15})
      }.justifyContent(FlexAlign.Center)
    }.padding(20)
  }
}

$r('app.media.app_icon')引入的是src/main/resources/base/media路径下的静态资源

如何抽离封装公共样式和模板

这一块是我觉得挺有意思的地方,除了单独创建封装子组件,鸿蒙提供了很多种封装样式和模板的方法,直接展示代码段:

js
/**
 * @Extend:扩展组件,样式、事件,实现复用的效果
 */
@Extend(Text)
function textFn(color: ResourceColor,txt:string){
  .fontSize(20)
  .fontWeight(FontWeight.Bold)
  .fontColor(color)
  .padding(20)
  .backgroundColor('#e1e1e1')
  .width('100%')
  .onClick(() => {
      AlertDialog.show({
        message: txt
      })
  })
}

// 注意@Styles不支持传参
/**
 * @Styles: 抽取通用属性、事件
 */
// 1、全局定义
@Styles function commonStyles(){
  .width(100)
  .height(100)
  .onClick(() => {
    AlertDialog.show({
      message: "点击"
    })
  })
}

/**
 * 全局@Builde封装,自定义构建函数(结构、样式、事件)
 */
@Builder
function navItem(icon: ResourceStr, text: string){
  Column({space:10}){
    Image(icon).width(80)
    Text(text)
  }.width('100%').onClick(() => {
    AlertDialog.show({
      message: "点击" + text
    })
  })
}

@Component
export struct Study02 {

  @State count: number = 0

  color: string = 'blue'

  // 组件内部定义@Styles 只有在组件内定义才可以访问状态
  @Styles setBg() {
    .backgroundColor(this.color)
  }

  submit: () => void = () => {
    this.count++;
  }

  build() {
    Column({space: 30}) {
      Text('张三Exrend').textFn(Color.Blue,"点击张三")
      Text('李四Exrend').textFn(Color.Blue,"点击李四")
      Text('王五Exrend').textFn(Color.Blue,"点击王五")
      Text("公共Styles").setBg().commonStyles().fontColor("#fff")

      Column(){
        navItem($r('app.media.app_icon'),"阿里拍卖")
        navItem($r('app.media.app_icon'),"菜鸟")
      }
    }
  }
}

@Extend注解让开发者可以自由扩展原生组件的样式和属性,@Styles不支持传参,用于抽取通用属性、事件,@Builder注解用的也很多,可以理解为自定义构建函数(结构、样式、事件),可理解为颗粒度更小的组件,根据不同的场景使用不同的方法。

条件渲染和循环渲染

js
// 组件外的普通变量,改变不会引起视图变化
let msg3:string = "test3"


interface Person{
  stuId: number
  name: string
  gerder: string
  age: number
  url: string
}

@Component
export struct StudyBase {

  // 组件内的普通变量,改变不会引起视图变化
  msg2:string = "test2"

  // @State 标注的变量在变量改变时会触发UI的渲染刷新
  @State mas: string = "hello world"

  @State age: number = 15

  @State stuArr: Person[] = [
    {
      stuId: 1,
      name: "张三",
      gerder: "男",
      age: 18,
      url: 'app.media.app_icon'
    },
    {
      stuId: 2,
      name: "李四",
      gerder: "女",
      age: 24,
      url: 'app.media.app_icon'
    },
    {
      stuId: 3,
      name: "王五",
      gerder: "男",
      url: 'app.media.app_icon',
      age: 26
    },
    {
      stuId: 4,
      name: "张三",
      url: 'app.media.app_icon',
      gerder: "男",
      age: 18
    },
  ]

  // 复杂对象数组改变其中某一项只能替换整个item
  changeTxt = ():void => {
    this.stuArr[1] = {
      stuId: 2,
      name: "张翼德",
      gerder: "女",
      age: 24,
      url: 'app.media.app_icon'
    }
  }

  build() {
    Column({space: 20}){

      Row(){
        Text(this.msg2)
        Text(msg3)
      }

      Column(){
        Text(String(this.age))
        if(this.age < 18){
          Text("18岁以下")
        }
        else if(this.age < 30){
          Text("30岁以下")
        }
        else{
          Text("30岁以上!")
        }

        Button("增加年龄").onClick(() => {
          this.age += 5
        })
      }

      // 循环渲染示例
      Column({space: 20}){
        Text("循环渲染示例").fontSize(24)
        ForEach(this.stuArr, (item:Person,index) => {
          Row(){
            Text(item.name).fontSize(24).fontColor(Color.Orange).layoutWeight(1)
            Image($r(item.url)).width(60).height(60).borderRadius(10)
          }.justifyContent(FlexAlign.SpaceBetween).width('100%').padding(15).backgroundColor('#e1e1e1')
        })
        Button("改变第一项").onClick((event: ClickEvent) => {
          this.changeTxt()
        })
      }


    }.padding(20).backgroundColor('#fff')
  }
}

这里对于学习过VueReact的也比较简单,不做赘述

Scroll用法

这个组件实测下来没有ListListItem组件好用

js
@Component
export struct Study03 {

  myScroll: Scroller = new Scroller()

  build() {
    Column(){
      Scroll(this.myScroll){
        Column({space: 10}){
          ForEach(Array.from({length: 10}) , (item:string,index) => {
            Text("测试文本"+(index + 1)).fontColor('#fff').width('100%').borderRadius(10).backgroundColor(Color.Orange).textAlign(TextAlign.Center).height(60)
          })
        }
      }.width('100%').height(400).scrollable(ScrollDirection.Vertical)
      .scrollBar(BarState.Auto)
      .scrollBarColor(Color.Blue)
      .scrollBarWidth(5)
      .edgeEffect(EdgeEffect.Fade)
      .onScrollStop(() => {
        AlertDialog.show({
          message: `已经滚动的距离,${this.myScroll.currentOffset().yOffset}`
        })
      })

      Button('控制滚动条位置').margin({top:50}).onClick(() => {
        this.myScroll.scrollEdge(Edge.Top)
      })
      Button('获取已经滚动的距离').margin({ top:30 }).onClick(() => {
        const y = this.myScroll.currentOffset().yOffset
        AlertDialog.show({
          message: `y: ${y}`
        })
      })

    }.padding(20)
  }
}

插槽

js
@Component
export struct SonCom {
  @Builder
  tDefaultBuilder(){
    Text('第一个插槽默认内容')
  }
  @Builder
  cDefaultBuilder(){
    Text('第二个插槽默认内容')
  }
  @BuilderParam tBuilder: () => void = this.tDefaultBuilder
  @BuilderParam cBuilder: () => void = this.cDefaultBuilder


  build(){
    Scroll(){
      Column({space: 20}){
        Text("插槽-tBuilder").width('100%').textAlign(TextAlign.Center)
        // 熟悉的插槽。。。 理解为匿名插槽 <slot />
        this.tBuilder()

        Text("插槽-cBuilder").width('100%').textAlign(TextAlign.Center)
        this.cBuilder()
      }
    }
  }
}

在父组件中使用

js
@Component
struct Parent{
  
    @Builder ftBuilder(){
      Text("我是tBuilder内部");
    }
    @Builder fcBuilder(){
      Text("我是cBuilder内部");
    }

    build(){
      SonCom({
        tBuilder: this.ftBuilder,
        cBuilder: this.fcBuilder
      })
    }
}

组件通信

组件通信的几种方式

组件通信一直都是前端开发中必不可少的部分,鸿蒙的父子组件传参跟react的方式很像,通信方式有:单向通信@Prop,双向通信@Link,还有Provide/Inject @Provide/@Consume

js
/**
 * 插槽插槽
 */
@Component
struct SonChildCom {
  @Consume themeColor: string

  build() {
    Column(){
      Text("孙子组件")
      Text(this.themeColor)

      Button("孙子组件修改值").onClick(() => {
        this.themeColor = "red"
      })
    }
  }
}

@Component
export struct SonCom {
  // 父子通信演示 单向通信
  // 父向子通信
  @Prop info : string
  // 子向父通信,和react方式一样
  changeInfo = (txt:string) => {}

  // 双向绑定,相当于v-model
  @Link count: number

  // 相当于inject接收变量
  @Consume themeColor: string

  build(){
    Scroll(){
      Column({space: 20}){

        Text("info")
        Text(this.info)
        Button("修改info").onClick(() => { this.changeInfo('新的值') })

        Text("count")
        Text(this.count.toString())
        Button("内部修改count").onClick(() => { this.count++ })

        Text("二级组件")
        Text(this.themeColor)
        Button("二级组件修改值").onClick(() => {
          this.themeColor = "red"
        })

        // 孙子组件
        SonChildCom()
      }
    }
  }
}

最外层父组件

js
@Entry
@Component
struct Index {

  // 要向子组件发送的值
  @State info: string = "么么哒"
  // 父组件传递给子组件使用的方法,子组件可以调用这个方法实现子组件向父组件通信
  changeInfo = (txt:string) => {
    this.info = txt
  }

  // 双向绑定值演示
  @State count:number = 0

  // 相当于Provide
  @Provide themeColor: string = 'yellow'

  build() {
    Column(){
      // 这里Count使用的是@Link,子组件或父组件改变均可以更新值
      Button("外部修改count").onClick(() => { this.count++ }).margin({top:10})
      // 组件通信
      SonCom({
        info: this.info,
        changeInfo: this.changeInfo,
        count: this.count
      })

    }
  }
}

对象数组更新失败问题

当更新复杂的对象数组时,如果直接更新其中某一项的值是不会触发页面渲染,例如this.list[index].age = 15,这点跟Vue很相似,这时候需要使用@ObjectLink来解决这个问题,具体方法就是在子组件中需要更新的那一项使用@ObjectLink注解标识。当然也可以整体更新那一项也可以触发页面刷新, 例如:this.list[index] = { age:14,... }

js
/**
 * Observed用法
 */
interface IPerson {
  id: number
  name: string
  age: number
}

@Observed
class Person implements IPerson{
  id: number
  name: string
  age: number

  constructor(config:IPerson) {
    this.id = config.id
    this.name = config.name
    this.age = config.age
  }
}

@Component
export struct ObservedAndLink {

  @State personList: Person[] = [
    new Person({
      id: 1,
      name: '张三',
      age: 18
    }),
    new Person({
      id: 2,
      name: '张三',
      age: 18
    }),
    new Person({
      id: 3,
      name: '张三',
      age: 18
    })
  ]

  build() {
    Column({space: 20}){
      Text('父组件').fontSize(20)
      // 属性更新逻辑: 当我们@ObjectLink装饰过的数据属性改变的时候,就会监听到
      // 遍历依赖它的@ObjectLink 包装类,通知数据更新
      Text(this.personList[0].age.toString()).fontSize(20)
      Column({space:10}){
        List({space: 10}){
          ForEach(this.personList,(item: Person,index:number) => {
            ItemCom({
              info:item,
              addAge: () => {
                item.age++
                AlertDialog.show({
                  message: JSON.stringify(this.personList)
                })
              }
            })
          })
        }
      }
    }
  }
}

@Component
struct ItemCom{
  @ObjectLink info : Person
  addAge = () => {}

  build(){
    Row(){
      Text('姓名:'+this.info.name)
      Text('年龄:'+this.info.age)
      Blank()
      Button('修改数据').onClick(() => {
        // this.info.age++
        this.addAge()
      })
    }
  }
}

配置文件

配置文件我目前了解到的主要关注三个主要配置文件

main_pages.json配置所有页面路由,不注册进来就无法访问,顺便提一句@Entry标注的struct就代表一个页面,可以被跳转

src/main/resources/base/profile/main_pages.json

json
{
  "src": [
    "pages/Index"
  ]
}

app.json5配置包名,版本信息,应用图标 AppScope/app.json5

json
{
  "app": {
    "bundleName": "com.example.myapplication",
    "vendor": "example",
    "versionCode": 1000000,
    "versionName": "1.0.0",
    "icon": "$media:app_icon",
    "label": "$string:app_name"
  }
}

module.json5主要针对的是abilities中的配置,这里解释下鸿蒙中认为一个Ability就是一个任务,在使用安卓手机的时候有时候一个APP可以创建多个任务卡片,这里一个任务就是一个Ability,这里可以配置启动的时候采用哪个任务。 src/main/module.json5

json
{
  "module": {
    "name": "entry",
    "type": "entry",
    "description": "$string:module_desc",
    "mainElement": "EntryAbility",
    "deviceTypes": [
      "phone",
      "tablet",
      "2in1"
    ],
    "deliveryWithInstall": true,
    "installationFree": false,
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:layered_image",
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:startIcon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "entities": [
              "entity.system.home"
            ],
            "actions": [
              "action.system.home"
            ]
          }
        ]
      }
    ],
    "extensionAbilities": [
      {
        "name": "EntryBackupAbility",
        "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
        "type": "backup",
        "exported": false,
        "metadata": [
          {
            "name": "ohos.extension.backup",
            "resource": "$profile:backup_config"
          }
        ],
      }
    ]
  }
}

生命周期

待补充

上次更新于: