鸿蒙原生开发从入门到遥遥领先

283 阅读27分钟

本文写于2023年10月,HarmonyOS NEXT发布的时候,预判到纯血鸿蒙的时代即将到来,所以写了此文在组内分享。现在发布到这里,希望对大家有帮助

前言-鸿蒙准备好了么?

几个事实:

  1. 搭载鸿蒙系统设备7亿,抖音 Android 用户中,鸿蒙的设备占比约四分一
  2. 鸿蒙终于不套壳了?纯血 HarmonyOS NEXT 即将到来
  3. 美团与华为达成合作:启动鸿蒙原生应用开发

一、鸿蒙系统介绍

gitee.com/openharmony…

OpenHarmony是由开放原子开源基金会(OpenAtom Foundation)孵化及运营的开源项目,目标是面向全场景、全连接、全智能时代,基于开源的方式,搭建一个智能终端设备操作系统的框架和平台,促进万物互联产业的繁荣发展。

最重要的特点:

  • 分布式:分布式软总线(同一应用不同设备间通信)、分布式数据管理(数据跨设备存储和流动)、分布式任务调度(远程启动、绑定、迁移)、设备虚拟化(超级虚拟终端)

  • 弹性部署:轻量系统(最小内存128KB,智能家居,可穿戴,传感器)、小型系统(最小内存1MB,监控、路由器,行车记录仪)、标准系统(最小内存128MB,冰箱显示屏)

  • 方舟编译器与方舟运行时

二、纯血鸿蒙应用开发

以下开发流程均基于Harmony OS 3.1/4.0 API 9 release,版本说明。在3.0还支持Java API

此外鸿蒙还支持类似Web开发HML、CSS、JavaScript三段式开发方式,感兴趣可以了解概述-兼容JS的类Web开发范式-UI开发-开发-HarmonyOS应用开发

2.1 基本概念

  • ArkTS

    • JavaScript是一种属于网络的高级脚本语言,已经被广泛用于Web应用开发,常用来为网页添加各式各样的动态功能,为用户提供更流畅美观的浏览效果。
    • TypeScript 是 JavaScript 的一个超集,它扩展了 JavaScript 的语法,通过在JavaScript的基础上添加静态类型定义构建而成,是一个开源的编程语言。
    • ArkTS兼容TypeScript语言,拓展了声明式UI、状态管理、并发任务等能力。

  • ArkUI

方舟开发框架(简称ArkUI)为HarmonyOS应用的UI开发提供了完整的基础设施,包括简洁的UI语法、丰富的UI功能(组件、布局、动画以及交互事件),以及实时界面预览工具等,可以支持开发者进行可视化界面开发。

2.2 开发环境配置与介绍

DevEcoStudio下载地址:HUAWEI DevEco Studio和SDK下载和升级 | HarmonyOS开发者

环境搭建:搭建开发环境流程-快速开始-DevEco Studio使用指南-工具-HarmonyOS应用开发

配置好环境后后新建项目时如果碰到The SDK license agreement is not accepted的问题,可以参考下面的链接修改国家后重新打开SDK Manager下载ToolChains解决:搞不懂,老提示我The SDK license agreement is not accepted这玩意在哪同意啊 | 华为开发者论坛

2.3 应用模型与项目结构

FA模型和Stage模型
  • FA(Feature Ability)模型:HarmonyOS早期版本开始支持的模型,已经不再主推。
  • Stage模型:HarmonyOS 3.1 Developer Preview版本开始新增的模型,是目前主推且会长期演进的模型

应用模型解读-应用模型概述-应用模型-开发-HarmonyOS应用开发

Stage模型概述

  • UIAbility组件 和ExtensionAbility组件:Stage模型提供UIAbility和ExtensionAbility两种类型的组件,这两种组件都有具体的类承载,支持面向对象的开发方式。

    • UIAbility组件是一种包含UI界面的应用组件,主要用于和用户交互。例如,图库类应用可以在UIAbility组件中展示图片瀑布流,在用户选择某个图片后,在新的页面中展示图片的详细内容。同时用户可以通过返回键返回到瀑布流页面。UIAbility的生命周期只包含创建/销毁/前台/后台等状态,与显示相关的状态通过WindowStage的事件暴露给开发者。
    • ExtensionAbility组件是一种面向特定场景的应用组件(壁纸、输入法,无障碍等)。例如WorkSchedulerExtensionAbility提供延迟任务服务的注册、取消和查询。FormExtensionAbility提供服务卡片的能力
  • WindowStage: 每个UIAbility类实例都会与一个WindowStage类实例绑定,该类提供了应用进程内窗口管理器的作用。它包含一个主窗口。也就是说UIAbility通过WindowStage持有了一个窗口,该窗口为ArkUI提供了绘制区域。

  • Context:在Stage模型上,Context及其派生类向开发者提供在运行期可以调用的各种能力。UIAbility组件和各种ExtensionAbility派生类都有各自不同的Context类,他们都继承自基类Context,但是各自又根据所属组件,提供不同的能力。跟安卓的Context概念类似。

  • A bilityStage (类似Application) :每个Entry类型或者Feature类型的HAP在运行期都有一个AbilityStage类实例,当HAP中的代码首次被加载到进程中的时候,系统会先创建AbilityStage实例。每个在该HAP中定义的UIAbility类,在实例化后都会与该实例产生关联。开发者可以使用AbilityStage获取该HAP中UIAbility实例的运行时信息。

  • Want:类似Intent,用来显式或者隐式启动UIAbility以及传输数据
Stage模型项目结构

UIAbility使用

跟Acitivity不能说毫不相干,只能说一模一样

声明

需要在module.json5(类似安卓的AndroidManifest.xml)中声明,其中launchType为启动模式(与安卓Activity启动模式类似),而且UIAblity也支持配置隐式启动参数:

{
  "module": {
    // ...
    "abilities": [
      {
        "name": "EntryAbility", // UIAbility组件的名称
        "srcEntrance": "./ets/entryability/EntryAbility.ts", // UIAbility组件的代码路径
        "description": "$string:EntryAbility_desc", // UIAbility组件的描述信息
        "icon": "$media:icon", // UIAbility组件的图标,如果配置了icon和label,就会在桌面展示一个图标
        "label": "$string:EntryAbility_label", // UIAbility组件的标签
        "startWindowIcon": "$media:icon", // UIAbility组件启动页面图标资源文件的索引
        "startWindowBackground": "$color:start_window_background", // UIAbility组件启动页面背景颜色资源文件的索引
        "launchType": "standard", // 启动模式 singleton,standard,specified为指定实例模式,针对一些特殊场景使用
                                   //(例如文档应用中每次新建文档希望都能新建一个文档实例,重复打开一个已保存的文档希望打开的都是同一个文档实例)
        // ...
      }
    ]
  }
}
生命周期及基本用法

