鸿蒙开发速通(一)

ArkTS

在ArkTS中,类的字段必须在声明的时候或是在构造函数中显示初始化。类似于typescriptstrictPropertyInitialization模式。

在ArkTS中必不可少的是Struct。在日常开发中会经常用到,主要处理界面。

页面和组件

在默认生成的页面代码是这样的:

typescript 复制代码
@Entry                  // 1
@ComponentV2            // 2
struct Index {          // 3
  @Local message: string = 'Hello World';   // 4

  build() {
    RelativeContainer() {    // 5
      Text(this.message)     // 6
        .id('HelloWorld')    // 7
        .fontSize($r('app.float.page_text_font_size'))
        .fontWeight(FontWeight.Bold)
        .alignRules({
          center: { anchor: '__container__', align: VerticalAlign.Center },
          middle: { anchor: '__container__', align: HorizontalAlign.Center }
        })
        .onClick(() => {     // 8
          this.message = 'Welcome';
        })
    }
    .height('100%')
    .width('100%')
  }
}
  1. @Entry:装饰器,表示这是一个页面(入口组件)
  2. @ComponentV2:表示自定义组件,更重要的是在组件内部使用状态管理V2版本。@Component对应的就是状态管理V1。现在推荐使用的是状态管理V2,不过参考代码大部分都是V1的。本文中使用的都是状态管理的V2版本。
  3. Struct,自定义组件。自定义组件就是@ComponentStruct的组合。
  4. @Local,组件内部的状态,这里是message。在用户点击了界面上的Hello World 文本之后会显示message的内容。
  5. RelativeContainer,相对布局。在alignRules里定义了布局的规则。鸿蒙使用声明式UI来实现组件开发和布局。
  6. Text,是文本组件。
  7. id('xxxxxx'),属性方法,后面出现的fontSize()alignRules()也是属性方法。
  8. onClick,事件方法,用来响应Text组件的点击事件。

自定义组件

如上所述,鸿蒙试用报告声明式UI开发组件。

一个简单的例子:

typescript 复制代码
Column() {
    Text("Hello Bro")
    // ...
}

这里包含了一个Column布局和一个Text组件。

正式自定义一个简单的组件:

tyepscript 复制代码
@Preview
@ComponentV2
export struct ThemedButton {
  @Param message: string = "Hello ThemedButton";

  build() {
    Row() {
      Text(this.message);
    }
    .justifyContent(FlexAlign.Center)
    .alignItems(VerticalAlign.Center)
    .width('100%')
    .height(60)
    .padding(20)
    .borderColor(Color.Blue)
    .borderWidth(1)
    .borderRadius(30)
  }
}

看起来效果是这样的:

在自定义组件的时候可以给组件加一个@Preview的注解,这样可以在IDE的Previewer里看到组件的效果。这样就不需要反复的运行项目才能看到效果了。

@Param是状态管理的一部分,会在后面的状态管理节点细讲。

组件作为参数

使用@BuildParam装饰参数,参数的类型是:() => void。参数可以根据要传入的组件定义。 完成代码:

typescript 复制代码
@Preview
@ComponentV2
export struct Card {
  // ...略...
  @BuilderParam renderContent?: () => void; // 1

  build() {
    Stack({ alignContent: Alignment.TopStart }) {
      Column() {
        if (this.renderContent) {
          this.renderContent()  // 2
        } else {
          Text(this.message)
            .height(20)
            .width(300)
            .align(Alignment.Center)
        }
      }
      // ...略...
    }
    // ...略...
  }
}
  1. 使用@BuildParam装饰要传入的组件,类型是() => void
  2. 在组件的build方法中使用传入的组件。

在其他组件中这样使用:

typescript 复制代码
build() {
    // ...略...
    Column() {
      Card({ title: 'Task pool', message: this.message}) {
        this.renderTaskpool()  // *
      }
    }
  // ...略...

在标记星号的行是一个尾随闭包。换成常规写法是:

typescript 复制代码
Card({ title: 'Task pool', message: this.message, renderContent: this.renderTaskpool()})

复用样式

把几个标准样式放在一起可以定义一个可以复用的样式。

定义一个全局样式

typescript 复制代码
@Styles
function themedBorder() {
  .borderWidth(1)
  .borderColor(Color.Gray)
}

定义一个组件内样式

typescript 复制代码
@Styles
matchParent() {
  .width('100%')
  .height('100%')
}

使用的方法都一样,和标准的属性方法一样:

typescript 复制代码
Column() {
  Text(this.message)
    .height(20)
    .width(300)
    .align(Alignment.Center)
}
.matchParent()
.themedBorder()

renderProps

HOC

组件的生命周期

文档在这里

组件的生命周期: aboutToAppear -> build -> onDidBuild -> aboutToDisappear

  • aboutToAppearbuild方法之前执行,在其中执行初始化组件的任务。不要执行耗时的任务。可以修改状态变量,会在build中起作用。
  • aboutToDisappear在组件销毁前执行,可以在其中执行资源的回收。不可以修改状态变量。

页面的生命周期

页面 就是有@Entry装饰的自定义组件。

所以上面说到的自定义组件的生命周期方法都会被调用。额外的还增加了三个生命周期方法: onPageShowonPageHideonBackPress。最后的生命周期方法调用是这样的:

弹窗

List

Grid

布局

Stack

层叠布局,看起来是这样式儿的

代码是这样的:

typescript 复制代码
Stack({ alignContent: Alignment.BottomEnd }) { 
  Text('Layer 1')
    .width('100%')
    .height('100%')
    .backgroundColor('#FFE66D')
    .textAlign(TextAlign.Center)
    .fontColor(Color.Black)

  Text('Layer 2')
    .width(150)
    .height(150)
    .backgroundColor('#FF6B6B')
    .textAlign(TextAlign.Center)
    .fontColor(Color.White)

  Text('Layer 3')
    .width(80)
    .height(80)
    .backgroundColor('#4ECDC4')
    .textAlign(TextAlign.Center)
    .fontColor(Color.White)
}
.width('100%')
.height(200)
.borderRadius(8)
.margin({ top: 20, bottom: 12 })

把颜色,大小等属性方法都删掉,看看关于Stack最重要的部分:

typescript 复制代码
Stack({ alignContent: Alignment.BottomEnd }) {   ///*
  Text('Layer 1')

  Text('Layer 2')

  Text('Layer 3')
}

这里最重要的就是在初始化Stack的时候的参数:alignContentAlignemnt枚举有几个不同的值,分别制定了Stack内的组件的排列顺序。如图:

Flex

和H5的flex基本类似,只是写法换了一下。比如,flex最核心的flex direction和justify content和align items的作用都一样。只是给定值的时候用了鸿蒙自定义的枚举值。

typescript 复制代码
Flex({
  direction: this.flexDirection,
  justifyContent: this.justifyContent,
  alignItems: this.alignItems
}) {
  Text('Item 1')
    .width(80)
    .height(80)
    .backgroundColor('#FF6B6B')
    .textAlign(TextAlign.Center)
    .fontColor(Color.White)

  Text('Item 2')
    .width(80)
    .height(80)
    .backgroundColor('#4ECDC4')
    .textAlign(TextAlign.Center)
    .fontColor(Color.White)

  Text('Item 3')
    .width(80)
    .height(80)
    .backgroundColor('#45B7D1')
    .textAlign(TextAlign.Center)
    .fontColor(Color.White)
}
.width('100%')
.height(200)
.backgroundColor('#F7F7F7')
.padding(12)

Column和Row

ColumnRow就是Flex这个布局的语法糖。

Column就是FlexDirection的值为Column的时候,Row也一样。

RelativeContainer

这个布局用好了有神奇功效。可以把布局的嵌套减少,提高渲染效率。

看代码:

typescript 复制代码
RelativeContainer() {
  // 左上角元素
  Row()
    // 略
    .id('topLeft')
    .alignRules({        ///*
      top: { anchor: '__container__', align: VerticalAlign.Top },
      left: { anchor: '__container__', align: HorizontalAlign.Start }
    })
    
    // 略
}

RelativeContainer里的组件定位依赖的是相对于哪个组件的定位规则。本例中使用的是anchor: '__container__',也就是相对于容器定位,具体的定位规则是align: VerticalAlign.Top。在父容器的上部分。本例只要给出topleft就可以。

当然,可以相对定位的组件可以是父容器,也可以是容器内地其他组件。或者是参考边界辅助线 等。更多可以参考这里

GridRow/GridCol

这个擅长解决不同屏幕尺寸的适配问题。文档在这里

先看效果:

在折叠屏展开的时候,只显示一行,8列。在折叠屏折叠之后显示2行,4列。

代码:

typescript 复制代码
GridRow({ /// 1
  breakpoints: {  /// 2
    value: ['320vp', '600vp', '840vp', '1440vp',
      '1600vp'], // 表示在保留默认断点['320vp', '600vp', '840vp']的同时自定义增加'1440vp', '1600vp'的断点,实际开发中需要根据实际使用场景,合理设置断点值实现一次开发多端适配。
    reference: BreakpointsReference.WindowSize  /// 3
  },
  columns: { /// 4
    xs: 2, // 窗口宽度落入xs断点上,栅格容器分为2列。
    sm: 4, // 窗口宽度落入sm断点上,栅格容器分为4列。
    md: 8, // 窗口宽度落入md断点上,栅格容器分为8列。
    lg: 12, // 窗口宽度落入lg断点上,栅格容器分为12列。
    xl: 12, // 窗口宽度落入xl断点上,栅格容器分为12列。
    xxl: 12 // 窗口宽度落入xxl断点上,栅格容器分为12列。
  },
}) {
  ForEach(this.bgColors, (color: ResourceColor, index?: number | undefined) => {
    GridCol({ span: 1 }) { // 所有子组件占一列。
      Row() {
        Text(`${index}`)
      }.width('100%').height('50vp')
    }.backgroundColor(color)
  })
}
.height(200)
.border({ color: 'rgb(39,135,217)', width: 2 })
.onBreakpointChange((breakPoint) => {
  this.currentBreakpoint = breakPoint
})
  1. 基本的结构就是外面是GridRow里面是GridCol
  2. breakpoints,也就是断点。其实更适合叫触发点。这里的value定义了一个数组。这里的值定义了屏幕宽度的触发点。屏幕的宽度到了某个值的范围后就会触发一个动作。这个动作在columns定义。
  3. GridRow监听的是哪个组件的宽度,这里是Window的宽度。
  4. columns定义的就是每个宽度对应要显示几列。比如屏幕宽度在xs的时候显示两列,sm宽度显示4列,等。也可以直接给定列数值,那么不管屏幕的宽度如何变化列数也就只显示给定的列数。

默认情况

  1. API version 20之前,columns显示12列。没有设置columns的话,任何断点都是显示12列。
  2. API version 20之后,columns默认值为{ xs: 2, sm: 4, md: 8, lg: 12, xl: 12, xxl: 12 }

初识UIAbility

一个应用可以包含一个或者多个UIAbility。一个UIAbility可以在最近任务中作为一个任务显示。一个Ability可以包含一组界面。所以,使用Ability也可以达到避免加载不必要的资源的效果。

Ability的配置

配置文件module.json5在:

css 复制代码
project
└── entry
    └── src
        └── main
            └── ets
                └── module.json5

默认的看起来是这样的:

json 复制代码
{
  "abilities": [
    {
      "name": "EntryAbility",
      "srcEntry": "./ets/entryability/EntryAbility.ets",
      "exported": true,
      // 其他略
    }
  ]
}

在这个默认的配置文件中,已经标示了默认生成的Ability的文件位置,名称等。

UIAbility的基本使用

新建一个UIAbility

我们修改已经有的Todo App,把todo详情改成在一个Ability里显示。

在DevEco Studio中,点New->Ability 就可以新建一个Ability。这个Ability就叫DetailAbility。也可以手动新建一个,不过要自己在module.json5里添加配置。具体方法可以参考上一节。

新建好之后就可以修改之前nav跳转到Detail页面的代码,这里就要唤起新建的这个Ability了:

typescript 复制代码
.onClick(() => {
  this.isShowFloatingButton(false)
  // this.navPathStack.pushPathByName('detail', item) // 这里是nav跳转的

    const want: Want = {
      deviceId: '',
      bundleName: 'com.example.myapplication',
      abilityName: 'DetailAbility',
      parameters: {
        todo: JSON.stringify(item.toModel())
      }
    };

  (this.getUIContext().getHostContext() as common.UIAbilityContext).startAbility(want)  // 使用startAbility唤起`DetailAbility`
})

使用startAbility唤起DetailAbility的时候,还需要一个Want类型的参数。在这里指定了打开的Ability在哪里是谁。deviceId是空的,说明这个Ability在同一个设备

再新建一个DetailPage,目前这个页面只显示了Detail Page文本。稍后更改这个页面。

注意 ,这一步不做页面显示白板。在entry/src/main/resources/profile/main_pages.json文档添加新建的这个页面。

json 复制代码
{
  "src": [
    "pages/Index",
    "pages/DetailPage"
  ]
}

Ability加载的页面都需要在这里添加配置。

但是,这个打不开Detail页面,默认的加载路径不是Detail页面:

javascript 复制代码
onWindowStageCreate(windowStage: window.WindowStage): void {
  // Main window is created, set main page for this ability
  hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');

  windowStage.loadContent('pages/Index', (err) => {  /// ***
    if (err.code) {
      hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
      return;
    }
    hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
  });
}

windowStage.loadContent方法中第一个参数就是页面的位置,自动生成的时候给的是:pages/Index, 但是我们要打开的是Detail页面。修改页面位置为pages/DetailPage就可以打开了。

效果是这样的:

一个Ability,对应在任务栏显示一个任务。这里有两个,默认的一个和新建的DetailAbility。

显示todo详情

现在要使用Want中的parameters了,这里传递了一个todo。但是转成了JSON字符串。而且传递的是一个Model实例。

DetailAbilityonCreate中可以拿到这个want的实例,并从parameters中拿到这个json串。反序列化并使用:

typescript 复制代码
const todo = want?.parameters?.['todo'];

if (!todo) {
  hilog.error(DOMAIN, 'testTag', 'todo is null');
  return;
}

const todoInfo = new TodoViewModel( JSON.parse(todo as string) as TodoModel);
AppStorageV2.connect<TodoViewModel>(TodoViewModel, 'todo_detail', () => todoInfo)

在使用的时候得到model实例,并转成viewmodel后使用。

注意 ,在DetailPage页面的onPageHide生命周期回调中需要关闭这个Ability:

typescript 复制代码
onPageHide(): void {
  (this.getUIContext().getHostContext() as common.UIAbilityContext).terminateSelf()
}

否则,没有办法在点击其他的todo的时候显示对应的todo的title。详情查看后面的UIAbility数据同步章节。

Ability的生命周期

onCreate

onWindowStageCreate

这里注意,加载页面是在onWindowStageCreate这个方法进行的:

typescript 复制代码
onWindowStageCreate(windowStage: window.WindowStage): void {
  hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');

  windowStage.loadContent('pages/Index', (err) => { /// *
    if (err.code) {
      hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
      return;
    }
    hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
  });
}

onWindowStageCreate方法中使用windowStage.loadContent('pages/Index', ()=>{})加载了页面。

也可以在这个方法里订阅获焦/失焦、切到前台/切到后台、前台可交互/前台不可交互等事件。详细文档在这里

onForeground

这是Ability的UI可见之前的最后一个回调,在这里申请需要的系统资源。

typescript 复制代码
import { UIAbility } from '@kit.AbilityKit';
// ···

export default class EntryAbility extends UIAbility {
// ···

  onForeground(): void {
    // 申请系统需要的资源,或者重新申请在onBackground()中释放的资源
  }

// ···
}

onBackground

在Ability的UI完全不见之后,触发onBackground回调。开发者可以在这个回调中释放不需要的系统资源,比如停止定位功能等。

onWindowStageWillDestroy

Ability在销毁之前,系统触发这个回调。这个时候WindowStage还没有销毁,还可以用。

typescript 复制代码
onWindowStageWillDestroy(windowStage: window.WindowStage): void {
  // 释放通过windowStage对象获取的资源
  // 在onWindowStageWillDestroy()中注销WindowStage事件订阅(获焦/失焦、切到前台/切到后台、前台可交互/前台不可交互)
  try {
    if (windowStage) {
      windowStage.off('windowStageEvent');
    }
  } catch (err) {
    let code = (err as BusinessError).code;
    let message = (err as BusinessError).message;
    hilog.error(DOMAIN, 'testTag', `Failed to disable the listener for windowStageEvent. Code is ${code}, message is ${message}`);
  }
}

onWindowStageDestroy

在这里WindowStage还是没有销毁。可以在这里释放UI资源。

onDestroy

UIAbility的最后一个生命周期回调。可以在这里做最后的资源释放,清理等工作。

onNewWant

在Ability实例已经创建,再次调用方法启动这个Ability的时候会触发这个回调。可以在这里更新加载的资源或者数据。

typescript 复制代码
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
// ···

export default class EntryAbility extends UIAbility {
// ···

  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam) {
    // 更新资源、数据
  }
}