下面展示的是一个UIAbility,与Activity不同的是UIAbility中并不直接生成UI或者对UI进行一些操作,而是在onWindowStageCreate中指定加载了一个Page(类比Fragment),这个Page路径是在'pages/Index'文件中,这里面使用声明式UI描述了UI的样式,这里在Index中展示了一个Hello World

// entryability/EntryAbility.ets 
import UIAbility from '@ohos.app.ability.UIAbility'
import Window from '@ohos.window'

export  default  class EntryAbility extends UIAbility {
  onCreate(want, launchParam) {
  }

  onDestroy() {
  }

  onWindowStageCreate(windowStage: Window.WindowStage) {
    // 指定UIAbility的启动Page
    windowStage.loadContent('pages/Index', (err, data) => {
      if (err.code) {
        return
}
    })
  }

  onWindowStageDestroy() {
  }

  onForeground() {
  }

  onBackground() {
  }
}
// pages/Index.ets 的内容
@Entry
@Component
struct Index {
  @State message: string = 'Hello World'

  build() {
    Column() {
      Text(this.message)
        .fontSize(50)
        .fontColor(0x999900)
        .fontWeight(FontWeight.Bold)
    }
    .width('100%')
  }
}

上面的例子比较简单,实际商业应用页面的UI一般都比较复杂,如果把UI的声明都放在page里面,既不好维护,也不利于复用,这时就需要封装成组件(Component),再在page中组装成完整的一个页面:

以直播列表为例, Live是一个Page,使用Entry和Component两个装饰器修饰,这里调用Header和LiveList两个Component,跟Page的区别是仅用Component修饰,也就是说Page也是一种Component(是不是page也可以嵌入到其他page里面?):

// pages/Live.ets
@Entry
@Component
struct Live {
  build() {
    Column() {
      Header()
      LiveList()
    }
    .width(LiveConstants.FULL_WIDTH_PERCENT)
    .height(LiveConstants.FULL_HEIGHT_PERCENT)
  }
}

// components/Header.ts
@Component
export struct Header {
  @Link currentBreakpoint: string;

  build() {
    Row() {
      Image($r('app.media.ic_back'))
        .width($r('app.float.icon_width'))
        .height($r('app.float.icon_height'))
        .margin({ left: $r('app.float.icon_margin') })
        .onClick(() => {
          router.back()
        })
      Text($r('app.string.play_list'))
        .fontSize(new BreakpointType({
          sm: $r('app.float.header_font_sm'),
          md: $r('app.float.header_font_md'),
          lg: $r('app.float.header_font_lg')
        }).getValue(this.currentBreakpoint))
        .fontWeight(HeaderConstants.TITLE_FONT_WEIGHT)
        .fontColor($r('app.color.title_color'))
        .opacity($r('app.float.title_opacity'))
        .letterSpacing(HeaderConstants.LETTER_SPACING)
        .padding({ left: $r('app.float.title_padding_left') })

      Blank()

      Image($r('app.media.ic_more'))
        .width($r('app.float.icon_width'))
        .height($r('app.float.icon_height'))
        .margin({ right: $r('app.float.icon_margin') })
        .bindMenu(this.getMenu())
    }
    .width(StyleConstants.FULL_WIDTH)
    .height($r('app.float.title_bar_height'))
    .zIndex(HeaderConstants.Z_INDEX)
  }

// LiveList.ets
@Component
export struct LiveList {
  private scroller: Scroller = new Scroller();
  @State liveStreams: LiveStream[] = new LiveStreamViewModel().getLiveStreamList();
  @State currentBreakpoint: string = LiveConstants.CURRENT_BREAKPOINT;

  build() {
    GridRow({
      columns: {
        sm: LiveConstants.FOUR_COLUMN,
        md: LiveConstants.EIGHT_COLUMN,
        lg: LiveConstants.TWELVE_COLUMN
      },
      breakpoints: {
        value: [
        LiveConstants.SMALL_DEVICE_TYPE,
        LiveConstants.MIDDLE_DEVICE_TYPE,
        LiveConstants.LARGE_DEVICE_TYPE
        ]
      },
      gutter: { x: $r('app.float.grid_row_gutter') }
    }) {
       .......
    }
 }
UIAbility组件与Page进行通信

由于UIAbility不能直接操作UI,这就涉及到UIAbility与Page之间的通信