UIAbility内数据同步

UIAblity和它内部的页面同步数据的方法有两种,一个是使用AppStorageV2(当然PersistenceV2也可以)和事件的方式。

使用AppStorageV2

typescript 复制代码
export default class DetailAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    const todo = want?.parameters?.['todo'];

    if (!todo) {
      hilog.error(DOMAIN, 'testTag', 'todo is null');
      return;
    }

    const todoInfo = new TodoViewModel(JSON.parse(todo as string) as TodoModel);
    AppStorageV2.connect<TodoViewModel>(TodoViewModel, 'todo_detail', () => todoInfo)
  }
  
  // 其他略
  
}

把want参数的序列化的字符串解析出来后,转成viewmodel然后通过AppStorage实现App全局共享,这样在DetailPage就可以拿到了。

使用EventHub事件的方式

使用EventHubemit方法发出事件,使用EventHubon接收事件。用完之后可以使用off方法取消该事件订阅。

在Ability里接收,在页面发出。

接收:

typescript 复制代码
import { hilog } from '@kit.PerformanceAnalysisKit';
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
// ···

const DOMAIN = 0x0000;
const TAG: string = '[EventAbility]';

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // 获取eventHub
    let eventhub = this.context.eventHub;
    // 执行订阅操作
    eventhub.on('event1', this.eventFunc);
    eventhub.on('event1', (data: string) => {
      // 触发事件,完成相应的业务操作
    });
    hilog.info(DOMAIN, TAG, '%{public}s', 'Ability onCreate');
  }

  eventFunc(argOne: object, argTwo: object): void {
    hilog.info(DOMAIN, TAG, '1. ' + `${argOne}, ${argTwo}`);
    return;
  }

// ···
}

发出:

typescript 复制代码
import { common } from '@kit.AbilityKit';

@Entry
@Component
struct EventHubPage {
  private context = this.getUIContext().getHostContext() as common.UIAbilityContext;

  eventHubFunc(): void {
    // 不带参数触发自定义"event1"事件
    this.context.eventHub.emit('event1');
    // 带1个参数触发自定义"event1"事件
    this.context.eventHub.emit('event1', 1);
    // 带2个参数触发自定义"event1"事件
    this.context.eventHub.emit('event1', 2, 'test');
    // 开发者可以根据实际的业务场景设计事件传递的参数
  }

  build() {
    Column() {
      List({ initialIndex: 0 }) {
        ListItem() {
          Row() {
            // ···
          }
          .onClick(() => {
            this.eventHubFunc();
            this.getUIContext().getPromptAction().showToast({
              message: 'EventHubFuncA'
            });
          })
        // ···
        }

        ListItem() {
          Row() {
            // ···
          }
          .onClick(() => {
            this.context.eventHub.off('event1');
            this.getUIContext().getPromptAction().showToast({
              message: 'EventHubFuncB'
            });
          })
        // ···
        }
      }
    // ···
    }
    // ···
  }
}

Ability之间跳转

文档在这里

Ability的冷启动和热启动:

  • 冷启动,就是这个Ability在内存中不存在。这次启动需要完成Ability的初始化和启动的动作。
  • 热启动,这个Ability已经存在于内存中,但是不可见。启动的时候不需要在执行初始化的逻辑,只会触发onNewWant生命周期方法。

启动一个Ability

typescript 复制代码
const want: Want = {
  deviceId: '', // 1
  bundleName: 'com.example.myapplication',
  moduleName: 'entry',  // 2
  abilityName: 'DetailAbility',
  parameters: {
    todo: JSON.stringify(item.toModel())
  }
};
(this.getUIContext().getHostContext() as common.UIAbilityContext).startAbility(want)

接收这些want的数据

  1. deviceId,这里是空,表示本设备
  2. 指定moduleName,非必需。
typescript 复制代码
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  const todo = want?.parameters?.['todo'];

  if (!todo) {
    hilog.error(DOMAIN, 'testTag', 'todo is null');
    return;
  }

  const todoInfo = new TodoViewModel(JSON.parse(todo as string) as TodoModel);
  AppStorageV2.connect<TodoViewModel>(TodoViewModel, 'todo_detail', () => todoInfo)
}

启动一个Ability并获得返回结果

这次使用startAbilityForResult,这个方法可以打开一个Ability并获得返回结果。它返回一个Promise,所以获取返回结果需要用到then,或者使用await

代码:

typescript 复制代码
// 略

@Entry
@Component
struct MainPage {
  private context = this.getUIContext().getHostContext() as common.UIAbilityContext;