  1. EventHub:基于发布订阅模式来实现,事件需要先订阅后发布,订阅者收到消息后进行处理,跟其他平台的事件总线没有什么区别
  2. globalThis:ArkTS引擎实例内部的一个全局对象,在ArkTS引擎实例内部都能访问。

onWindowStageCreate(windowStage: window.WindowStage) {
  // Main window is created, set main page for this ability
  hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
  globalThis.entryStr = 'This is a message from UIAbility'; // show this message in index
  windowStage.loadContent('pages/Index', (err, data) => {
  ....
  });
}

// 以下是page中的UI代码
@Entry
@Component
struct Index {
  @State message: string = globalThis.entryStr // 通过globalThis获取值
  build() {
    Column() {
      Text(this.message)
        .fontSize(50)
        .fontColor(0x999900)
        .fontWeight(FontWeight.Bold)
    }
    .width('100%')
  }
}

除了用来与Page进行通信,globalThis还可用来UIAbility之间的通信,需要注意的是同一个进程下共享同一个引擎,如果出现同名的情况可能会覆盖(

  1. 使用AppStorage/LocalStorage进行数据同步:鸿蒙页面共享数据实践(LocalStorage)
UIAbility之间的跳转
  • 可以构造Want(与Intent类似)并通过startAbility启动其他UIAbility
  • 可以通过startAbilityForResult启动其他UIAbility并获取结果
  • 可以启动其他应用UIAbility(显示启动bundleName和abilityName,隐式启动entities、actions)

Stage进程模型
  • 应用中(同一包名)的所有UIAbility、ServiceExtensionAbility和DataShareExtensionAbility运行在同一个独立进程中,即主进程。
  • 应用中(同一包名)同一类型ExtensionAbility(除ServiceExtensionAbility和DataShareExtensionAbility外)均是运行在一个独立进程中。
  • WebView拥有独立的渲染进程。
  • 系统应用可以申请多进程,把UIAbility放在不同的进程下。

基于鸿蒙的进程模型,系统也提供了类似于安卓广播的机制,通过CES(Common Event Service,公共事件服务)为应用程序提供订阅、发布、退订公共事件的能力。这种通知可以是一对多,即可能会有多个不同的事件订阅者。公共事件从系统角度可分为:系统公共事件和自定义公共事件。

系统公共事件列表

  • 系统公共事件:CES内部定义的公共事件,只有系统应用和系统服务才能发布,例如HAP安装,更新,卸载等公共事件。目前支持的系统公共事件详见系统公共事件定义-HarmonyOS应用开发
  • 自定义公共事件:应用自定义一些公共事件用来实现跨进程的事件通信能力。

Stage线程模型

HarmonyOS应用中每个进程都会有一个主线程,主线程有如下职责:

  1. 执行UI绘制;
  2. 管理主线程的ArkTS引擎实例,使多个UIAbility组件能够运行在其之上;
  3. 管理其他线程(例如Worker线程)的ArkTS引擎实例,例如启动和终止其他线程;
  4. 分发交互事件;
  5. 处理应用代码的回调,包括事件处理和生命周期管理;
  6. 接收Worker线程发送的消息;

work线程相当于子线程,可以执行耗时操作,最多有8个worker线程

线程间通信通过Emitter(订阅)或者主线程可以向worker发布事件

2.4 基于ArkTS声明式布局的UI开发

除了使用ArkTS声明式布局的UI开发,鸿蒙还支持类似JS Web的开发方式,即采用Html、CSS、JS开发页面,这个暂不在本次讨论范围,感兴趣的可以参考概述-HarmonyOS应用开发

2.4.1 Hello World

2.4.2 状态管理

一般应用开发过程中经常根据数据对UI进行刷新,ArkUI可以通过状态变量(相对而言,常规变量只用于辅助计算,并不会引起UI变化)数据的变化驱动UI发生改变,这些数据在组件(Component)内部,父子组件之间以及在应用内的状态同步和管理是通过一些装饰器实现的,这些装饰器有:

暂时无法在飞书文档外展示此内容

装饰器同步类型允许装饰的变量类型从父组件初始化初始化子组件
@State不与父组件同步Object、class、string、number、boolean、enum类型,以及这些类型的数组仅本地初始化可以用于子组件中常规变量、@State、@Link、@Prop、@Provide修饰的变量
@Prop单向同步,即子组件的修改不会同步回父组件string、number、boolean、enum类型,不支持any,不允许使用undefined和null如果本地有初始化,则是可选的。没有的话,则必选,支持父组件中的常规变量、@State、@Link、@Prop、@Provide、@Consume、@ObjectLink、@StorageLink、@StorageProp、@LocalStorageLink和@LocalStorageProp去初始化子组件中的@Prop变量。支持去初始化子组件中的常规变量、@State、@Link、@Prop、@Provide。
@Link双向同步Object、class、string、number、boolean、enum类型,以及这些类型的数组禁止本地初始化。与父组件@State, @StorageLink和@Link 建立双向绑定。允许父组件中@State、@Link、@Prop、@Provide、@Consume、@ObjectLink、@StorageLink、@StorageProp、@LocalStorageLink和@LocalStorageProp装饰变量初始化子组件@Link。可用于初始化常规变量、@State、@Link、@Prop、@Provide。
@Provide与@Consume双向同步Object、class、string、number、boolean、enum类型,以及这些类型的数组仅本地初始化可用于初始化@State、@Link、@Prop、@Provide
@Consume与@Provide双向同步Object、class、string、number、boolean、enum类型,以及这些类型的数组通过相同的变量名和alias(别名)从@Provide初始化可用于初始化@State、@Link、@Prop、@Provide
@LocalStorageProp单向同步:从LocalStorage的对应属性到组件的状态变量Object、class、string、number、boolean、enum类型,以及这些类型的数组禁止,只能从LocalStorage中key对应的属性初始化,如果没有对应key的话,将使用本地默认值初始化。可用于初始化@State、@Link、@Prop、@Provide
@LocalStorageLink双向同步与LocalStorageObject、class、string、number、boolean、enum类型,以及这些类型的数组禁止,只能从LocalStorage中key对应的属性初始化,如果没有对应key的话,将使用本地默认值初始化。可用于初始化@State、@Link、@Prop、@Provide
@StorageProp单向同步:从AppStorage的对应属性到组件的状态变量Object、class、string、number、boolean、enum类型,以及这些类型的数组禁止,只能从AppStorage中key对应的属性初始化,如果没有对应key的话,将使用本地默认值初始化。可用于初始化@State、@Link、@Prop、@Provide
@StorageLink双向同步与AppStorageObject、class、string、number、boolean、enum类型,以及这些类型的数组禁止,只能从AppStorage中key对应的属性初始化,如果没有对应key的话,将使用本地默认值初始化。可用于初始化@State、@Link、@Prop、@Provide

有一个典型的例子就是音乐播放器,音乐播放状态、播放进度这些状态需要在播放页、应用主页等会展示和控制播放状态的页面之间进行共享:

@Component
export struct ControlComponent {
  @StorageLink('isPlay') @Watch('updatePlay') isPlay: boolean = false;
}

@Component
export struct Player {
  @StorageLink('isPlay') @Watch('animationFun') isPlay: boolean = false;
}

2.4.3 布局

  1. 线性布局:Row(列,水平布局) Column(行,垂直布局)
  2. 层叠布局(Stack):按顺序叠起来,可以通过AlignContent参数指定对齐方式,通过zIndex指定z轴的顺序,数字大的在上方

Column(){
  Stack({ }) {
    Column(){}.width('90%').height('100%').backgroundColor('#ff58b87c')
    Text('text').width('60%').height('60%').backgroundColor('#ffc3f6aa')
    Button('button').width('30%').height('30%').backgroundColor('#ff8ff3eb').fontColor('#000')
  }.width('100%').height(150).margin({ top: 50 })
}

3. 弹性布局(Flex):设置元素在主轴和交叉轴上的布局方式 弹性布局(Flex)-HarmonyOS应用开发 4. 相对布局(RelativeContainer)

  1. 栅格布局(GridRow、GridCol)
  2. 列表(List)

下面展示了在一个列表里展示一个联系人列表

import util from '@ohos.util';

class Contact {
  key: string = util.generateRandomUUID(true);
  name: string;
  icon: Resource;

  constructor(name: string, icon: Resource) {
    this.name = name;
    this.icon = icon;
  }
}

@Entry
@Component
struct SimpleContacts {
  private contacts = [
    new Contact('小明', $r("app.media.iconA")),
    new Contact('小红', $r("app.media.iconB")),
    ...
  ]

  build() {
    List() {
      ForEach(this.contacts, (item: Contact) => {
        ListItem() {
          Row() {
            Image(item.icon)
              .width(40)
              .height(40)
              .margin(10)
            Text(item.name).fontSize(20)
          }
          .width('100%')
          .justifyContent(FlexAlign.Start)
        }
      }, item => item.key)
    }
    .width('100%')
  }
}

7. 轮播(Swiper)

2.4.4 组件

  1. 按钮Button
Button('Ok', { type: ButtonType.Normal, stateEffect: true }) 
  .onClick(()=>{ 
    console.info('Button onClick') 
  })

2. 文本

跟安卓类似,可以通过Span来设置文本的样式

Text() {
  Span('我是Span1,').fontSize(16).fontColor(Color.Grey)
    .decoration({ type: TextDecorationType.LineThrough, color: Color.Red })
  Span('我是Span2').fontColor(Color.Blue).fontSize(16)
    .fontStyle(FontStyle.Italic)
    .decoration({ type: TextDecorationType.Underline, color: Color.Black })
  Span(',我是Span3').fontSize(16).fontColor(Color.Grey)
    .decoration({ type: TextDecorationType.Overline, color: Color.Green })
}
.borderWidth(1)
.padding(10)

3. 输入框(单行TextInput/多行TextArea)

TextInput()
  .onChange((value: string) => {
    console.info(value);
  })
  .onFocus(() => {
    console.info('获取焦点');
  })

4. 进度条

Progress有5种可选类型,在创建时通过设置ProgressType枚举类型给type可选项指定Progress类型。其分别为:ProgressType.Linear(线性样式)、 ProgressType.Ring(环形无刻度样式)、ProgressType.ScaleRing(环形有刻度样式)、ProgressType.Eclipse(圆形样式)和ProgressType.Capsule(胶囊样式)。

@Entry
@Component
struct ProgressCase1 { 
  @State progressValue: number = 0    // 设置进度条初始值为0
  build() {
    Column() {
      Column() {
        Progress({value:0, total:100, type:ProgressType.Capsule}).width(200).height(50)
          .style({strokeWidth:50}).value(this.progressValue)
        Row().width('100%').height(5)
        Button("进度条+5")
          .onClick(()=>{
            this.progressValue += 5
            if (this.progressValue > 100){
              this.progressValue = 0
            }
          })
      }
    }.width('100%').height('100%')
  }
}

  1. 自定义弹窗
  2. 单选框&切换按钮
  3. XComponent(Surface)
  4. 气泡提示和菜单

2.4.5 动画

显式动画把要执行的动画放在闭包中,可以执行比较复杂的动画,例如数据变化引起的组件的增删,或者组件属性的变化。属性动画则是指定做动画的属性,当属性发生改变时会自动做动画。这么看显式动画和属性动画与安卓的属性动画的概念并不一致。这两个动画都会改变组件的实际属性。

  • 显式动画&属性动画
// 显式动画调用以下接口实现“第一个参数指定动画参数,第二个参数为动画的闭包函数。
animateTo(value: AnimateParam, event: () => void): void

Column({ space: 10 }) {
  Column() {
    // Button放在足够大的容器内,使其不影响更外层的组件位置
    Button("text")
      .type(ButtonType.Normal)
      .width(this.myWidth)
      .height(this.myHeight)
  }
  .margin(20)
  .width(200)
  .height(100)

  Button("area: click me")
    .fontSize(12)
    .onClick(() => {
      animateTo({ duration: 1000, curve: Curve.Ease }, () => {
        // 动画闭包中根据标志位改变控制第一个Button宽高的状态变量,使第一个Button做宽高动画
        if (this.flag) {
          this.myWidth = 100;
          this.myHeight = 50;
        } else {
          this.myWidth = 200;
          this.myHeight = 100;
        }
        this.flag = !this.flag;
      });
    })
}
.width("100%")
.height("100%")

// 属性动画的接口,想要组件随某个属性值的变化而产生动画,此属性需要加在animation属性之前。
// 有的属性变化不希望通过animation产生属性动画,可以放在animation之后
animation(value: AnimateParam)

@Entry
@Component
struct LayoutChange2 {
  @State myWidth: number = 100;
  @State myHeight: number = 50;
  @State flag: boolean = false;
  @State myColor: Color = Color.Blue;

  build() {
    Column({ space: 10 }) {
      Button("text")
        .type(ButtonType.Normal)
        .width(this.myWidth)
        .height(this.myHeight)
        // animation只对其上面的type、width、height属性生效,时长为1000ms,曲线为Ease
        .animation({ duration: 1000, curve: Curve.Ease })
        // animation对下面的backgroundColor、margin属性不生效
        .backgroundColor(this.myColor)
        .margin(20)

      Button("area: click me")
        .fontSize(12)
        .onClick(() => {
          // 改变属性值,配置了属性动画的属性会进行动画过渡
          if (this.flag) {
            this.myWidth = 100;
            this.myHeight = 50;
            this.myColor = Color.Blue;
          } else {
            this.myWidth = 200;
            this.myHeight = 100;
            this.myColor = Color.Pink;
          }
          this.flag = !this.flag;
        })
    }
  }
}
  • 转场动画

组件内转场动画的接口为:

transition(value: TransitionOptions)

Button()
  .transition({ type: TransitionType.Insert, translate: { x: 200, y: -200 }, opacity: 0 })
  .transition({ type: TransitionType.Delete, rotate: { x: 0, y: 0, z: 1, angle: 360 } })

页面转场动画

通过PageTransitionEnter和PageTransitionExit指定页面进入和退出的动画

PageTransitionEnter({type?: RouteType,duration?: number,curve?: Curve | string,delay?: number})
PageTransitionExit({type?: RouteType,duration?: number,curve?: Curve | string,delay?: number})

共享元素

共享元素的页面间转场接口:

sharedTransition(id: string, options?: sharedTransitionOptions)

在原页面和目标页面的共享元素组件上都绑定同样id的转场动画,即可实现共享元素的转场(类似飞书中点击聊天中的图片查看大图时的转场动画):

// 原页面
struct SharedTransitionSrc {
  build() {
    Column() {
      // 配置Exchange类型的共享元素转场,共享元素id为"sharedImage1"
      Image($r('app.media.mountain')).width(50).height(50)
        .sharedTransition('sharedImage1', { duration: 1000, curve: Curve.Linear })
        .onClick(() => {
          // 点击小图时路由跳转至下一页面
          router.pushUrl({ url: 'pages/myTest/sharedTransitionDst' });
        })
    }
    ......
  }
}
// 目标页面
struct SharedTransitionDest {
  build() {
    Column() {
      ........
      // 配置Exchange类型的共享元素转场,共享元素id为"sharedImage1"
      Image($r('app.media.mountain'))
        .width(150)
        .height(150)
        .sharedTransition('sharedImage1', { duration: 500, curve: Curve.Linear })
        .onClick(() => {
          // 点击图片时路由返回至上一页面
          router.back();
        })
    }
    
  }
}

2.4.6 提高UI性能的建议

  1. 数据懒加载

例如在使用列表组件时不是把所有Item都加载,而是使用IDataSource接口进行懒加载(类似RecyclerView.Adapter/UITableDelegate)

  1. 设置List组件的宽高

在使用Scroll容器组件嵌套List组件加载长列表时,若不指定List的宽高尺寸,则默认全部加载。这点与安卓中ScrollView嵌套ListView的情况类似

  1. 使用条件渲染替代显隐控制

以下代码实现的是对组件的显示隐藏的控制,跟一般安卓或者iOS的UI开发的直觉不同,在声明式的UI中可以使用条件渲染代替显示隐藏的属性

@Entry
@Component
struct MyComponent {
  @State isVisible: Visibility = Visibility.Visible;

  build() {
    Column() {
      Button("显隐切换")
        .onClick(() => {
          if (this.isVisible == Visibility.Visible) {
            this.isVisible = Visibility.None
          } else {
            this.isVisible = Visibility.Visible
          }
        })
      Row().visibility(this.isVisible)
        .width(300).height(300).backgroundColor(Color.Pink)
    }.width('100%')
  }
}

@Entry
@Component
struct MyComponent {
  @State isVisible: boolean = true;

  build() {
    Column() {
      Button("显隐切换")
        .onClick(() => {
          this.isVisible = !this.isVisible
        })
      if (this.isVisible) {
        Row()
          .width(300).height(300).backgroundColor(Color.Pink)
      }
    }.width('100%')
  }
}

4. 使用Column/Row替代Flex

这点好理解,Flex需要额外的计算。

2.4.7 交互

交互事件按照触发类型来分类,包括触屏事件、键鼠事件和焦点事件。

  1. 触屏事件

触屏事件分为点击事件,拖拽事件和触摸事件。

  • 点击事件比较简单,onClick里面接收和处理事件。
  • 拖拽事件指的是长按组件超过500ms,直至手势松开

| onDragStart(event: (event?: DragEvent, extraParams?: string) => CustomBuilder | DragItemInfo) | 拖拽启动接口。当前仅支持自定义pixelmap和自定义组件。 | | ---------------------------------------------------------------------------------------------- | ------------------------------------------------- | | onDragEnter(event: (event?: DragEvent, extraParams?: string) => void) | 拖拽进入组件接口。DragEvent定义拖拽发生位置,extraParmas表示用户自定义信息 | | onDragLeave(event: (event?: DragEvent, extraParams?: string) => void) | 拖拽离开组件接口。DragEvent定义拖拽发生位置,extraParmas表示拖拽事件额外信息。 | | onDragMove(event: (event?: DragEvent, extraParams?: string) => void) | 拖拽移动接口。DragEvent定义拖拽发生位置,extraParmas表示拖拽事件额外信息。 | | onDrop(event: (event?: DragEvent, extraParams?: string) => void) | 拖拽释放组件接口。DragEvent定义拖拽发生位置,extraParmas表示拖拽事件额外信息。 |

  • 触摸事件

通过onTouch接口接收触摸事件:

onTouch(event: (event?: TouchEvent) => void)

触摸事件分为Down Up Move三种事件,触摸事件传递是由子view向上传递到布局里面,下面的代码在Text和Row上分别监听触摸事件并打印日志:

import hilog from '@ohos.hilog';

@Entry
@Component
struct Index {
  @State message: string = globalThis.entryStr
  build() {
    Column() {
      Text(this.message)
        .fontSize(50)
        .fontColor(0x999900)
        .fontWeight(FontWeight.Bold)
      Row({space: 20}){
        Text(this.message)
          .fontSize(25)
          .fontWeight(FontWeight.Bold).onDragStart((event?: DragEvent, extraParams?: String) => {
              hilog.info(0x0000, 'testTag', 'drag start on Text');
        })
        Text(this.message)
          .fontSize(25)
          .fontWeight(FontWeight.Bold).onTouch((event: TouchEvent) => {
          if (event.type === TouchType.Down) {
            hilog.info(0x0000, 'testTag', 'touch down on Text');
          }
          if (event.type === TouchType.Up) {
            hilog.info(0x0000, 'testTag', 'touch up on Text');
          }
          if (event.type === TouchType.Move) {
            hilog.info(0x0000, 'testTag', 'touch move on Text');
          }
          // 如果要阻止该触摸事件向上传递,则调用以下方法:
          // event.stopPropagation()
        })
      }
      .height('100%')
      .onTouch((event: TouchEvent) => {
        if (event.type === TouchType.Down) {
          hilog.info(0x0000, 'testTag', 'touch down on Row');
        }
        if (event.type === TouchType.Up) {
          hilog.info(0x0000, 'testTag', 'touch up on Row');
        }
        if (event.type === TouchType.Move) {
          hilog.info(0x0000, 'testTag', 'touch move on Row');
        }
      })
    }
    .width('100%')
  }
}

上面的代码打印的日志为:
10-24 08:00:03.946 17467-3079/com.bytedance.demoapp I 00000/testTag: touch down on Text
10-24 08:00:03.946 17467-3079/com.bytedance.demoapp I 00000/testTag: touch down on Row
10-24 08:00:04.036 17467-3079/com.bytedance.demoapp I 00000/testTag: touch up on Text
10-24 08:00:04.036 17467-3079/com.bytedance.demoapp I 00000/testTag: touch up on Row
加上event.stopPropagation()后,打印的日志为:
10-24 08:03:02.472 17993-3219/com.bytedance.demoapp I 00000/testTag: touch down on Text
10-24 08:03:02.596 17993-3219/com.bytedance.demoapp I 00000/testTag: touch up on Text

2. 键鼠事件

可以响应以下鼠标事件:

onHover(event: (isHover: boolean) => void)鼠标进入或退出组件时触发该回调。isHover:表示鼠标是否悬浮在组件上,鼠标进入时为true, 退出时为false。
onMouse(event: (event?: MouseEvent) => void)当前组件被鼠标按键点击时或者鼠标在组件上悬浮移动时,触发该回调,event返回值包含触发事件时的时间戳、鼠标按键、动作、鼠标位置在整个屏幕上的坐标和相对于当前组件的坐标。

键盘事件通过onKeyEvent接收,回调参数为KeyEvent,可由该参数获得当前按键事件的按键行为(KeyType)、键码(keyCode)、按键英文名称(keyText)、事件来源设备类型(KeySource)、事件来源设备id(deviceId)、元键按压状态(metaKey)、时间戳(timestamp)、阻止冒泡设置(stopPropagation):

onKeyEvent(event: (event?: KeyEvent) => void)

3. 焦点事件

onFocus(event: () => void)

获焦事件回调,绑定该API的组件获焦时,回调响应。

onBlur(event:() => void)

失焦事件回调,绑定该API的组件失焦时,回调响应。

  1. 手势处理

可以使用以下接口给组件绑定手势:

.gesture(gesture: GestureType, mask?: GestureMask)

Column() {
      Text('Gesture').fontSize(28)
        // 采用gesture手势绑定方法绑定TapGesture
        .gesture(
          TapGesture()
            .onAction(() => {
              console.info('TapGesture is onAction');
            }))
    }

// 带优先级的手势绑定,在默认情况下,当父组件和子组件使用gesture绑定同类型的手势时,子组件优先识别通过gesture绑定的手势。
// 当父组件使用priorityGesture绑定与子组件同类型的手势时,父组件优先识别通过priorityGesture绑定的手势。
.priorityGesture(gesture: GestureType, mask?: GestureMask)。
// parallelGesture(并行手势绑定方法),可以在父子组件上绑定可以同时响应的相同手势。
.parallelGesture(gesture: GestureType, mask?: GestureMask)

上述例子中绑定的TapGesture是一种单击的单一手势,除此之外还有长按手势(LongPressGesture)、拖动手势(PanGesture)、捏合手势(PinchGesture)、旋转手势(RotationGesture)、滑动手势(SwipeGesture)。

除了单一手势外,还可以对手势进行组合使用

GestureGroup(mode:GestureMode, ...gesture:GestureType[])

组合手势中可以设置GestureMode以顺序识别(GestureMode.Sequence)、并行识别(GestureMode.Sequence)、互斥识别(GestureMode.Sequence)三种方式进行组合。例如顺序组合长按+拖动的手势,可以实现长按拖动某个组件的效果。

2.4.8 页面路由

Router模块提供了两种跳转模式,分别是router.pushUrl()router.replaceUrl()。这两种模式决定了目标页是否会替换当前页。

  • router.pushUrl():目标页不会替换当前页,而是压入页面栈。这样可以保留当前页的状态,并且可以通过返回键或者调用router.back()方法返回到当前页。
  • router.replaceUrl():目标页会替换当前页,并销毁当前页。这样可以释放当前页的资源,并且无法返回到当前页。

页面栈的最大容量为32个页面。如果超过这个限制,可以调用router.clear()方法清空历史页面栈,释放内存空间

跟UIAbility的启动模式类似,Router模块提供了两种实例模式,分别是Standard和Single,调用pushUrl时也可以传递参数,在目标页中,可以通过调用Router模块的getParams()方法来获取传递过来的参数

router.pushUrl({
    url: 'pages/Detail', // 目标url
    params: paramsInfo // 添加params属性,传递自定义参数
  }, (err) => {
    if (err) {
      console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`);
      return;
    }
    console.info('Invoke pushUrl succeeded.');
  })

鸿蒙系统上实现页面的切换有两种方式,第一种是通过跳转UIAbility来实现,第二种就是在UIAbility内部通过跳转不同的page实现,这一点上,UIAbility与Page的关系有点类似于安卓中Activity与Fragment之间的关系。

2.5 异步、并发&后台任务

ArkTS异步(Promise、async、await关键字)

Promise有三种状态:pending(进行中)、fulfilled(已完成)和rejected(已拒绝)。Promise对象创建后处于pending状态,并在异步操作完成后转换为fulfilled或rejected状态。

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const randomNumber = Math.random();
    if (randomNumber > 0.5) {
      resolve(randomNumber);
    } else {
      reject(new Error('Random number is too small'));
    }
  }, 1000);
});

promise.then(result => {
  console.info(`Random number is ${result}`);
}).catch(error => {
  console.error(error.message);
});

await和async是Promise的语法糖,可以直接从返回值中读取结果,而不需要用回调的方式:

async function myAsyncFunction() {
  const result = await new Promise((resolve) => {
    setTimeout(() => {
      resolve('Hello, world!');
    }, 3000);
  });
  console.info(String(result)); // 输出: Hello, world!
}

myAsyncFunction();

TaskPool与Worker

差异对比:

实现TaskPoolWorker
内存模型线程间隔离,内存不共享。线程间隔离,内存不共享。
参数传递机制采用标准的结构化克隆算法(Structured Clone)进行序列化、反序列化,完成参数传递。支持ArrayBuffer转移和SharedArrayBuffer共享。采用标准的结构化克隆算法(Structured Clone)进行序列化、反序列化,完成参数传递。支持ArrayBuffer转移和SharedArrayBuffer共享。
参数传递直接传递,无需封装,默认进行transfer。消息对象唯一参数,需要自己封装。
方法调用直接将方法传入调用。在Worker线程中进行消息解析并调用对应方法。
返回值异步调用后默认返回。主动发送消息,需在onmessage解析赋值。
生命周期TaskPool自行管理生命周期,无需关心任务负载高低。开发者自行管理Worker的数量及生命周期。
任务池个数上限自动管理,无需配置。(工作线程数上限是4个)同个进程下,最多支持同时开启8个Worker线程。
任务执行时长上限无限制。(文档里说超过3分钟的超长任务会被自动回收,暂不明确以哪个为准)无限制。
设置任务的优先级不支持。不支持。
执行任务的取消支持取消任务队列中等待的任务。不支持。
使用限制无限制Ability类型的Module支持使用Worker,Library类型的Module不支持使用Worker。创建Worker不支持使用其他Module的Worker.ts文件,即不支持跨模块调用Worker

用法:

CPU密集型任务开发指导-使用多线程并发能力进行开发-并发-ArkTS语言基础类库-开发

后台任务

后台任务指的是应用进入后台后(Home、锁屏以及切换应用等操作),允许开发者在一定限制下执行一些操作,后台任务包括:

  • 短时任务:应用在退至后台五秒内申请,最多只能有3个短时任务,有配额限制,配额默认单日为10分钟,单次最大3分钟,配额会根据手机状态和用户习惯调整。适合执行短耗时任务
  • 长时任务:后台执行用户可感知的任务,例如播放音乐、导航等,同时系统有展示与长时任务相关的通知栏消息,用户删除通知栏消息任务也停止。在启动长时任务时需要传入对应的BackgroundMode告诉系统你想执行的任务类型
  • 延迟任务:执行实时性要求不高的任务,一个应用最多只能申请10个延迟任务。通过WorkSchedulerExtensionAbility实现。开发中定义的WorkSchedulerExtensionAbility跟UIAbility一样,需要在module.json5文件中注册。满足调度条件时,系统会回调onWorkStart方法
export default class MyWorkSchedulerExtensionAbility extends WorkSchedulerExtensionAbility {
  // 延迟任务开始回调
  onWorkStart(workInfo: workScheduler.WorkInfo) {
    console.info(`onWorkStart, workInfo = ${JSON.stringify(workInfo)}`);
  }

  // 延迟任务结束回调
  onWorkStop(workInfo: workScheduler.WorkInfo) {
    console.info(`onWorkStop, workInfo is ${JSON.stringify(workInfo)}`);
  }
}
  • 代理提醒:提醒用户的定时类任务,例如提醒会议日程等,有倒计时、日历和闹钟三种,普通应用最多支持30个提醒

2.6网络请求和跨进程通信

  1. 网络请求

HTTP请求
  1. 注册网络权限ohos.permission.INTERNET
  2. 调用createHttp()接口创建一个httpRequest
  3. 调用on(type: 'headersReceive')订阅http响应头事件(根据业务需要,可选)
  4. 调用httpRequest.request()方法,传入请求的相关参数,包括URL、method、header、数据类型和数据体、超时时间以及Http协议版本,并订阅响应的回调
  5. 调用该对象的off(type: 'headersReceive')方法,取消订阅http响应头事件。(如果第3步有注册,就传入对应的callback反注册,或者传空取消所有的)
  6. 当该请求使用完毕时,调用destroy()方法主动销毁。(待验证,这个方法是否能取消请求)
WebSocket请求
  1. 通过createWebSocket()创建一个WebSocket对象
  2. 通过on()接口订阅open、message、close、error事件(根据需求可选订阅)
  3. 调用connect发起连接
  4. 调用close关闭连接
Socket连接

developer.harmonyos.com/cn/docs/doc…

  1. 跨进程通信

也是用的binder,也有1MB的的大小限制。既然是用的binder,那必然也需要定义接口、服务端的实现,客户端通信时,要获取服务端的本地代理,使用代理进行通信:

Binder通信的鸿蒙新衣

从功能上来说,最大的区别,是鸿蒙系统扩展了原来的通信模式,在底层扩展了一个软****总线。软总线用于跨设备之间的通信,可以在多个华为设备之间通信,比如手表、平板、电脑、汽车等。在实现中,软总线只是在底层的通信层分叉了,上层只要传入一个不同的参数就可以把IPC变成RPC,上层代码完全复用且无需关心细节

2.7 数据管理

  1. 首选项(Preference)

跟安卓的Preference类似,内存缓存+持久化的方式,键值对形式,键为长度不超过80字节的String类型,如果值为String类型,长度不超过8192字节。用法通过getPreferences获取Preferences实例,再调用:put写入、delete移除、get获取、has检车是否有、flush写入持久化,on监听key对应的值变化,off取消监听。

  1. 键值型数据库

键值型数据库分为单版本和设备协同两种数据库。使用限制:

  • 设备协同数据库,针对每条记录,Key的长度≤896 Byte,Value的长度<4 MB。
  • 单版本数据库,针对每条记录,Key的长度≤1 KB,Value的长度<4 MB。
  • 接口回调中禁止执行阻塞操作
接口名称描述
createKVManager(config: KVManagerConfig): KVManager创建一个KVManager对象实例,用于管理数据库对象。
getKVStore(storeId: string, options: Options, callback: AsyncCallback): void指定Options和storeId,创建并得到指定类型的KVStore数据库。
put(key: string, value: Uint8Arraystringnumberboolean, callback: AsyncCallback): void添加指定类型的键值对到数据库。
get(key: string, callback: AsyncCallback<Uint8Arraystringbooleannumber>): void获取指定键的值。
delete(key: string, callback: AsyncCallback): void从数据库中删除指定键值的数据。
  1. 关系型数据库

基于SQLite

接口名称描述
getRdbStore(context: Context, config: StoreConfig, callback: AsyncCallback): void获得一个相关的RdbStore,操作关系型数据库,用户可以根据自己的需求配置RdbStore的参数,然后通过RdbStore调用相关接口可以执行相关的数据操作。
executeSql(sql: string, bindArgs: Array, callback: AsyncCallback):void执行包含指定参数但不返回值的SQL语句。
insert(table: string, values: ValuesBucket, callback: AsyncCallback):void向目标表中插入一行数据。
update(values: ValuesBucket, predicates: RdbPredicates, callback: AsyncCallback):void根据RdbPredicates的指定实例对象更新数据库中的数据。
delete(predicates: RdbPredicates, callback: AsyncCallback):void根据RdbPredicates的指定实例对象从数据库中删除数据。
query(predicates: RdbPredicates, columns: Array, callback: AsyncCallback):void根据指定条件查询数据库中的数据。
deleteRdbStore(context: Context, name: string, callback: AsyncCallback): void删除数据库。
  1. 数据的可靠性与安全性

备份:键值型数据库与关系型数据库支持备份与恢复,键值型数据库还支持删除备份。具体使用方法为KVStore与RdbStore实例中调用对应的backup和restorefangfa

加密:键值型数据库支持在getKVStore中指定options参数中配置是否加密

let kvStore;
try {
  const options = {
    createIfMissing: true,
    // 设置数据库加密
    encrypt: true,
    backup: false,
    kvStoreType: distributedKVStore.KVStoreType.SINGLE_VERSION,
    securityLevel: distributedKVStore.SecurityLevel.S2
  };
  kvManager.getKVStore('storeId', options, (err, store) => {
    if (err) {
      console.error(`Fail to get KVStore. Code:${err.code},message:${err.message}`);
      return;
    }
    console.info('Succeeded in getting KVStore.');
    kvStore = store;
  });
} catch (e) {
  console.error(`An unexpected error occurred. Code:${e.code},message:${e.message}`);
}

关系型数据库也是在getRdbStore对应的参数中配置是否加密:

import relationalStore from '@ohos.data.relationalStore';

let store;
let context = getContext(this);
const STORE_CONFIG = {
  name: 'RdbTest.db',
  securityLevel: relationalStore.SecurityLevel.S1,
  encrypt: true
};
relationalStore.getRdbStore(context, STORE_CONFIG, (err, rdbStore) => {
  store = rdbStore;
  if (err) {
    console.error(`Failed to get RdbStore. Code:${err.code},message:${err.message}`);
    return;
  }
  console.info(`Succeeded in getting RdbStore.`);
})

2.8 分布式跨设备迁移

业务“跨设备迁移”的本质即通过分布式组网把一个设备的“Ability运行状态”迁移到另外一台设备上。

官方CodeLab中展示了一个新闻客户端的示例,浏览到一半的新闻可以迁移到另外一个设备上继续阅读:

要实现跨设备迁移需要通过设备管理器发现可信的设备并进行认证,完成后通过startAbility,并且在want中指定对应的deviceId、要启动的应用以及页面、场景数据,完成跨设备的迁移

创建设备管理器
async createDeviceManager(context: common.UIAbilityContext): Promise<void> {
  if (this.deviceManager !== undefined) {
      return;  
  }
  await new Promise((resolve: (value: Object | PromiseLike<Object>) => void, reject:   
   ((reason?: RejectError) => void)) => {    
   deviceManager.createDeviceManager(context.abilityInfo.bundleName, (err, value) => {      
       if (err) {        
           reject(err);      
           logger.error('createDeviceManager failed.');        
           return;      
       }      
       this.deviceManager = value;      
       // 注册设备状态监听      
       this.registerDeviceStateListener();      
       // 获取信任设备列表      
       this.getTrustedDeviceList();      
       resolve(value);    
    })  
  })
}
发现设备

用户点击新闻详情页底部的分享按钮,调用startDeviceDiscovery()方法,发现周边处在同一无线网络下的设备并添加设备至已发现的设备列表。

// RemoteDeviceModel.ets
startDeviceDiscovery(): void {  
    if (this.deviceManager === undefined) {    
        logger.error('deviceManager has not initialized');    
        this.showToast($r('app.string.no_device_manager')); 
        return; 
   }  
   this.deviceManager.on('deviceFound', (data) => {    
       if (data === null) {      return;    }    
       // 监听设备发现
       this.deviceFound(data);
   })  
   this.deviceManager.on('discoverFail', (data) => {    
       logger.error(`discoverFail data = ${JSON.stringify(data)}`);  
   })
   this.deviceManager.on('serviceDie', () => {
       logger.error('serviceDie');  
   })  
   let info: deviceManager.SubscribeInfo = {    
       subscribeId: SUBSCRIBE_ID,    
       mode: CommonConstants.INFO_MODE,    
       medium: 0,    
       freq: CommonConstants.INFO_FREQ,    
       isSameAccount: false,    
       isWakeRemote: true,    
       capability: 0  
   };  
   // 添加设备至发现列表  
   this.discoverList = [];  
   AppStorage.setOrCreate(CommonConstants.DISCOVER_DEVICE_LIST, this.discoverList);  
   try {
       this.deviceManager.startDeviceDiscovery(info);  
   } catch (err) {
       logger.error(`startDeviceDiscovery failed error = ${JSON.stringify(err)}`); 
   }
}
进行可信认证连接

在已发现的设备列表中选择设备,调用deviceManager.authenticateDevice()方法进行可信认证,用户输入PIN码,连接设备,将设备改为信任状态,添加至已信任设备列表。

跨设备启动UIAbility

可信认证后,用户再次点击分享按钮,选择已信任设备列表中的设备,调用startAbilityContinuation()方法进行拉起应用,在另一设备中触发aboutToAppear()方法渲染当前的新闻详情页,实现跨设备启动UIAbility。

function startAbilityContinuation(deviceId: string, newsId: string, context: common.UIAbilityContext): void {
  let want: Want = {
      deviceId: deviceId,    
      bundleName: context.abilityInfo.bundleName,    
      abilityName: CommonConstants.ABILITY_NAME,    
      parameters: {
            newsId: newsId    
      }
   };  
   // 拉起应用  
   context.startAbility(want).catch((err: Error) => {    
       Logger.error(`startAbilityContinuation failed error = ${JSON.stringify(err)}`);    
       prompt.showToast({
             message: $r('app.string.start_ability_continuation_error')    
       });  
   })
}

// NewsDetail.ets
aboutToAppear() {
  let newsId: string | undefined = AppStorage.get<string>('wantNewsId');  
  if (newsId === undefined) {
      this.newsData = (router.getParams() as Record<string, NewsData>)['newsItem'];  
      return;  
  }  
  // 读取跨设备传递的参数信息  
  this.newsData = this.newsItems.filter((item: NewsData) => (item.newsId === newsId))[0];}
跨设备文件访问

跨设备文件访问即应用的文件允许其他设备上的同应用进行访问,例如在手机上保存的音乐,在车机或者平板上可以直接播放,这个功能是基于鸿蒙系统分布式文件系统的能力实现的

应用要实现跨设备文件访问能力只需要通过基础的文件接口,把文件放在分布式文件路径即可(/data/storage/el2/distributedfiles/),当然,访问的前提是设备已经完成认证和组网。

// A设备上写入
let context = ...; // 获取设备A的UIAbilityContext信息
let pathDir = context.distributedFilesDir;
// 获取分布式目录的文件路径
let filePath = pathDir + '/test.txt';

try {
  // 在分布式目录下创建文件
  let file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
  console.info('Succeeded in createing.');
  // 向文件中写入内容
  fs.writeSync(file.fd, 'content');
  // 关闭文件
  fs.closeSync(file.fd);
} catch (err) {
  console.error(`Failed to openSync / writeSync / closeSync. Code: ${err.code}, message: ${err.message}`);
}


// B设备进行读取
let context = ...; // 获取设备B的UIAbilityContext信息
let pathDir = context.distributedFilesDir;
// 获取分布式目录的文件路径
let filePath = pathDir + '/test.txt';

try {
  // 打开分布式目录下的文件
  let file = fs.openSync(filePath, fs.OpenMode.READ_WRITE);
  // 定义接收读取数据的缓存
  let buffer = new ArrayBuffer(4096);
  // 读取文件的内容,返回值是读取到的字节个数
  let num = fs.readSync(file.fd, buffer, {
    offset: 0
  });
  // 打印读取到的文件数据
  console.info('read result: ' + String.fromCharCode.apply(null, new Uint8Array(buffer.slice(0, num))));
} catch (err) {
  console.error(`Failed to openSync / readSync. Code: ${err.code}, message: ${err.message}`);
}

2.9 构建与产物

构建工具

API 4-7版本项目构建采用的Gradle进行构建,最新的构建工具是Hvigor,是一款全新基于TS实现的前端构建任务编排工具,结合npm包管理机制,主要提供任务管理机制,任务注册编排、工程模型管理、配置管理等关键能力

构建插件hvigor-ohos-plugin:是基于Hvigor构建工具开发的一个插件,利用Hvigor的任务编排机制实现应用/服务构建任务流的执行,完成HAP/APP的构建打包,应用于应用/服务的构建。

依赖管理

鸿蒙应用通过ohpm(OpenHarmony Package Manager,ohpm.openharmony.cn/#/cn/home) 安装、分发和共享代码。ohpm包的依赖一般包括以下三种:ohpm原生三方包、ohpm三方共享包和ohpm本地共享模块,开发者可在工程或模块下的oh-package.json5中进行配置:

"dependencies": {
  "eslint": "^7.32.0", // 原生三方包依赖,这个应该指的是原生npm管理的包,但是按照文档里面的配置会报错,还需要看下是什么原因
  "@ohos/lottie": "^2.0.0", // 三方共享包
  "library": "file:../library", // 本地共享模块
}

依赖配置完成后,请在Terminal窗口执行ohpm install命令下载依赖包,或者点击Sync Now进行同步。

AOT编译模式

当前仅支持API 9及以上版本Stage模型的ArkTS工程。

Node.js需要10.14以上版本。

仅支持在64位ROM上运行。

AOT编译有两种模式,buildOption内的aotCompileMode字段可以设置为以下值,对应不同的AOT模式:

type默认模式,仅编译类型信息到字节码文件,编译速度最快。
partial使用记录高频操作信息的ap文件(Arkcompiler Profile)进行部分编译,编译速度较快。

当aotCompileMode设置为partial时,需要设置apPath

{
  "apiType": 'stageMode',
  "buildOption": {
    "aotCompileMode": "partial",
    "apPath":"./modules.ap"
  },
  ...
}

ap文件是先在真机上操作应用,记录该应用用户常用操作后生成,作为AOT编译的优化信息的输入(猜测可能类似虚拟机在JIT过程中的Profiling数据),具体生成ap文件的过程可参考开启AOT编译模式-编译构建-DevEco Studio使用指南-工具

获得性能的同时,代价是什么?实测打开AOT编译模式时,包大小会增加。以codelab中Healthy-life为例,打开AOT编译模式并设置为type后编译的hap文件解压后modules.abc大小会从318KB增加到464KB

三、思考:鸿蒙APP开发架构与Android、iOS会有什么差异么?

页面架构

MVC?MVP?MVVM

组件化

鸿蒙项目如何组件化?

热修&插件化

是否支持热修和插件化

鸿蒙的跨平台

ArkUI-X: ArkUI-X扩展ArkUI开发框架到多个OS平台, 让开发者基于一套主代码, 就可以构建支持多平台的精美、高性能应用。The ArkUI-X project extends the

OpenHarmony-SIG/flutter_flutter

四、实战示例

developer.huawei.com/consumer/cn…

gitee.com/openharmony…