  build() {
    Column() {
      List({ initialIndex: 0, space: 8 }) {

        // ···

        ListItem() {
          Row() {
            // ···
          }
          .onClick(() => {
            let context = this.getUIContext().getHostContext() as common.UIAbilityContext; // UIAbilityContext
            const RESULT_CODE: number = 1001;   // 1
            let want: Want = {
              deviceId: '', // deviceId为空表示本设备
              bundleName: 'com.samples.uiabilityinteraction',
              moduleName: 'entry', // moduleName非必选
              abilityName: 'FuncAbilityA',
              parameters: {
                // 自定义信息
                // app.string.main_page_return_info资源文件中的value值为'来自EntryAbility MainPage页面'
                info: $r('app.string.main_page_return_info')
              }
            };
            context.startAbilityForResult(want).then((data) => { // 2
              if (data?.resultCode === RESULT_CODE) {
                // 解析被调用方UIAbility返回的信息
                let info = data.want?.parameters?.info;
                hilog.info(DOMAIN_NUMBER, TAG, JSON.stringify(info) ?? '');
                if (info !== null) {
                  this.getUIContext().getPromptAction().showToast({
                    message: JSON.stringify(info)
                  });
                }
              }
              hilog.info(DOMAIN_NUMBER, TAG, JSON.stringify(data.resultCode) ?? '');
            }).catch((err: BusinessError) => { // 3
              hilog.error(DOMAIN_NUMBER, TAG, `Failed to start ability for result. Code is ${err.code}, message is ${err.message}`);
            });
          })
        }

        // ···
      }
    // ···
    }
    // ···
  }
}
  1. 设置一个返回结果的唯一标识,别接受了别的Ability的返回结果。
  2. 开始一个Ability并处理返回值:startAbilityForResult(want).then((data) => {}
  3. 处理异常。

但是被打开的Ability怎么把结果返回回去呢,使用terminateSelfWithResult()方法。代码:

typescript 复制代码
const RESULT_CODE: number = 1001; // FuncAbilityA返回的结果
let abilityResult: common.AbilityResult = {
  resultCode: RESULT_CODE,
  want: {
    bundleName: 'com.samples.uiabilityinteraction',
    moduleName: 'entry', // moduleName非必选
    abilityName: 'FuncAbilityA',
    parameters: {
      // app.string.ability_return_info资源文件中的value值为'来自FuncAbility Index页面'
      info: $r('app.string.ability_return_info')
    },
  },
};
context.terminateSelfWithResult(abilityResult, (err) => {
  if (err.code) {
    hilog.error(DOMAIN_NUMBER, TAG, `Failed to terminate self with result. Code is ${err.code}, message is ${err.message}`);
    return;
  }
});

状态管理

状态管理上,有V1和V2两个版本。推荐使用的是V2版本。在具体的开发中结合MVVM模式使用。

V1

V2

以下的装饰器都只能在ComponentV2装饰的组件内部使用。@ObservedV2@Trace除外。

@Local

只在组件内部使用。

typescript 复制代码
@Entry
@ComponentV2
struct Index {
  // 点击的次数
  @Local count: number = 0;
  @Local message: string = 'Hello';
  @Local flag: boolean = false;

  build() {
    Column() {
      Text(`${this.count}`)
      Text(`${this.message}`)
      Text(`${this.flag}`)
      Button('change Local')
        .onClick(() => {
          // 当@Local装饰简单类型时,能够观测到对变量的赋值
          this.count++;
          this.message += ' World';
          this.flag = !this.flag;
        })
    }
  }
}

@Param

接收付组件传入的值。

typescript 复制代码
// 子组件
@ComponentV2
struct Child {
  @Param count: number = 0;         /// 1
  @Require @Param message: string;
  @Require @Param flag: boolean;

  build() {
    Column() {
      Text(`Param ${this.count}`)
      Text(`Param ${this.message}`)
      Text(`Param ${this.flag}`)
    }
  }
}

// 父组件
@Entry
@ComponentV2
struct Index {
  // 点击的次数
  @Local count: number = 0;
  @Local message: string = 'Hello';
  @Local flag: boolean = false;

  build() {
    Column() {
      Text(`Local ${this.count}`)
      Text(`Local ${this.message}`)
      Text(`Local ${this.flag}`)
      Button('change Local')
        .onClick(() => {
          // 对数据源的更改会同步给子组件
          this.count++;
          this.message += ' World';
          this.flag = !this.flag;
        }) 
      Child({   /// 2
        count: this.count,
        message: this.message,
        flag: this.flag
      })
    }
  }
}

@Event

接收父组件传入的方法,用来更新父组件的数据源。

typescript 复制代码
// 子组件
@ComponentV2
struct Child {
  @Param title: string = '';
  @Param fontColor: Color = Color.Black;
  @Event changeFactory: (x: number) => void = (x: number) => {}; /// 1

  build() {
    Column() {
      Text(`${this.title}`)
        .fontColor(this.fontColor)
      Button('change to Title Two')
        .onClick(() => {
          this.changeFactory(2); /// 2
        })
      Button('change to Title One')
        .onClick(() => {
          this.changeFactory(1);
        })
    }
  }
}

// 父组件
@Entry
@ComponentV2
struct Index {
  @Local title: string = 'Title One';
  @Local fontColor: Color = Color.Red;

  build() {
    Column() {
      Child({
        title: this.title,
        fontColor: this.fontColor,
        changeFactory: (type: number) => { /// 3
          if (type == 1) {
            this.title = 'Title One';
            this.fontColor = Color.Red;
          } else if (type == 2) {
            this.title = 'Title Two';
            this.fontColor = Color.Green;
          }
        }
      })
    }
  }
}
  1. 在子组件定义一个@Event回调。
  2. 在子组件调用这个回调。
  3. 在父组件传递一个箭头函数给子组件定义的。

在父组件给出的定义中,子组件调用这个回调的时候可以修改父组件的titlefontColor两个数据。

@ObservedV2和@Trace

用于监视对象内部属性的变化。

在上面描述的装饰器中,如果是一个对象,那么只能监听到赋值的变化。如果是一个数组等数据集合,一般只能监听到集合内部API的调用引起的变化。如果修改了数据某个对象的属性的值,是无法监听到的,也就是这样的修改不会出现在界面上。

这就需要用到@ObservedV2@Trace的装饰器组合。

  1. @ObservedV2装饰类。
  2. @Trace装饰需要观察的属性。

注意@ObservedV2装饰的类要在new出来之后才有观察变化的能力。

代码:

typescript 复制代码
@ObservedV2  /// 1
class Son {
  @Trace public age: number = 100; /// 2
}

class Father {
  public son: Son = new Son();  /// 3
}

@Entry
@ComponentV2
struct Index {
  father: Father = new Father();   /// 4

  build() {
    Column() {
      // 当点击改变age时,Text组件会刷新
      Text(`${this.father.son.age}`)  /// 5
        .onClick(() => {
          this.father.son.age++;  /// 6
        })
    }
  }
}
  1. 使用@ObservedV2装饰类。
  2. 使用@Trace装饰需要监听的属性age
  3. @Observed装饰的类在另一个类力使用。不是使用的必要步骤,只是说明监听的穿透力强
  4. 初始化需要监听的类。必须初始化才可以监听到变化。
  5. 如果属性的值发生变化,而且被监听到了,那么在界面上显示对应的变化。
  6. 在点击事件中更改@Trace装饰的属性。

@Provider / @Consumer

跨组件双向同步数据。遇到同名的时候,使用组件树上最近的那个。

定义的时候是这样的:

  1. @Provider(aliasName?: string) varName : varType = initValuealiasName为空的时候使用属性名作为aliasName
  2. @Consumer(aliasName?: string) varName : varType = initValuealiasName,如果为空就是属性名,是@Provider@Consumer关联的唯一key值。
typescript 复制代码
@ComponentV2
struct Parent {
  // 未定义aliasName, 使用属性名'str'作为aliasName
  @Provider() str: string = 'hello';
}

@ComponentV2
struct Child {
  // 定义aliasName为'str',使用aliasName去寻找
  // 能够在Parent组件上找到, 使用@Provider的值'hello'
  @Consumer('str') str: string = 'world';
}
  1. 这两个需要在同一个组件树中,在这个组件树的不同层级双向同步数据。
  2. 如果@Consumer找不到对应的@Provider,则使用本地的默认值。

@Monitor

装饰一个方法,可以在装饰器参数指定监视的状态。被监听的状态变化的时候触发Monitor装饰的方法。

typescript 复制代码
import { hilog } from '@kit.PerformanceAnalysisKit';

@Entry
@ComponentV2
struct Index {
  @Local message: string = 'Hello World';
  @Local name: string = 'Tom';
  @Local age: number = 24;

  @Monitor('message', 'name')
  onStrChange(monitor: IMonitor) {
    monitor.dirty.forEach((path: string) => {
      hilog.info(0xFF00, 'testTag', '%{public}s',
        `${path} changed from ${monitor.value(path)?.before} to ${monitor.value(path)?.now}`);
    });
  }

  build() {
    Column() {
      Button('change string')
        .onClick(() => {
          this.message += '!';
          this.name = 'Jack';
        })
    }
  }
}

也可以监听被@Trace装饰的属性的变化:

监听@Trace装饰的属性
typescript 复制代码
@Monitor('info')
infoChange(monitor: IMonitor) {
  hilog.info(0xFF00, 'testTag', '%{public}s', `info change`);
}

@Monitor('info.name') ///*
infoPropertyChange(monitor: IMonitor) {
  hilog.info(0xFF00, 'testTag', '%{public}s', `info name change`);
}

属性name@Trace装饰的时候可以被@Monitor监听到。

监听多个
typescript 复制代码
@Monitor('region', 'job') /// *
onChange(monitor: IMonitor) {
  monitor.dirty.forEach((path: string) => {
    hilog.info(0xFF00, 'testTag', '%{public}s',
      `${path} change from ${monitor.value(path)?.before} to ${monitor.value(path)?.now}`);
  })
}
可以在@ObservedV2装饰的类中使用

@Computed

装饰一个get属性,从一个或者多个获取到一个最终值。避免多个状态变化是多次计算。只做读,不做写

typescript 复制代码
@Computed
get sum() {
  return this.count1 + this.count2 + this.count3;
}

@Type

你的类需要被序列化,这时你的类里面还定义了一个属性,这个属性的类型也是一个类。为了不在序列化的时候丢失这个属性的类型可以用@Type来装饰属性。

注意:

  1. 构造函数不包含参数
  2. 一般配合 PersistenceV2一起使用
typescript 复制代码
class Sample {
  private data: number = 0;
}

@ObservedV2
class Info {
  @Type(Sample)
  @Trace public sample: Sample = new Sample(); // 正确用法
}

@AppStorageV2

整个app的状态管理,可以跨UIAbility共享数据。

定义:

typescript 复制代码
AppStorageV2.connect(/* 参数 */)

代码:

typescript 复制代码
AppStorageV2.connect<ThemeModel>(ThemeModel, 'app_theme', () => new ThemeModel(initialColorModel))
connect

这个方法用来创建或者获取存储的数据。

connect的参数。第一个是存放的类型。第二个可以是key,也可以是类型的默认构造器。如果第二个不是默认构造器,或者第二个参数不合法,那么第二个必须是默认构造器。

remove

使用remove删除指定的key对应的数据

keys

使用keys可以得到AppStorageV2中的全部key。

注意AppStorageV2只支持class类型。只能在UI线程使用。不支持collections.Setcollections.Map等类型。

@PersistenceV2

整个app的状态管理,另外带有持久化功能。

AppStorageV2一样,只是这个可以持久化存储。在第二次打开app的时候对应的状态不会丢失。

MVVM

App的代码组织模式。

简单的应用可以使用这样的模式来实现。

基本的组成有:

  1. Model层,负责管理数据。
  2. ModelView层,连接Model和View,负责管理UI的状态和业务逻辑。通过监控Model数据的变化,处理业务逻辑并将数据更新到View层。
  3. UI层,展示数据和与用户交互。

另外还有对数据库或者服务器操作的repository层。

示例代码在这里。作为一个Todo类App的Model层,这里定义了TodoModelTodoListModel

Model

TodoModel中,只负责持有数据。在TodoListModel中则需要借助repository处理本地SQLite的数据。入:

typescript 复制代码
import { TodoModel } from './TodoModel';
import { TodoRepository } from '../libs/repository';

export class TodoListModel {
  private _todos: TodoModel[] = []; /// 1
  private _repo?: TodoRepository;   /// 2

  // 略

  async loadTodoList() {
    if (!this._repo) {
      this._repo = await TodoRepository.getInstance(this._context)
    }

    const todoList = await this._repo?.queryAll()  /// 3
    this._todos = todoList ?? []
  }

  // 其他略
}
  1. TodoListModel的数据,一组TodoModel实例。
  2. TodoRepository,用来管理本地数据库的数据。
  3. 使用repository获取数据。

ModelView

typescript 复制代码
import { TodoModel } from '../model/TodoModel'

@ObservedV2
export class TodoViewModel {
  id: number = 0;
  createdAt: number = Date.now();
  updatedAt: number = Date.now();
  @Trace title: string = ''; // 标题
  @Trace description: string = ''; // 描述
  @Trace completed: boolean = false; // 完成状态
  @Trace version: number = 0; // 版本(用于乐观锁)
  
  // 其他略
}  

这里就使用了@ObservedV2@Trace来监听数据的变化。

typescript 复制代码
import { Type } from '@kit.ArkUI';
import { TodoListModel } from '../model/TodoListModel';
import { TodoModel } from '../model/TodoModel';
import { TodoViewModel } from './TodoViewModel';

@ObservedV2
export class TodoListViewModel {
  @Trace todoList: TodoViewModel[] = [];
  private _todoListModel?: TodoListModel

  // TODO: Add error message for display
  
  // 略

  async addTodo(title: string, description: string = '') {
    const todo: TodoModel = new TodoModel({ title, description });
    await this._todoListModel?.addTodo(todo);
    this.todoList.push(new TodoViewModel(todo));
  }
 
  // 其他略
}

TodoListViewModel的Model数据就是_todoListModel,并使用@Trace进行深度监听。

UI / View

typescript 复制代码
@Entry
@ComponentV2
struct Index {
  // 略
  
  @Local todoList: TodoListViewModel = new TodoListViewModel(this.getUIContext().getHostContext())
  
  // 其他略
  
}

在视图中把TodoListViewModel的示例使用@Local装饰。这样就把ViewModel这个模式的各个要素串联到了一起。

导航

这里介绍Stack导航和Tab导航。

Navigation四件套:

  1. Navigation组件,这个必不可少。
  2. NavPathStack实例。实际控制导航到哪里。
  3. 页面映射关系。这个定义在一个@Builder装饰的方法里。
  4. NavDestination。导航的目标"页"中最外面的组件就是NavDestination

代码如下:

typescript 复制代码
// ...

@Entry
@ComponentV2
struct Index {
  navStack: NavPathStack = new NavPathStack()  // 1

  build() {
    Navigation(this.navStack) {   // 2
      Column() {
        ThemedButton({ message: '弹窗' })
          .padding(10)
          .onClick(() => {
            this.navStack.pushPath({ name: 'Pops' }) // 3
          })
      }
      // ...
    }
    .navDestination(this.pageMap)  // 4
  }

  @Builder
  pageMap(name: string) {  // 5
    if (name === "Pops") {
      PopupSamples()
    }
  }
}

目标页:

typescript 复制代码
@Preview
@ComponentV2
export struct PopupSamples {
  build() {
    NavDestination() {  、、 6
      Column() {
        Text('PopupSamples')
      }
    }
  }
}
  1. NavPathStack,作为页面的成员初始化。
  2. Navigation,这就应该出现了,在布局顶层。并把NavPathStack的实例作为成员传入。这两个就关联在一起了。
  3. 使用NavPathStack成员执行导航。
  4. Navigation的属性方法中配置可以导航的页面。
  5. 定义页面地图。映射导航的名称和对应的组件。

更多高级内容稍后补充。。。

Tab

typescript 复制代码
@Preview
@ComponentV2
export struct TabsSamples {
  build() {
    NavDestination() {
      Tabs({barPosition: BarPosition.End}) { // 1
        TabContent() {                       // 2
          Text('Tab 1')
        }
        .tabBar("Tab 1")                     // 3

        TabContent() {
          Text('Tab 2')
        }
        .tabBar("Tab 2")
      }
    }
  }
}

Tab布局的实现需要三件套: TabsTabContenttabBar属性方法。

上面的代码中:

  1. Tabs,Tab布局的总体设置都在这里。比如在上面的例子中配置tab bar的位置在底部。
  2. TabContent,每个tab的内容容器。上例的内容为一个Text组件。
  3. tabBar,在这配置tab按钮

访问网络

首先,可以使用如下代码实现一个极简的服务器,get请求后可以返回一个json串来验证鸿蒙网络请求正确与否。

如下的bash命令直接在terminal运行即可跑起来一个server。

node版本:

bash 复制代码
node -e "require('http').createServer((req, res) => { res.writeHead(200, {'Content-Type': 'application/json'}); res.end(JSON.stringify({status: 'ok'})); }).listen(8000); console.log('Server running on port 8000')"

python:

bash 复制代码
python3 -c "from http.server import HTTPServer, BaseHTTPRequestHandler; import json; class S(BaseHTTPRequestHandler): do_GET = do_POST = lambda s: (s.send_response(200), s.send_header('Content-Type', 'application/json'), s.end_headers(), s.wfile.write(json.dumps({'status': 'ok'}).encode())); HTTPServer(('0.0.0.0', 8000), S).serve_forever()"

也可以直接使用这个地址:

arduino 复制代码
https://jsonplaceholder.typicode.com/posts/1

使用http模块

typescript 复制代码
async sendHttpRequest() {
  this.httpLoading = true
  this.httpResponseText = ''

  try {
    const httpRequest = http.createHttp() /// 1
    const url = 'https://jsonplaceholder.typicode.com/posts/1'

    const response = await httpRequest.request(url, { /// 2
      method: http.RequestMethod.GET,
      header: {
        'Content-Type': 'application/json'
      },
      connectTimeout: 60000,
      readTimeout: 60000
    })

    if (response.responseCode === 200) {
      this.httpResponseText = JSON.stringify(JSON.parse(response.result as string), null, 2)
    } else {
      this.httpResponseText = `请求失败: HTTP ${response.responseCode}`
    }

    httpRequest.destroy()  /// 3
  } catch (error) {
    this.httpResponseText = `请求出错: ${JSON.stringify(error)}`
  } finally {
    this.httpLoading = false
  }
}
  1. 使用http.createHttp()新建一个http请求实例:httpRequest
  2. 使用httpRequest请求服务器,并配置http method,请求的url地址等。
  3. 最后要销毁httpRequest

使用Axios访问网络:

typescript 复制代码
async sendAxiosRequest() {
  this.axiosLoading = true
  this.axiosResponseText = ''

  try {
    const response:AxiosResponse<string> = await axios.get('https://jsonplaceholder.typicode.com/posts/1', {
      headers: {
        'Content-Type': 'application/json'
      },
      timeout: 60000
    })   /// 1

    this.axiosResponseText = JSON.stringify(response.data, null, 2)
  } catch (error) {
    this.axiosResponseText = `请求出错: ${JSON.stringify(error)}`
  } finally {
    this.axiosLoading = false
  }
}

使用axios就简单多了。直接使用axios.get请求。

数据存储

SQLite, 四个读连接,一个写连接。

代码:

typescript 复制代码
import { relationalStore, ValuesBucket } from '@kit.ArkData'
import { Context } from '@kit.AbilityKit'
import { TodoModel } from '../model/TodoModel'

export const DB_NAME = 'todo_db.db'

export const DB_VERSION = 1

export const CREATE_TABLE_SQL = `
  CREATE TABLE IF NOT EXISTS todo_info (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    description TEXT,
    completed INTEGER DEFAULT 0,
    version INTEGER DEFAULT 1,
    createdAt INTEGER,
    updatedAt INTEGER
  )
`

export const STORE_CONFIG: relationalStore.StoreConfig = {
  name: DB_NAME,
  securityLevel: relationalStore.SecurityLevel.S1
}

export class TodoRepository {
  private static instance: TodoRepository
  private rdbStore: relationalStore.RdbStore | null = null
  private context: Context | null = null

  static async getInstance(context?: Context): Promise<TodoRepository> {
    if (!TodoRepository.instance) {
      if (!context) {
        throw new Error('Context must be provided for first initialization')
      }
      TodoRepository.instance = new TodoRepository()
      await TodoRepository.instance.init(context)
    }
    return TodoRepository.instance
  }

  private async init(context: Context): Promise<void> {
    this.context = context
    this.rdbStore = await relationalStore.getRdbStore(context, STORE_CONFIG)
    await this.rdbStore.executeSql(CREATE_TABLE_SQL)
  }

  async queryAll(): Promise<TodoModel[]> {
    if (!this.rdbStore) {
      this.rdbStore = await relationalStore.getRdbStore(this.context!, STORE_CONFIG)
    }

    const resultSet = await this.rdbStore.querySql('SELECT * FROM todo_info ORDER BY createdAt DESC')
    const list: TodoModel[] = []
    while (resultSet.goToNextRow()) {
      const todo = new TodoModel()
      todo.id = resultSet.getLong(resultSet.getColumnIndex('id'))
      todo.title = resultSet.getString(resultSet.getColumnIndex('title'))
      todo.description = resultSet.getString(resultSet.getColumnIndex('description'))
      todo.completed = resultSet.getLong(resultSet.getColumnIndex('completed')) === 1
      todo.createdAt = resultSet.getDouble(resultSet.getColumnIndex('createdAt'))
      todo.updatedAt = resultSet.getDouble(resultSet.getColumnIndex('updatedAt'))

      list.push(todo)
    }
    resultSet.close()
    return list
  }

  async getTodoById(id: number): Promise<TodoModel | null> {
    if (!this.rdbStore) {
      this.rdbStore = await relationalStore.getRdbStore(this.context!, STORE_CONFIG)
    }

    const predicates = new relationalStore.RdbPredicates('todo');
    predicates.equalTo('id', id);

    const columns = ['id', 'title', 'description', 'completed', 'version', 'createdAt', 'updatedAt'];
    const result = await this.rdbStore.query(predicates, columns);

    if (result.goToNextRow()) {
      const todo = TodoModel.fromDatabase({
        id: result.getDouble(result.getColumnIndex('id')),
        title: result.getString(result.getColumnIndex('title')),
        description: result.getString(result.getColumnIndex('description')),
        completed: result.getDouble(result.getColumnIndex('completed')),
        version: result.getDouble(result.getColumnIndex('version')),
        createdAt: result.getDouble(result.getColumnIndex('createdAt')),
        updatedAt: result.getDouble(result.getColumnIndex('updatedAt'))
      });
      result.close();
      return todo;
    }

    result.close();
    return null;
  }

  async insert(todo: TodoModel): Promise<void> {
    if (!this.rdbStore) {
      this.rdbStore = await relationalStore.getRdbStore(this.context!, STORE_CONFIG)
    }

    const valueBucket: ValuesBucket = {
      // id: todo.id,
      title: todo.title,
      description: todo.description,
      completed: todo.completed ? 1 : 0,
      createdAt: todo.createdAt
    }
    await this.rdbStore.insert('todo_info', valueBucket)
  }

  async updateStatus(todo: TodoModel): Promise<void> {
    const updateStatement = 'UPDATE todo_info SET ' + todo.toKeyValuePairs() + ` WHERE id = ${todo.id}}`

    if (!this.rdbStore) {
      this.rdbStore = await relationalStore.getRdbStore(this.context!, STORE_CONFIG)
    }

    await this.rdbStore.executeSql(updateStatement)
  }

  async deleteTodo(id: number): Promise<boolean> {
    if (!this.rdbStore) {
      throw new Error('数据库未初始化');
    }

    const predicates = new relationalStore.RdbPredicates('todo_info');
    predicates.equalTo('id', id);

    const affectedRows = await this.rdbStore.delete(predicates);
    return affectedRows > 0;
  }
}

通知和推送

异步编程

有两种方式实现,一个是异步并发使用promise和async/await实现。依靠单线程的事件循环。

另外一种就是多线程并发。使用TaskPool和Worker实现。

异步并发

使用Promise或者async/await实现。如:

typescript 复制代码
const promise: Promise<number> = new Promise((resolve: Function, reject: Function) => {
  setTimeout(() => {
    const randomNumber: number = Math.random();
    if (randomNumber > 0.5) {
      resolve(randomNumber);
    } else {
      reject(new Error('Random number is too small'));
    }
  }, 1000);
})

Async/Await

typescript 复制代码
async function myAsyncFunction(): Promise<string> {
  const result: string = await new Promise((resolve: Function) => {
    setTimeout(() => {
      resolve('Hello, world!');
    }, 3000);
  });
  console.info(result); // 输出: Hello, world!
  return result;
}

多线程并发

多线程并发可以使用taskpoolWorker实现。

Taskpool

底层原理是Actor模型。开发中可以使用TaskPool或者Worker实现。详细的文档可以看这里

简言之,Actor模型的多个线程之间不同享内存,一个线程就是一个Actor。多个线程需要通信则互发消息。

taskpool的典型用法,看代码:

typescript 复制代码
import { taskpool } from '@kit.ArkTS';
import { sleep } from '../utils';

@Concurrent  // => 1
async function generateNumber(ms: number): Promise<number> {
  await sleep(ms);
  return Math.random();
}

@ComponentV2
export struct AsyncSamples {
  @Local message: string = 'Async Samples';

  aboutToAppear(): void {
    const task = new taskpool.Task(generateNumber, 2000); // => 2
    taskpool.execute(task).then((result) => { // => 3
      console.log('Task result', result);
    });
  }

  build() {
    NavDestination() {
      Column() {
        Text(this.message)
      }
      // ...
    }
  }
}

注意:这里只说鸿蒙文档的典型用法,其他用法在后面会提到。

上面的代码,首先引入taskpool

  1. 定义一个并发方法,通过@Concurrent这个装饰器装饰一个方法实现。这个方法可以是一个async方法,也可以不是。
  2. 用定义好的并发方法新建一个taskpool.Task实例。如果这个并发方法需要一个参数,那么在定义Task的时候在第二个参数给出。
  3. 使用taskpool.execute执行前一步定义好的task。在then里获取执行的结果,并更新组件的状态。task的结果就显示在界面上了。

在定义并发方法的时候,方法内部使用的只能是局部变量,入参和import引入的变量。

taskpool也可以这样:

ts 复制代码
function someFunc(param: string) {
    //...
}

taskpool.execute(someFunc, param).then((ret) => { // 定期执行可以使用executePeriodically
    // ...
});

Worker

Worker实现三步:

  1. 新建一个新的文件放worker的代码。
  2. 新建worker.ThreadWorker实例。
  3. 宿主现场和子线程之间互传消息。

具体实现如下:

点了Start worker之后开始执行线程代码。每次更新progress bar的进度,一直到达到百分之百。

代码如下:

typescript 复制代码
import { MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS';

const workerPort: ThreadWorkerGlobalScope = worker.workerPort; /// 1

let progressTimer: number | null = null
let currentProgress: number = 0

workerPort.onmessage = (e: MessageEvents) => {  /// 2
  const message:string = e.data

  if (message=== 'START') {
    currentProgress = 0

    // 清除旧定时器
    if (progressTimer) {
      clearInterval(progressTimer)
    }

    // 每 300ms 发送进度
    progressTimer = setInterval(() => {
      currentProgress += 2  // 每次增加 2%

      // 发送进度回主线程
      workerPort.postMessage({                  /// 3
        type: 'PROGRESS_UPDATE',
        value: currentProgress
      })

      // 完成时清理
      if (currentProgress >= 100) {
        if (progressTimer) {
          clearInterval(progressTimer)
          progressTimer = null
        }
        workerPort.postMessage({ type: 'COMPLETED' })
      }
    }, 300)
  }

  if (message=== 'STOP') {
    if (progressTimer) {
      clearInterval(progressTimer)
      progressTimer = null
    }
  }
}
  1. 初始化workerPost实例,这是线程互通的关键。
  2. 接受主线程的消息,根据STARTSTOP开始或者停止发送消息。
  3. 在本线程给主线程发送消息:workerPort.postMessage

在UI:

typescript 复制代码
ThemedButton({
  message: this.progressText, handleClick: () => {
    let workerInstance = new worker.ThreadWorker('entry/ets/workers/worker.ets'); /// 1
    
    workerInstance.postMessage('START');             /// 2
    workerInstance.onmessage = ((e: MessageEvents) => {   /// 3
      const data: WorkerMessage = e.data as WorkerMessage;
      if (data.type === 'PROGRESS_UPDATE') {
        this.progressValue = data.value;
        this.status = 'running';
      } else if (data.type === 'COMPLETED') {
        this.progressValue = 100;
        this.status = 'completed';
      }
    });
  }
})
  1. 使用定义好的worker.ets文件新建worker实例。
  2. 给子线程发送消息:workerInstance.postMessage()。这里通知子线程开始执行。
  3. 接收子线程发送过来的消息,并更新界面。

注意 :在处理worker文件的时候有些注意事项,请看这里

TaskpoolWorker的对比,看这里

相关推荐
特立独行的猫a2 小时前
HarmonyOS鸿蒙PC开源QT软件移植:移植开源文本编辑器 NotePad--(Ndd)到鸿蒙 PC实践总结
qt·开源·notepad++·harmonyos·notepad--·鸿蒙pc
IntMainJhy2 小时前
【futter for open harmony】Flutter 聊天应用实战:Material Design 3 全局 UI 规范落地指南✨
flutter·华为·harmonyos
IntMainJhy2 小时前
【flutter for open harmony】Flutter 聊天应用实战:go_router 路由管理完全实现指南
flutter·华为·harmonyos
liulian09162 小时前
【Flutter For OpenHarmony第三方库】Flutter 页面导航的鸿蒙化适配实战
flutter·华为·学习方法·harmonyos
南村群童欺我老无力.3 小时前
鸿蒙PC开发的borderWidth_API签名的类型陷阱
华为·harmonyos
前端不太难3 小时前
鸿蒙游戏 + AI:自动测试与自动发布
人工智能·游戏·harmonyos
Lanren的编程日记3 小时前
Flutter 鸿蒙应用用户反馈功能实战:快速收集用户意见与建议
flutter·华为·harmonyos
Lanren的编程日记1 天前
Flutter鸿蒙应用开发:数据加密功能实现实战,全方位保护用户隐私数据
flutter·华为·harmonyos
想你依然心痛1 天前
HarmonyOS 6健康应用实战:基于悬浮导航与沉浸光感的“光影律动“智能健身系统
华为·harmonyos·悬浮导航·沉浸光感