HarmonyOS开发学习笔记

HarmonyOS开发学习笔记

基本步骤

  1. 开发环境搭建 按照官网步骤
  2. 语言选择 官方建议选择arkTs(TypeScript的超集)
  3. 官方开发 参考指南 入门必读
  4. 官方开发 参考api 实战必读

HarmonyOS实战所需基础能力

  1. 页面布局能力
  2. 页面之间的数据通信能力(Ability和page; Ability和Ability; page和page)
  3. 数据存储和读取能力
  4. 网络请求能力
  5. 权限管理能力
  6. 媒体播放能力
  7. 通知管理能力
  8. 文件管理能力
  9. web接入交互能力

基于ArkTs应用(Stage模型)

  • 工程目录结构
    • AppScope > app.json5:应用的全局配置信息。
    • entry:HarmonyOS工程模块,编译构建生成一个HAP包。
      • src > main > ets:用于存放ArkTS源码。
      • src > main > ets > entryability:应用/服务的入口。
      • src > main > ets > pages:应用/服务包含的页面。
      • src > main > resources:用于存放应用/服务所用到的资源文件,如图形、多媒体、字符串、布局文件等。关于资源文件详见资源分类与访问
      • src > main > resources > profile > main_pages.json: 配置页面路由
      • src > main > module.json5:Stage模型模块配置文件。主要包含HAP包的配置信息、应用/服务在具体设备上的配置信息以及应用/服务的全局配置信息。具体的配置文件说明,详见module.json5配置文件
      • build-profile.json5:当前的模块信息、编译信息配置项,包括buildOption、targets配置等。其中targets中可配置当前运行环境,默认为HarmonyOS。
      • hvigorfile.ts:模块级编译构建任务脚本,开发者可以自定义相关任务和代码实现。
    • oh_modules:用于存放三方库依赖信息。关于原npm工程适配ohpm操作,请参考历史工程迁移
    • build-profile.json5:应用级配置信息,包括签名、产品配置等。
    • hvigorfile.ts:应用级编译构建任务脚本。

HarmonyOS开发中@Entry @Component @State作用

  1. @Entry:这个注解在自定义组件时使用,表示该自定义组件为入口组件。
  2. @Component:这个注解在HarmonyOS的UI框架中,表示一个组件。Component类位于ohos.agp.components包中,直接派生自java.lang.Object,并且是HarmonyOS中所有界面组件的直接或间接父类。每一个组件在屏幕上占用一个矩形区域,在这个区域中对绘制和事件处理做出响应。
  3. @State:这个注解在组件中定义状态变量。状态变量变化会触发UI刷新。它具有以下特征:
    支持多种类型:允许class、number、boolean、string强类型的按值和按引用类型。允许这些强类型构成的数组,即Array<class>、Array<string>、Array<boolean>、Array<number>。不允许object和any。
    支持多实例:组件不同实例的内部状态数据独立。
    内部私有:标记为@State的属性是私有变量,只能在组件内访问。
    需要本地初始化:必须为所有@State变量分配初始值,将变量保持未初始化可能导致框架行为未定义。
    创建自定义组件时支持通过状态变量名设置初始值:在创建组件实例时,可以通过变量名显式指定@State状态属性的初始值。

在HarmonyOS开发中Component中@Builder 和build()作用是否一样

在HarmonyOS的Component中,@Builder和build()的作用并不完全相同。

@Builder是一种装饰器,用于修饰函数,以指示该函数是一个自定义构建函数。被@Builder装饰的函数需要遵循build()函数语法规则,可以将重复使用的UI元素抽象成一个方法,然后在build方法中调用。通过使用@Builder装饰器,开发者可以更方便地复用UI元素,简化代码量。

build()函数是Component中一个重要的方法,用于构建UI界面。在build()函数中,开发者可以通过调用@Builder装饰的函数来复用UI元素,也可以在build()函数中编写其他的UI构建逻辑。因此,build()函数和@Builder装饰器是紧密相关的,但并不是相同的概念。

综上所述,@Builder和build()在HarmonyOS的Component中都是重要的开发工具,但它们的作用略有不同。@Builder用于定义自定义构建函数,而build()函数则用于构建UI界面。

HarmonyOS中其它一些常用的装饰器

  • @Builder/@BuilderParam:特殊的封装UI描述的方法,细粒度的封装和复用UI描述。
  • @Extend/@Style:扩展内置组件和封装属性样式,更灵活地组合内置组件。
  • stateStyles:多态样式,可以依据组件的内部状态的不同,设置不同样式。

HarmonyOS中Component中几个常用的生命周期方法

  • aboutToAppear 组件即将出现时回调该接口,具体时机为在创建自定义组件的新实例后,在执行其build()函数之前执行。 组件生命周期方法
  • aboutToDisappear 在自定义组件即将析构销毁时执行。 组件生命周期方法

HarmonyOS中被@Entry装饰的组件生命周期

  • onPageShow 页面每次显示时触发。 页面生命周期方法
  • onPageHide 页面每次隐藏时触发一次。 页面生命周期方法
  • onBackPress 当用户点击返回按钮时触发。页面生命周期方法

HarmonyOS开发所需ArkTs基本语法参考文档

声明式ui描述

  1. 创建组件
    • 无参数
      如果组件的接口定义没有包含必选构造参数,则组件后面的“()”不需要配置任何内容。例如,Divider组件不包含构造参数:
      Column() {
         Text('item 1')
         Divider()
         Text('item 2')
       }
      
    • 有参数 如果组件的接口定义包含构造参数,则在组件后面的“()”配置相应参数。
      • Image组件的必选参数src。
        Image('https://xyz/test.jpg')
        
      • Text组件的非必选参数content。
        // string类型的参数
        Text('test')
        // $r形式引入应用资源,可应用于多语言场景
        Text($r('app.string.title_value'))
        // 无参数形式
        Text()
        
      • 变量或表达式也可以用于参数赋值,其中表达式返回的结果类型必须满足参数类型要求。
        例如,设置变量或表达式来构造Image和Text组件的参数。
        Image(this.imagePath)
        Image('https://' + this.imageUrl)
        Text(`count: ${this.count}`)
        
  2. 配置属性
    属性方法以“.”链式调用的方式配置系统组件的样式和其他属性,建议每个属性方法单独写一行。
    • 配置Text组件的字体大小。
      Text('test')
        .fontSize(12)
      
    • 配置组件的多个属性。
      Image('test.jpg')
        .alt('error.jpg')    
        .width(100)    
        .height(100)
      
    • 除了直接传递常量参数外,还可以传递变量或表达式。
      Text('hello')
        .fontSize(this.size)
      Image('test.jpg')
        .width(this.count % 2 === 0 ? 100 : 200)    
        .height(this.offset + 100)
      
    • 对于系统组件,ArkUI还为其属性预定义了一些枚举类型供开发者调用,枚举类型可以作为参数传递,但必须满足参数类型要求。
      例如,可以按以下方式配置Text组件的颜色和字体样式。
      Text('hello')
        .fontSize(20)
        .fontColor(Color.Red)
        .fontWeight(FontWeight.Bold)
      
  3. 配置事件
    事件方法以“.”链式调用的方式配置系统组件支持的事件,建议每个事件方法单独写一行。
    • 使用lambda表达式配置组件的事件方法。
      Button('Click me')
        .onClick(() => {
          this.myText = 'ArkUI';
        })
      
    • 使用匿名函数表达式配置组件的事件方法,要求使用bind,以确保函数体中的this指向当前组件。
      Button('add counter')
        .onClick(function(){
          this.counter += 2;
        }.bind(this))
      
    • 使用组件的成员函数配置组件的事件方法。
      myClickHandler(): void {
          this.counter += 2;
      }
      ...
        Button('add counter')
          .onClick(this.myClickHandler.bind(this))
      
  4. 配置子组件
    如果组件支持子组件配置,则需在尾随闭包"{...}"中为组件添加子组件的UI描述。Column、Row、Stack、Grid、List等组件都是容器组件。
  • 以下是简单的Column组件配置子组件的示例。
        Column() {
        Text('Hello')
          .fontSize(100)
        Divider()
        Text(this.myText)
          .fontSize(100)
          .fontColor(Color.Red)
        }
    
  • 容器组件均支持子组件配置,可以实现相对复杂的多级嵌套。
      Column() {
        Row() {
          Image('test1.jpg')
            .width(100)
            .height(100)
          Button('click +1')
            .onClick(() => {
              console.info('+1 clicked!');
            })
        }
      }
    

自定义组件

在ArkUI中,UI显示的内容均为组件,由框架直接提供的称为系统组件,由开发者定义的称为自定义组件。
自定义组件具有以下特点:

  • 可组合:允许开发者组合使用系统组件、及其属性和方法。
  • 可重用:自定义组件可以被其他组件重用,并作为不同的实例在不同的父组件或容器中使用。
  • 数据驱动UI更新:通过状态变量的改变,来驱动UI的刷新。

自定义组件的基本用法:

@Component
struct HelloComponent {
  @State message: string = 'Hello, World!';

  build() {
    // HelloComponent自定义组件组合系统组件Row和Text
    Row() {
      Text(this.message)
        .onClick(() => {
          // 状态变量message的改变驱动UI刷新,UI从'Hello, World!'刷新为'Hello, ArkUI!'
          this.message = 'Hello, ArkUI!';
        })
    }
  }
}

HelloComponent可以在其他自定义组件中的build()函数中多次创建,实现自定义组件的重用。

@Entry
@Component
struct ParentComponent {
  build() {
    Column() {
      Text('ArkUI message')
      HelloComponent({ message: 'Hello, World!' });
      Divider()
      HelloComponent({ message: '你好!' });
    }
  }
}

创建自定义组件

  1. 自定义组建的基本结构
    • struct:自定义组件基于struct实现,struct + 自定义组件名 + {...}的组合构成自定义组件,不能有继承关系。对于struct的实例化,可以省略new。注:自定义组件名、类名、函数名不能和系统组件名相同。
    • @Component:@Component装饰器仅能装饰struct关键字声明的数据结构。struct被@Component装饰后具备组件化的能力,需要实现build方法描述UI,一个struct只能被一个@Component装饰。注: 从API version 9开始,该装饰器支持在ArkTS卡片中使用。
       @Component
       struct MyComponent {
       }
      
  • build()函数:build()函数用于定义自定义组件的声明式UI描述,自定义组件必须定义build()函数。
    @Component
    struct MyComponent {
      build() {
      }
    }
    
  • @Entry:@Entry装饰的自定义组件将作为UI页面的入口。在单个UI页面中,最多可以使用@Entry装饰一个自定义组件。@Entry可以接受一个可选的LocalStorage的参数. 注: 从API version 9开始,该装饰器支持在ArkTS卡片中使用.
      @Entry
      @Component
      struct MyComponent {
      }
    
  1. 成员函数/变量
    自定义组件除了必须要实现build()函数外,还可以实现其他成员函数,成员函数具有以下约束:
    • 不支持静态函数。
    • 成员函数的访问始终是私有的, 变量的访问规则与成员函数的访问规则相同。
    • 自定义组件的成员变量本地初始化有些是可选的,有些是必选的。具体是否需要本地初始化,是否需要从父组件通过参数传递初始化子组件的成员变量,请参考状态管理。
  2. 自定义组件参数规定
    我们已经了解到,可以在build方法或者@Builder装饰的函数里创建自定义组件,在创建的过程中,参数可以被提供给组件。
     @Component
     struct MyComponent {
       private countDownFrom: number = 0;
       private color: Color = Color.Blue;
    
       build() {
       }
     }
    
     @Entry
     @Component
     struct ParentComponent {
       private someColor: Color = Color.Pink;
    
       build() {
         Column() {
           // 创建MyComponent实例,并将创建MyComponent成员变量countDownFrom初始化为10,将成员变量color初始化为this.someColor
           MyComponent({ countDownFrom: 10, color: this.someColor })
         }
       }
     }
    
  3. build()函数
    所有声明在build()函数的语言,我们统称为UI描述语言,UI描述语言需要遵循以下规则:
    • @Entry装饰的自定义组件,其build()函数下的根节点唯一且必要,且必须为容器组件,其中ForEach禁止作为根节点。@Component装饰的自定义组件,其build()函数下的根节点唯一且必要,可以为非容器组件,其中ForEach禁止作为根节点。
        @Entry
        @Component
        struct MyComponent {
          build() {
            // 根节点唯一且必要,必须为容器组件
            Row() {
              ChildComponent() 
            }
          }
        }
      
        @Component
        struct ChildComponent {
          build() {
            // 根节点唯一且必要,可为非容器组件
            Image('test.jpg')
          }
        }
      
    • build()函数内不允许声明本地变量,反例如下。
      build() {
         // 反例:不允许声明本地变量
         let a: number = 1;
       }
      
  • build()函数内不允许在UI描述里直接使用console.info,但允许在方法或者函数里使用,反例如下。
    build() {
      // 反例:不允许console.info
      console.info('print debug log');
    }
    
  • build()函数内不允许创建本地的作用域,反例如下。
    build() {
      // 反例:不允许本地作用域
      {
        ...
      }
    }
    
  • build()函数内不允许调用除了被@Builder装饰以外的方法,允许系统组件的参数是TS方法的返回值。
    @Component
    struct ParentComponent {
      doSomeCalculations() {
      }
    
      calcTextValue(): string {
        return 'Hello World';
      }
    
      @Builder doSomeRender() {
        Text(`Hello World`)
      }
    
      build() {
        Column() {
          // 反例:不能调用没有用@Builder装饰的方法
          this.doSomeCalculations();
          // 正例:可以调用
          this.doSomeRender();
          // 正例:参数可以为调用TS方法的返回值
          Text(this.calcTextValue())
        }
      }
    }
    
  • build()函数内不允许switch语法,如果需要使用条件判断,请使用if。反例如下。
    build() {
      Column() {
        // 反例:不允许使用switch语法
        switch (expression) {
          case 1:
            Text('...')
            break;
          case 2:
            Image('...')
            break;
          default:
            Text('...')
            break;
        }
      }
    }
    
  • build()函数内不允许使用表达式,反例如下。
    build() {
      Column() {
        // 反例:不允许使用表达式
        (this.aVar > 10) ? Text('...') : Image('...')
      }
    }
    
  1. 自定义组件通用样式
    自定义组件通过“.”链式调用的形式设置通用样式。
    @Component
     struct MyComponent2 {
       build() {
         Button(`Hello World`)
       }
     }
    
     @Entry
     @Component
     struct MyComponent {
       build() {
         Row() {
           MyComponent2()
             .width(200)
             .height(300)
             .backgroundColor(Color.Red)
         }
       }
     }
    
    注意: ArkUI给自定义组件设置样式时,相当于给MyComponent2套了一个不可见的容器组件,而这些样式是设置在容器组件上的,而非直接设置给MyComponent2的Button组件。通过渲染结果我们可以很清楚的看到,背景颜色红色并没有直接生效在Button上,而是生效在Button所处的开发者不可见的容器组件上。

页面和自定义组件生命周期

页面生命周期,即被@Entry装饰的组件生命周期,提供以下生命周期接口:

  • onPageShow 页面每次显示时触发。 页面生命周期方法
  • onPageHide 页面每次隐藏时触发一次。 页面生命周期方法
  • onBackPress 当用户点击返回按钮时触发。页面生命周期方法

组件生命周期,即一般用@Component装饰的自定义组件的生命周期,提供以下生命周期接口:

  • aboutToAppear 组件即将出现时回调该接口,具体时机为在创建自定义组件的新实例后,在执行其build()函数之前执行。 组件生命周期方法
  • aboutToDisappear 在自定义组件即将析构销毁时执行。 组件生命周期方法
    [图片上传失败...(image-c82397-1697789591540)]
  1. 自定义组件的创建和渲染流程

    • 自定义组件的创建:自定义组件的实例由ArkUI框架创建。
    • 初始化自定义组件的成员变量:通过本地默认值或者构造方法传递参数来初始化自定义组件的成员变量,初始化顺序为成员变量的定义顺序
    • 如果开发者定义了aboutToAppear,则执行aboutToAppear方法。
    • 在首次渲染的时候,执行build方法渲染系统组件,如果子组件为自定义组件,则创建自定义组件的实例。在执行build()函数的过程中,框架会观察每个状态变量的读取状态,将保存两个map:
      • 状态变量 -> UI组件(包括ForEach和if)。
      • UI组件 -> 此组件的更新函数,即一个lambda方法,作为build()函数的子集,创建对应的UI组件并执行其属性方法,示意如下。
        build() {
           ...
           this.observeComponentCreation(() => {
             Button.create();
           })
        
           this.observeComponentCreation(() => {
             Text.create();
           })
           ...
         }
        
        当应用在后台启动时,此时应用进程并没有销毁,所以仅需要执行onPageShow。
  2. 自定义组件重新渲染
    当事件句柄被触发(比如设置了点击事件,即触发点击事件)改变了状态变量时,或者LocalStorage / AppStorage中的属性更改,并导致绑定的状态变量更改其值时:

    1. 框架观察到了变化,将启动重新渲染。
    2. 根据框架持有的两个map(自定义组件的创建和渲染流程中第4步),框架可以知道该状态变量管理了哪些UI组件,以及这些UI组件对应的更新函数。执行这些UI组件的更新函数,实现最小化更新。
  3. 自定义组件的删除
    如果if组件的分支改变,或者ForEach循环渲染中数组的个数改变,组件将被删除:

    1. 在删除组件之前,将调用其aboutToDisappear生命周期函数,标记着该节点将要被销毁。ArkUI的节点删除机制是:后端节点直接从组件树上摘下,后端节点被销毁,对前端节点解引用,当前端节点已经没有引用时,将被JS虚拟机垃圾回收。
    2. 自定义组件和它的变量将被删除,如果其有同步的变量,比如@Link、@Prop、@StorageLink,将从同步源上取消注册。

    不建议在生命周期aboutToDisappear内使用async await,如果在生命周期的aboutToDisappear使用异步操作(Promise或者回调方法),自定义组件将被保留在Promise的闭包中,直到回调方法被执行完,这个行为阻止了自定义组件的垃圾回收。

    以下示例展示了生命周期的调用时机:

    // Index.ets
       import router from '@ohos.router';
    
       @Entry
       @Component
       struct MyComponent {
         @State showChild: boolean = true;
    
         // 只有被@Entry装饰的组件才可以调用页面的生命周期
         onPageShow() {
           console.info('Index onPageShow');
         }
         // 只有被@Entry装饰的组件才可以调用页面的生命周期
         onPageHide() {
           console.info('Index onPageHide');
         }
    
         // 只有被@Entry装饰的组件才可以调用页面的生命周期
         onBackPress() {
           console.info('Index onBackPress');
         }
    
         // 组件生命周期
         aboutToAppear() {
           console.info('MyComponent aboutToAppear');
         }
    
         // 组件生命周期
         aboutToDisappear() {
           console.info('MyComponent aboutToDisappear');
         }
    
         build() {
           Column() {
             // this.showChild为true,创建Child子组件,执行Child aboutToAppear
             if (this.showChild) {
               Child()
             }
             // this.showChild为false,删除Child子组件,执行Child aboutToDisappear
             Button('create or delete Child').onClick(() => {
               this.showChild = false;
             })
             // push到Page2页面,执行onPageHide
             Button('push to next page')
               .onClick(() => {
                 router.pushUrl({ url: 'pages/Page2' });
               })
           }
    
         }
       }
    
       @Component
       struct Child {
         @State title: string = 'Hello World';
         // 组件生命周期
         aboutToDisappear() {
           console.info('[lifeCycle] Child aboutToDisappear')
         }
         // 组件生命周期
         aboutToAppear() {
           console.info('[lifeCycle] Child aboutToAppear')
         }
    
         build() {
           Text(this.title).fontSize(50).onClick(() => {
             this.title = 'Hello ArkUI';
           })
         }
       }
    

    以上示例中,Index页面包含两个自定义组件,一个是被@Entry装饰的MyComponent,也是页面的入口组件,即页面的根节点;一个是Child,是MyComponent的子组件。只有@Entry装饰的节点才可以生效页面的生命周期方法,所以MyComponent中声明了当前Index页面的页面生命周期函数。MyComponent和其子组件Child也同时也声明了组件的生命周期函数。

    • 应用冷启动的初始化流程为:MyComponent aboutToAppear --> MyComponent build --> Child aboutToAppear --> Child build --> Child build执行完毕 --> MyComponent build执行完毕 --> Index onPageShow。
    • 点击“delete Child”,if绑定的this.showChild变成false,删除Child组件,会执行Child aboutToDisappear方法。
    • 点击“push to next page”,调用router.pushUrl接口,跳转到另外一个页面,当前Index页面隐藏,执行页面生命周期Index onPageHide。此处调用的是router.pushUrl接口,Index页面被隐藏,并没有销毁,所以只调用onPageHide。跳转到新页面后,执行初始化新页面的生命周期的流程。
    • 如果调用的是router.replaceUrl,则当前Index页面被销毁,执行的生命周期流程将变为:Index onPageHide --> MyComponent aboutToDisappear --> Child aboutToDisappear。上文已经提到,组件的销毁是从组件树上直接摘下子树,所以先调用父组件的aboutToDisappear,再调用子组件的aboutToDisappear,然后执行初始化新页面的生命周期流程。
    • 点击返回按钮,触发页面生命周期Index onBackPress,且触发返回一个页面后会导致当前Index页面被销毁。
    • 最小化应用或者应用进入后台,触发Index onPageHide。当前Index页面没有被销毁,所以并不会执行组件的aboutToDisappear。应用回到前台,执行Index onPageShow。
    • 退出应用,执行Index onPageHide --> MyComponent aboutToDisappear --> Child aboutToDisappear。

@Builder装饰器

使用说明:

  1. 自定义组件内自定义构建函数
  2. 全局自定义构建函数
    • 全局的自定义构建函数可以被整个应用获取,不允许使用this和bind方法。
    • 如果不涉及组件状态变化,建议使用全局的自定义构建方法。

参数传递规则:

  1. 按值传递
    • 调用@Builder装饰的函数默认按值传递。当传递的参数为状态变量时,状态变量的改变不会引起@Builder方法内的UI刷新。所以当使用状态变量的时候,推荐使用按引用传递。
       @Builder function ABuilder(paramA1: string) {
         Row() {
           Text(`UseStateVarByValue: ${paramA1} `)
         }
       }
       @Entry
       @Component
       struct Parent {
         label: string = 'Hello';
         build() {
           Column() {
             ABuilder(this.label)
           }
         }
       }
      
  2. 按引用传递
    • 按引用传递参数时,传递的参数可为状态变量,且状态变量的改变会引起@Builder方法内的UI刷新。ArkUI提供$$作为按引用传递参数的范式。
        //全局构建
        @Builder function ABuilder($$: { paramA1: string }) {
            Row() {
              Text(`UseStateVarByReference: ${$$.paramA1} `)
            }
          }
      
        //组件内构建
        @Builder ABuilder( $$ : { paramA1: string } );
      
        @Entry
        @Component
        struct Parent {
          @State label: string = 'Hello';
          build() {
            Column() {
              // 在Parent组件中调用ABuilder的时候,将this.label引用传递给ABuilder
              ABuilder({ paramA1: this.label })
              Button('Click me').onClick(() => {
                // 点击“Click me”后,UI从“Hello”刷新为“ArkUI”
                this.label = 'ArkUI';
              })
            }
          }
        }
      

@BuilderParam装饰器:引用@Builder函数

使用场景:

  • 参数初始化组件
  • 尾随闭包初始化组件: 开发者可以将尾随闭包内的内容看做@Builder装饰的函数传给@BuilderParam。示例如下:
    // xxx.ets
      @Component
      struct CustomContainer {
        @Prop header: string;
        @BuilderParam closer: () => void
    
        build() {
          Column() {
            Text(this.header)
              .fontSize(30)
            this.closer()
          }
        }
      }
    
      @Builder function specificParam(label1: string, label2: string) {
        Column() {
          Text(label1)
            .fontSize(30)
          Text(label2)
            .fontSize(30)
        }
      }
    
      @Entry
      @Component
      struct CustomContainerUser {
        @State text: string = 'header';
    
        build() {
          Column() {
            // 创建CustomContainer,在创建CustomContainer时,通过其后紧跟一个大括号“{}”形成尾随闭包
            // 作为传递给子组件CustomContainer @BuilderParam closer: () => void的参数
            CustomContainer({ header: this.text }) {
              Column() {
                specificParam('testA', 'testB')
              }.backgroundColor(Color.Yellow)
              .onClick(() => {
                this.text = 'changeHeader';
              })
            }
          }
        }
      }
    

@Styles装饰器:定义组件重用样式

@Styles装饰器可以将多条样式设置提炼成一个方法,直接在组件声明的位置调用。通过@Styles装饰器可以快速定义并复用自定义样式。用于快速定义并复用自定义样式。可以类比为通用的css样式
使用说明:

  • 当前@Styles仅支持通用属性和通用事件。
  • @Styles方法不支持参数
  • @Styles可以定义在组件内或全局,在全局定义时需在方法名前面添加function关键字,组件内定义时则不需要添加function关键字。
  • 定义在组件内的@Styles可以通过this访问组件的常量和状态变量,并可以在@Styles里通过事件来改变状态变量的值
  • 组件内@Styles的优先级高于全局@Styles。
    框架优先找当前组件内的@Styles,如果找不到,则会全局查找。
    // 定义在全局的@Styles封装的样式
    @Styles function globalFancy  () {
      .width(150)
      .height(100)
      .backgroundColor(Color.Pink)
    }

    @Entry
    @Component
    struct FancyUse {
      @State heightValue: number = 100
      // 定义在组件内的@Styles封装的样式
      @Styles fancy() {
        .width(200)
        .height(this.heightValue)
        .backgroundColor(Color.Yellow)
        .onClick(() => {
          this.heightValue = 200
        })
      }

      build() {
        Column({ space: 10 }) {
          // 使用全局的@Styles封装的样式
          Text('FancyA')
            .globalFancy ()
            .fontSize(30)
          // 使用组件内的@Styles封装的样式
          Text('FancyB')
            .fancy()
            .fontSize(30)
        }
      }
    }

@Extend装饰器:定义扩展组件样式

使用说明:
@Extend(UIComponentName) function functionName { ... }
使用规则:

  • 和@Styles不同,@Extend仅支持定义在全局,不支持在组件内部定义。
  • 和@Styles不同,@Extend支持封装指定的组件的私有属性和私有事件和预定义相同组件的@Extend的方法。
        // @Extend(Text)可以支持Text的私有属性fontColor
      @Extend(Text) function fancy () {
        .fontColor(Color.Red)
      }
      // superFancyText可以调用预定义的fancy
      @Extend(Text) function superFancyText(size:number) {
          .fontSize(size)
          .fancy()
      }
    
  • 和@Styles不同,@Extend装饰的方法支持参数,开发者可以在调用时传递参数,调用遵循TS方法传值调用。
        // xxx.ets
        @Extend(Text) function fancy (fontSize: number) {
          .fontColor(Color.Red)
          .fontSize(fontSize)
        }
    
        @Entry
        @Component
        struct FancyUse {
          build() {
            Row({ space: 10 }) {
              Text('Fancy')
                .fancy(16)
              Text('Fancy')
                .fancy(24)
            }
          }
        }
    
  • @Extend装饰的方法的参数可以为function,作为Event事件的句柄。
      @Extend(Text) function makeMeClick(onClick: () => void) {
        .backgroundColor(Color.Blue)
        .onClick(onClick)
      }
    
      @Entry
      @Component
      struct FancyUse {
        @State label: string = 'Hello World';
    
        onClickHandler() {
          this.label = 'Hello ArkUI';
        }
    
        build() {
          Row({ space: 10 }) {
            Text(`${this.label}`)
              .makeMeClick(this.onClickHandler.bind(this))
          }
        }
      }
    
  • @Extend的参数可以为状态变量,当状态变量改变时,UI可以正常的被刷新渲染。
      @Extend(Text) function fancy (fontSize: number) {
        .fontColor(Color.Red)
        .fontSize(fontSize)
      }
    
      @Entry
      @Component
      struct FancyUse {
        @State fontSizeValue: number = 20
        build() {
          Row({ space: 10 }) {
            Text('Fancy')
              .fancy(this.fontSizeValue)
              .onClick(() => {
                this.fontSizeValue = 30
              })
          }
        }
      }
    

stateStyles:多态样式

@Styles和@Extend仅仅应用于静态页面的样式复用,stateStyles可以依据组件的内部状态的不同,快速设置不同样式。这就是我们本章要介绍的内容stateStyles(又称为:多态样式)。
stateStyles是属性方法,可以根据UI内部状态来设置样式,类似于css伪类,但语法不同。ArkUI提供以下四种状态:

  • focused:获焦态
  • normal:正常态。
  • pressed:按压态。
  • disabled:不可用态。
  1. @Styles和stateStyles联合使用
    正常态和按压态切换
    @Entry
    @Component
    struct MyComponent {
      @Styles normalStyle() {
        .backgroundColor(Color.Gray)
      }
    
      @Styles pressedStyle() {
        .backgroundColor(Color.Red)
      }
    
      build() {
        Column() {
          Text('Text1')
            .fontSize(50)
            .fontColor(Color.White)
            .stateStyles({
              normal: this.normalStyle,
              pressed: this.pressedStyle,
            })
        }
      }
    }
    
  2. 在stateStyles里使用常规变量和状态变量
    Button默认获焦显示红色,点击事件触发后,获焦态变为粉色。
     @Entry
     @Component
     struct CompWithInlineStateStyles {
       @State focusedColor: Color = Color.Red;
       normalColor: Color = Color.Green
    
       build() {
         Button('clickMe').height(100).width(100)
           .stateStyles({
             normal: {
               .backgroundColor(this.normalColor)
             },
             focused: {
               .backgroundColor(this.focusedColor)
             }
           })
           .onClick(() => {
             this.focusedColor = Color.Pink
           })
           .margin('30%')
       }
     }
    

ArkTs状态管理

  • @State:@State装饰的变量拥有其所属组件的状态,可以作为其子组件单向和双向同步的数据源。当其数值改变时,会引起相关组件的渲染刷新。
  • @Prop:@Prop装饰的变量可以和父组件建立单向同步关系,@Prop装饰的变量是可变的,但修改不会同步回父组件。
  • @Link:@Link装饰的变量和父组件构建双向同步关系的状态变量,父组件会接受来自@Link装饰的变量的修改的同步,父组件的更新也会同步给@Link装饰的变量。
  • @Provide/@Consume:@Provide/@Consume装饰的变量用于跨组件层级(多层组件)同步状态变量,可以不需要通过参数命名机制传递,通过alias(别名)或者属性名绑定。
  • @Observed:@Observed装饰class,需要观察多层嵌套场景的class需要被@Observed装饰。单独使用@Observed没有任何作用,需要和@ObjectLink、@Prop连用。
  • @ObjectLink:@ObjectLink装饰的变量接收@Observed装饰的class的实例,应用于观察多层嵌套场景,和父组件的数据源构建双向同步。

管理应用拥有的状态概述:
装饰器仅能在页面内,即一个组件树上共享状态变量。如果开发者要实现应用级的,或者多个页面的状态数据共享,就需要用到应用级别的状态管理的概念。ArkTS根据不同特性,提供了多种应用状态管理的能力:

  • LocalStorage:页面级UI状态存储,通常用于UIAbility内、页面间的状态共享。
  • AppStorage:特殊的单例LocalStorage对象,由UI框架在应用程序启动时创建,为应用程序UI状态属性提供中央存储;
  • PersistentStorage:持久化存储UI状态,通常和AppStorage配合使用,选择AppStorage存储的数据写入磁盘,以确保这些属性在应用程序重新启动时的值与应用程序关闭时的值相同;LocalStorage和AppStorage都是运行时的内存,但是在应用退出再次启动后,依然能保存选定的结果,是应用开发中十分常见的现象,这就需要用到PersistentStorage。
    注意: PersistentStorage的持久化变量最好是小于2kb的数据,不要大量的数据持久化,因为PersistentStorage写入磁盘的操作是同步的,大量的数据本地化读写会同步在UI线程中执行,影响UI渲染性能。如果开发者需要存储大量的数据,建议使用数据库api。PersistentStorage只能在UI页面内使用,否则将无法持久化数据。
  • Environment:应用程序运行的设备的环境参数,环境参数会同步到AppStorage中,可以和AppStorage搭配使用。

ArkTs渲染控制

ArkUI通过自定义组件的build()函数和@builder装饰器中的声明式UI描述语句构建相应的UI。在声明式描述语句中开发者除了使用系统组件外,还可以使用渲染控制语句来辅助UI的构建,这些渲染控制语句包括控制组件是否显示的条件渲染语句,基于数组数据快速生成组件的循环渲染语句以及针对大数据量场景的数据懒加载语句。

  • if/else:条件渲染
    更新机制:
    当if、else if后跟随的状态判断中使用的状态变量值变化时,条件渲染语句会进行更新,更新步骤如下:

    1. 评估if和else if的状态判断条件,如果分支没有变化,请无需执行以下步骤。如果分支有变化,则执行2、3步骤:
    2. 删除此前构建的所有子组件。
    3. 执行新分支的构造函数,将获取到的组件添加到if父容器中。如果缺少适用的else分支,则不构建任何内容。

    条件可以包括Typescript表达式。对于构造函数中的表达式,此类表达式不得更改应用程序状态。

    @Component
    struct CounterView {
      @State counter: number = 0;
      label: string = 'unknown';
    
      build() {
        Row() {
          Text(`${this.label}`)
          Button(`counter ${this.counter} +1`)
            .onClick(() => {
              this.counter += 1;
            })
        }
      }
    }
    
    @Entry
    @Component
    struct MainView {
      @State toggle: boolean = true;
    
      build() {
        Column() {
          if (this.toggle) {
            CounterView({ label: 'CounterView #positive' })
          } else {
            CounterView({ label: 'CounterView #negative' })
          }
          Button(`toggle ${this.toggle}`)
            .onClick(() => {
              this.toggle = !this.toggle;
            })
        }
      }
    }
    
  • ForEach:循环渲染
    ForEach基于数组类型数据执行循环渲染。

    • forEach接口描述
      ForEach(
        arr: any[], 
        itemGenerator: (item: any, index?: number) => void,
        keyGenerator?: (item: any, index?: number) => string 
      )
      
    • 使用限制:
      1. ForEach必须在容器组件内使用。
      2. 生成的子组件应当是允许包含在ForEach父容器组件中的子组件。
      3. 允许子组件生成器函数中包含if/else条件渲染,同时也允许ForEach包含在if/else条件渲染语句中。
      4. itemGenerator函数的调用顺序不一定和数组中的数据项相同,在开发过程中不要假设itemGenerator和keyGenerator函数是否执行及其执行顺序。例如,以下示例可能无法正确运行:
         ForEach(anArray.map((item1, index1) => { return { i: index1 + 1, data: item1 }; }), 
         item => Text(`${item.i}. item.data.label`),
         item => item.data.id.toString())
        
  • LazyForEach:数据懒加载
    LazyForEach从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当LazyForEach在滚动容器中使用了,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会进行组件销毁回收以降低内存占用。

    • 接口描述
      LazyForEach(
          dataSource: IDataSource,             // 需要进行数据迭代的数据源 
          itemGenerator: (item: any) => void,  // 子组件生成函数
          keyGenerator?: (item: any) => string // (可选) .键值生成函数
      ): void
      
    • 使用限制
      • LazyForEach必须在容器组件内使用,仅有List、Grid以及Swiper组件支持数据懒加载(可配置cachedCount属性,即只加载可视部分以及其前后少量数据用于缓冲),其他组件仍然是一次性加载所有的数据。
      • LazyForEach在每次迭代中,必须创建且只允许创建一个子组件。
      • 生成的子组件必须是允许包含在LazyForEach父容器组件中的子组件。
      • 允许LazyForEach包含在if/else条件渲染语句中,也允许LazyForEach中出现if/else条件渲染语句。
      • 键值生成器必须针对每个数据生成唯一的值,如果键值相同,将导致键值相同的UI组件被框架忽略,从而无法在父容器内显示。
      • LazyForEach必须使用DataChangeListener对象来进行更新,第一个参数dataSource使用状态变量时,状态变量改变不会触发LazyForEach的UI刷新。
      • 为了高性能渲染,通过DataChangeListener对象的onDataChange方法来更新UI时,需要生成不同于原来的键值来触发组件刷新。
      • itemGenerator函数的调用顺序不一定和数据源中的数据项相同,在开发过程中不要假设itemGenerator和keyGenerator函数是否执行及其执行顺序。例如,以下示例可能无法正常运行:
         LazyForEach(dataSource, 
           item => Text(`${item.i}. item.data.label`),
           item => item.data.id.toString())
        
    • 示例
      // Basic implementation of IDataSource to handle data listener
      class BasicDataSource implements IDataSource {
        private listeners: DataChangeListener[] = [];
      
        public totalCount(): number {
          return 0;
        }
      
        public getData(index: number): any {
          return undefined;
        }
      
        registerDataChangeListener(listener: DataChangeListener): void {
          if (this.listeners.indexOf(listener) < 0) {
            console.info('add listener');
            this.listeners.push(listener);
          }
        }
      
        unregisterDataChangeListener(listener: DataChangeListener): void {
          const pos = this.listeners.indexOf(listener);
          if (pos >= 0) {
            console.info('remove listener');
            this.listeners.splice(pos, 1);
          }
        }
      
        notifyDataReload(): void {
          this.listeners.forEach(listener => {
            listener.onDataReloaded();
          })
        }
      
        notifyDataAdd(index: number): void {
          this.listeners.forEach(listener => {
            listener.onDataAdd(index);
          })
        }
      
        notifyDataChange(index: number): void {
          this.listeners.forEach(listener => {
            listener.onDataChange(index);
          })
        }
      
        notifyDataDelete(index: number): void {
          this.listeners.forEach(listener => {
            listener.onDataDelete(index);
          })
        }
      
        notifyDataMove(from: number, to: number): void {
          this.listeners.forEach(listener => {
            listener.onDataMove(from, to);
          })
        }
      }
      
      class MyDataSource extends BasicDataSource {
        private dataArray: string[] = [];
      
        public totalCount(): number {
          return this.dataArray.length;
        }
      
        public getData(index: number): any {
          return this.dataArray[index];
        }
      
        public addData(index: number, data: string): void {
          this.dataArray.splice(index, 0, data);
          this.notifyDataAdd(index);
        }
      
        public pushData(data: string): void {
          this.dataArray.push(data);
          this.notifyDataAdd(this.dataArray.length - 1);
        }
      }
      
      @Entry
      @Component
      struct MyComponent {
        aboutToAppear() {
          for (var i = 100; i >= 80; i--) {
            this.data.pushData(`Hello ${i}`)
          }
        }
      
        private data: MyDataSource = new MyDataSource();
      
        build() {
          List({ space: 3 }) {
            LazyForEach(this.data, (item: string) => {
              ListItem() {
                Row() {
                  Text(item).fontSize(50)
                    .onAppear(() => {
                      console.info("appear:" + item)
                    })
                }.margin({ left: 10, right: 10 })
              }
              .onClick(() => {
                this.data.pushData(`Hello ${this.data.totalCount()}`);
              })
            }, item => item)
          }.cachedCount(5)
        }
      }
      

stage模型应用

Stage模型开发概述

[图片上传失败...(image-ed30bb-1697789591540)]

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

    • UIAbility组件是一种包含UI界面的应用组件,主要用于和用户交互。
    • ExtensionAbility组件是一种面向特定场景的应用组件。
  • WindowStage
    每个UIAbility类实例都会与一个WindowStage类实例绑定,该类提供了应用进程内窗口管理器的作用。它包含一个主窗口。也就是说UIAbility通过WindowStage持有了一个窗口,该窗口为ArkUI提供了绘制区域。

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

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

Stage模型应用/组件级配置

  • 应用包名配置
    应用需要在工程的AppScope目录下的app.json5配置文件中配置bundleName标签,该标签用于标识应用的唯一性。推荐采用反域名形式命名(如com.example.demo,建议第一级为域名后缀com,第二级为厂商/个人名,第三级为应用名,也可以多级)。
  • 应用图标和标签配置
    Stage模型的应用需要配置应用图标和应用标签。应用图标和标签是在设置应用中使用,例如设置应用中的应用列表,会显示出对应的图标和标签。
    应用图标需要在工程的AppScope目录下的app.json5配置文件中配置icon标签。应用图标需配置为图片的资源索引,配置完成后,该图片即为应用的图标。
    应用标签需要在工程的AppScope模块下的app.json5配置文件中配置label标签。标识应用对用户显示的名称,需要配置为字符串资源的索引。
  • 入口图标和标签配置
    Stage模型支持对组件配置入口图标和入口标签。入口图标和入口标签会显示在桌面上。
    入口图标需要在module.json5配置文件中配置,在abilities标签下面有icon标签。例如希望在桌面上显示该UIAbility的图标,则需要在skills标签下面的entities中添加"entity.system.home"、actions中添加"action.system.home"。同一个应用有多个UIAbility配置上述字段时,桌面上会显示出多个图标,分别对应各自的UIAbility。

UIAbility组件概述

  • UIAbility概述:
    UIAbility组件是一种包含UI界面的应用组件,主要用于和用户交互。
    UIAbility组件是系统调度的基本单元,为应用提供绘制界面的窗口;一个UIAbility组件中可以通过多个页面来实现一个功能模块。每一个UIAbility组件实例,都对应于一个最近任务列表中的任务。

  • UIAbility组件生命周期

    • UIAbility的生命周期包括Create、Foreground、Background、Destroy四个状态
  • UIAbility组件启动模式

    • singleton(单实例模式)
      每次调用startAbility()方法时,如果应用进程中该类型的UIAbility实例已经存在,则复用系统中的UIAbility实例。系统中只存在唯一一个该UIAbility实例,即在最近任务列表中只存在一个该类型的UIAbility实例。
      如果需要使用singleton启动模式,在module.json5配置文件中的"launchType"字段配置为"singleton"即可。
          {
          "module": {
            // ...
            "abilities": [
              {
                "launchType": "singleton",
                // ...
              }
            ]
          }
        }
      
    • standard(标准实例模式)
      standard启动模式为标准实例模式,每次调用startAbility()方法时,都会在应用进程中创建一个新的该类型UIAbility实例。即在最近任务列表中可以看到有多个该类型的UIAbility实例。这种情况下可以将UIAbility配置为standard(标准实例模式)。
      standard启动模式的开发使用,在module.json5配置文件中的"launchType"字段配置为"standard"即可。
          {
          "module": {
            // ...
            "abilities": [
              {
                "launchType": "standard",
                // ...
              }
            ]
          }
        }
      
    • specified(指定实例模式)
      specified启动模式为指定实例模式,针对一些特殊场景使用(例如文档应用中每次新建文档希望都能新建一个文档实例,重复打开一个已保存的文档希望打开的都是同一个文档实例)。
      在UIAbility实例创建之前,允许开发者为该实例创建一个唯一的字符串Key,创建的UIAbility实例绑定Key之后,后续每次调用startAbility()方法时,都会询问应用使用哪个Key对应的UIAbility实例来响应startAbility()请求。运行时由UIAbility内部业务决定是否创建多实例,如果匹配有该UIAbility实例的Key,则直接拉起与之绑定的UIAbility实例,否则创建一个新的UIAbility实例。
  • UIAbility组件基本用法
    UIAbility组件的基本用法包括:指定UIAbility的启动页面以及获取UIAbility的上下文UIAbilityContext。

    • 指定UIAbility的启动页面
      应用中的UIAbility在启动过程中,需要指定启动页面,否则应用启动后会因为没有默认加载页面而导致白屏。可以在UIAbility的onWindowStageCreate()生命周期回调中,通过WindowStage对象的loadContent()方法设置启动页面。

        import UIAbility from '@ohos.app.ability.UIAbility';
        import Window from '@ohos.window';
      
          export default class EntryAbility extends UIAbility {
            onWindowStageCreate(windowStage: Window.WindowStage) {
                // Main window is created, set main page for this ability
                windowStage.loadContent('pages/Index', (err, data) => {
                    // ...
                });
            }
      
            // ...
        }
      
    • 获取UIAbility的上下文信息

      • 在UIAbility中可以通过this.context获取UIAbility实例的上下文信息。
        let context = this.context;
      • 在页面中获取UIAbility实例的上下文信息,包括导入依赖资源context模块和在组件中定义一个context变量两个部分。
        import common from '@ohos.app.ability.common';
        @Entry
        @Component
        struct Index {
          private context = getContext(this) as common.UIAbilityContext;
        
          startAbilityTest() {
            let want = {
              // Want参数信息
            };
            this.context.startAbility(want);
          }
        
          // 页面展示
          build() {
            // ...
          }
        }
        
        也可以在导入依赖资源context模块后,在具体使用UIAbilityContext前进行变量定义。
          startAbilityTest() {
          let context = getContext(this) as common.UIAbilityContext;
          let want = {
            // Want参数信息
          };
          context.startAbility(want);
        }
        
  • UIAbility组件与UI的数据同步
    基于HarmonyOS的应用模型,可以通过以下两种方式来实现UIAbility组件与UI之间的数据同步。

    1. EventHub:基于发布订阅模式来实现,事件需要先订阅后发布,订阅者收到消息后进行处理。
      可类比为android中的EventBus
    2. globalThis:ArkTS引擎实例内部的一个全局对象,在ArkTS引擎实例内部都能访问。
      使用globalThis进行数据同步[图片上传失败...(image-852b6f-1697789591540)]
      • UIAbility和Page之间使用globalThis
        globalThis为ArkTS引擎实例下的全局对象,可以通过globalThis绑定属性/方法来进行UIAbility组件与UI的数据同步。例如在UIAbility组件中绑定want参数,即可在UIAbility对应的UI界面上使用want参数信息。
        1. 调用startAbility()方法启动一个UIAbility实例时,被启动的UIAbility创建完成后会进入onCreate()生命周期回调,且在onCreate()生命周期回调中能够接受到传递过来的want参数,可以将want参数绑定到globalThis上。
            import UIAbility from '@ohos.app.ability.UIAbility'
          
            export default class EntryAbility extends UIAbility {
                onCreate(want, launch) {
                    globalThis.entryAbilityWant = want;
                    // ...
                }
          
                // ...
            }
          
        2. 在UI界面中即可通过globalThis获取到want参数信息。
          let entryAbilityWant;
          
          @Entry
          @Component
          struct Index {
            aboutToAppear() {
              entryAbilityWant = globalThis.entryAbilityWant;
            }
          
            // 页面展示
            build() {
              // ...
            }
          }
          
      • UIAbility和UIAbility之间使用globalThis
        同理可类比上述使用方式
      • UIAbility和ExtensionAbility之间使用globalThis
        同理可类比上述使用方式
      • globalThis使用的注意事项
        • Stage模型下进程内的UIAbility组件共享ArkTS引擎实例,使用globalThis时需要避免存放相同名称的对象。例如AbilityA和AbilityB可以使用globalThis共享数据,在存放相同名称的对象时,先存放的对象会被后存放的对象覆盖。
        • FA模型因为每个UIAbility组件之间引擎隔离,不会存在该问题。
        • 对于绑定在globalThis上的对象,其生命周期与ArkTS虚拟机实例相同,建议在使用完成之后将其赋值为null,以减少对应用内存的占用。
  • UIAbility组件间交互(设备内)
    方法类比原生Android

AbilityStage组件容器

AbilityStage是一个Module级别的组件容器,应用的HAP在首次加载时会创建一个AbilityStage实例,可以对该Module进行初始化等操作。

AbilityStage与Module一一对应,即一个Module拥有一个AbilityStage。
我理解可以类比为Android的Application

具体创建步骤如下:

  1. 在工程Module对应的ets目录下,右键选择“New > Directory”,新建一个目录并命名为myabilitystage。
  2. 在myabilitystage目录,右键选择“New > TypeScript File”,新建一个TypeScript文件并命名为MyAbilityStage.ts。
  3. 打开MyAbilityStage.ts文件,导入AbilityStage的依赖包,自定义类继承AbilityStage并加上需要的生命周期回调,示例中增加了一个onCreate()生命周期回调。
  4. 在module.json5配置文件中,通过配置srcEntry参数来指定模块对应的代码路径,以作为HAP加载的入口。
     {
     "module": {
       "name": "entry",
       "type": "entry",
       "srcEntry": "./ets/myabilitystage/MyAbilityStage.ts",
       ...
     }
    }
    

AbilityStage拥有onCreate()生命周期回调和onAcceptWant()、onConfigurationUpdated()、onMemoryLevel()事件回调。

  • onCreate()生命周期回调:在开始加载对应Module的第一个UIAbility实例之前会先创建 AbilityStage,并在AbilityStage创建完成之后执行其onCreate()生命周期回调。AbilityStage模块提供在Module加载的时候,通知开发者,可以在此进行该Module的初始化(如资源预加载,线程创建等)能力。
  • onAcceptWant()事件回调:UIAbility指定实例模式(specified)启动时候触发的事件回调,具体使用请参见UIAbility启动模式综述。
  • onConfigurationUpdated()事件回调:当系统全局配置发生变更时触发的事件,系统语言、深浅色等,配置项目前均定义在Configuration类中。
  • onMemoryLevel()事件回调:当系统调整内存时触发的事件。

应用被切换到后台时,系统会将在后台的应用保留在缓存中。即使应用处于缓存中,也会影响系统整体性能。当系统资源不足时,系统会通过多种方式从应用中回收内存,必要时会完全停止应用,从而释放内存用于执行关键任务。为了进一步保持系统内存的平衡,避免系统停止用户的应用进程,可以在AbilityStage中的onMemoryLevel()生命周期回调中订阅系统内存的变化情况,释放不必要的资源。

应用上下文Context

[图片上传失败...(image-fac77d-1697789591541)]

订阅进程内Ability生命周期变化

在应用内的DFX统计场景,如需要统计对应页面停留时间和访问频率等信息,可以使用订阅进程内Ability生命周期变化功能。

在进程内Ability生命周期变化时,如创建、可见/不可见、获焦/失焦、销毁等,会触发进入相应的回调,其中返回的此次注册监听生命周期的ID(每次注册该ID会自增+1,当超过监听上限数量2^63-1时,返回-1),以在UIAbilityContext中使用为例进行说明。

import UIAbility from '@ohos.app.ability.UIAbility';
import Window from '@ohos.window';

const TAG: string = "[Example].[Entry].[EntryAbility]";

export default class EntryAbility extends UIAbility {
    lifecycleId: number;

    onCreate(want, launchParam) {
        let abilityLifecycleCallback = {
            onAbilityCreate(ability) {
                console.info(TAG, "onAbilityCreate ability:" + JSON.stringify(ability));
            },
            onWindowStageCreate(ability, windowStage) {
                console.info(TAG, "onWindowStageCreate ability:" + JSON.stringify(ability));
                console.info(TAG, "onWindowStageCreate windowStage:" + JSON.stringify(windowStage));
            },
            onWindowStageActive(ability, windowStage) {
                console.info(TAG, "onWindowStageActive ability:" + JSON.stringify(ability));
                console.info(TAG, "onWindowStageActive windowStage:" + JSON.stringify(windowStage));
            },
            onWindowStageInactive(ability, windowStage) {
                console.info(TAG, "onWindowStageInactive ability:" + JSON.stringify(ability));
                console.info(TAG, "onWindowStageInactive windowStage:" + JSON.stringify(windowStage));
            },
            onWindowStageDestroy(ability, windowStage) {
                console.info(TAG, "onWindowStageDestroy ability:" + JSON.stringify(ability));
                console.info(TAG, "onWindowStageDestroy windowStage:" + JSON.stringify(windowStage));
            },
            onAbilityDestroy(ability) {
                console.info(TAG, "onAbilityDestroy ability:" + JSON.stringify(ability));
            },
            onAbilityForeground(ability) {
                console.info(TAG, "onAbilityForeground ability:" + JSON.stringify(ability));
            },
            onAbilityBackground(ability) {
                console.info(TAG, "onAbilityBackground ability:" + JSON.stringify(ability));
            },
            onAbilityContinue(ability) {
                console.info(TAG, "onAbilityContinue ability:" + JSON.stringify(ability));
            }
        }
        // 1. 通过context属性获取applicationContext
        let applicationContext = this.context.getApplicationContext();
        // 2. 通过applicationContext注册监听应用内生命周期
        this.lifecycleId = applicationContext.on("abilityLifecycle", abilityLifecycleCallback);
        console.info(TAG, "register callback number: " + JSON.stringify(this.lifecycleId));
    }

    onDestroy() {
        let applicationContext = this.context.getApplicationContext();
        applicationContext.off("abilityLifecycle", this.lifecycleId, (error, data) => {
            console.info(TAG, "unregister callback success, err: " + JSON.stringify(error));
        });
    }
}

信息传递载体Want

作用类比于Android中的Intent

Want的类型

  • 显式Want:在启动Ability时指定了abilityName和bundleName的Want称为显式Want。
    当有明确处理请求的对象时,通过提供目标Ability所在应用的包名信息(bundleName),并在Want内指定abilityName便可启动目标Ability。显式Want通常用于在当前应用开发中启动某个已知的Ability。
      let wantInfo = {
        deviceId: '', // deviceId为空表示本设备
        bundleName: 'com.example.myapplication',
        abilityName: 'FuncAbility',
    }
    
  • 隐式的Want
    在启动UIAbility时未指定abilityName的Want称为隐式Want。
    当请求处理的对象不明确时,希望在当前应用中使用其他应用提供的某个能力(通过skills标签定义),而不关心提供该能力的具体应用,可以使用隐式Want。例如使用隐式Want描述需要打开一个链接的请求,而不关心通过具体哪个应用打开,系统将匹配声明支持该请求的所有应用。
      let wantInfo = {
        // uncomment line below if wish to implicitly query only in the specific bundle.
        // bundleName: 'com.example.myapplication',
        action: 'ohos.want.action.search',
        // entities can be omitted
        entities: [ 'entity.system.browsable' ],
        uri: 'https://www.test.com:8080/query/student',
        type: 'text/plain',
    };
    
    • 根据系统中待匹配Ability的匹配情况不同,使用隐式Want启动Ability时会出现以下三种情况。
      • 未匹配到满足条件的Ability:启动失败。
      • 匹配到一个满足条件的Ability:直接启动该Ability。
      • 匹配到多个满足条件的Ability(UIAbility):弹出选择框让用户选择。
    • 调用方传入的want参数中不带有abilityName和bundleName,则不允许通过隐式Want启动所有应用的ServiceExtensionAbility。
    • 调用方传入的want参数中带有bundleName,则允许使用startServiceExtensionAbility()方法隐式Want启动ServiceExtensionAbility,默认返回优先级最高的ServiceExtensionAbility,如果优先级相同,返回第一个。

Want参数说明在此

显式Want与隐式Want匹配规则在此

常见action与entities

action:表示调用方要执行的通用操作(如查看、分享、应用详情)。在隐式Want中,您可定义该字段,配合uri或parameters来表示对数据要执行的操作。如打开,查看该uri数据。例如,当uri为一段网址,action为ohos.want.action.viewData则表示匹配可查看该网址的Ability。在Want内声明action字段表示希望被调用方应用支持声明的操作。在被调用方应用配置文件skills字段内声明actions表示该应用支持声明操作。

  • 常见action
    • ACTION_HOME:启动应用入口组件的动作,需要和ENTITY_HOME配合使用;系统桌面应用图标就是显式的入口组件,点击也是启动入口组件;入口组件可以配置多个。
    • ACTION_CHOOSE:选择本地资源数据,例如联系人、相册等;系统一般对不同类型的数据有对应的Picker应用,例如联系人和图库。
    • ACTION_VIEW_DATA:查看数据,当使用网址uri时,则表示显示该网址对应的内容。
    • ACTION_VIEW_MULTIPLE_DATA:发送多个数据记录的操作。

entities:表示目标Ability的类别信息(如浏览器、视频播放器),在隐式Want中是对action的补充。在隐式Want中,开发者可定义该字段,来过滤匹配应用的类别,例如必须是浏览器。在Want内声明entities字段表示希望被调用方应用属于声明的类别。在被调用方应用配置文件skills字段内声明entites表示该应用支持的类别。

  • 常用entities
    • ENTITY_DEFAULT:默认类别无实际意义。
    • ENTITY_HOME:主屏幕有图标点击入口类别。
    • ENTITY_BROWSABLE:指示浏览器类别。

使用显式Want启动Ability在此

使用隐式Want打开网址

应用间使用Want分享数据

进程模型

  • 应用中(同一包名)的所有UIAbility运行在同一个独立进程中。
  • WebView拥有独立的渲染进程。

基于HarmonyOS的进程模型,系统提供了公共事件机制用于一对多的通信场景,公共事件发布者可能存在多个订阅者同时接收事件。

公共事件订阅概述

公共事件服务提供了动态订阅和静态订阅两种订阅方式。动态订阅与静态订阅最大的区别在于,动态订阅是应用运行时行为,而静态订阅是后台服务无需处于运行状态。

  • 动态订阅:指订阅方在运行时调用公共事件订阅的API实现对公共事件的订阅,详见动态订阅公共事件
  • 静态订阅:订阅方通过配置文件声明和实现继承自StaticSubscriberExtensionAbility的类实现对公共事件的订阅,详见静态订阅公共事件

动态订阅公共事件

场景介绍:
动态订阅是指当应用在运行状态时对某个公共事件进行订阅,在运行期间如果有订阅的事件发布那么订阅了这个事件的应用将会收到该事件及其传递的参数。例如,某应用希望在其运行期间收到电量过低的事件,并根据该事件降低其运行功耗,那么该应用便可动态订阅电量过低事件,收到该事件后关闭一些非必要的任务来降低功耗。订阅部分系统公共事件需要先申请权限,订阅这些事件所需要的权限请见公共事件权限列表

动态订阅接口说明

开发步骤:

  1. 导入CommonEvent模块。import commonEvent from '@ohos.commonEventManager';
  2. 创建订阅者信息,详细的订阅者信息数据类型及包含的参数请见CommonEventSubscribeInfo文档介绍。
     // 用于保存创建成功的订阅者对象,后续使用其完成订阅及退订的动作
     let subscriber = null;
     // 订阅者信息
     let subscribeInfo = {
         events: ["usual.event.SCREEN_OFF"], // 订阅灭屏公共事件
     }
    
  3. 创建订阅者,保存返回的订阅者对象subscriber,用于执行后续的订阅、退订等操作。
    // 创建订阅者回调
     commonEvent.createSubscriber(subscribeInfo, (err, data) => {
         if (err) {
             console.error(`[CommonEvent] CreateSubscriberCallBack err=${JSON.stringify(err)}`);
         } else {
             console.info(`[CommonEvent] CreateSubscriber success`);
             subscriber = data;
             // 订阅公共事件回调
         }
     })
    
  4. 创建订阅回调函数,订阅回调函数会在接收到事件时触发。订阅回调函数返回的data内包含了公共事件的名称、发布者携带的数据等信息,公共事件数据的详细参数和数据类型请见CommonEventData文档介绍。
     // 订阅公共事件回调
     if (subscriber !== null) {
         commonEvent.subscribe(subscriber, (err, data) => {
             if (err) {
                 console.error(`[CommonEvent] SubscribeCallBack err=${JSON.stringify(err)}`);
             } else {
                 console.info(`[CommonEvent] SubscribeCallBack data=${JSON.stringify(data)}`);
             }
         })
     } else {
         console.error(`[CommonEvent] Need create subscriber`);
     }
    

静态订阅公共事件(仅对系统应用开放)

静态订阅者在未接收订阅的目标事件时,处于未拉起状态,当系统或应用发布了指定的公共事件后,静态订阅者将被拉起,并执行onReceiveEvent回调,开发者可通过在onReceiveEvent回调中执行业务逻辑,实现当应用接收到特定公共事件时执行业务逻辑的目的。例如,某应用希望在设备开机的时候执行一些初始化任务,那么该应用可以静态订阅开机事件,在收到开机事件后会拉起该应用,然后执行初始化任务。静态订阅是通过配置文件声明和实现继承自StaticSubscriberExtensionAbility的类实现对公共事件的订阅。需要注意的是,静态订阅公共事件对系统功耗有一定影响,建议谨慎使用。

取消动态订阅公共事件

动态订阅者完成业务需要时,需要主动取消订阅,订阅者通过调用unsubscribe()方法取消订阅事件。
开发步骤:

  1. 导入CommonEvent模块。
    import commonEvent from '@ohos.commonEventManager';
  2. 根据动态订阅公共事件章节的步骤来订阅某个事件。
  3. 调用CommonEvent中的unsubscribe方法取消订阅某事件。
     // subscriber为订阅事件时创建的订阅者对象
     if (subscriber !== null) {
         commonEvent.unsubscribe(subscriber, (err) => {
             if (err) {
                 console.error(`[CommonEvent] UnsubscribeCallBack err=${JSON.stringify(err)}`)
             } else {
                 console.info(`[CommonEvent] Unsubscribe`)
                 subscriber = null
             }
         })
     }
    

公共事件发布

当需要发布某个自定义公共事件时,可以通过publish()方法发布事件。发布的公共事件可以携带数据,供订阅者解析并进行下一步处理。
接口说明

  • 发布不携带信息的公共事件(不携带信息的公共事件,只能发布无序公共事件。)

    1. 导入CommonEvent模块。
      import commonEvent from '@ohos.commonEventManager';
      
    2. 传入需要发布的事件名称和回调函数,发布事件。
       // 发布公共事件
       commonEvent.publish("usual.event.SCREEN_OFF", (err) => {
           if (err) {
               console.error(`[CommonEvent] PublishCallBack err=${JSON.stringify(err)}`);
           } else {
               console.info(`[CommonEvent] Publish success`);
           }
       })
      
  • 发布携带信息的公共事件:
    携带信息的公共事件,可以发布为无序公共事件、有序公共事件和粘性事件,可以通过参数CommonEventPublishData的isOrdered、isSticky的字段进行设置。

    1. 导入CommonEvent模块。
      import commonEvent from '@ohos.commonEventManager';
    2. 传入需要发布的事件名称和回调函数,发布事件。
       // 公共事件相关信息
       let options = {
           code: 1, // 公共事件的初始代码
           data: "initial data", // 公共事件的初始数据
       }
      
    3. 传入需要发布的事件名称、需要发布的指定信息和回调函数,发布事件。
       // 发布公共事件
       commonEvent.publish("usual.event.SCREEN_OFF", options, (err) => {
           if (err) {
               console.error('[CommonEvent] PublishCallBack err=' + JSON.stringify(err));
           } else {
               console.info('[CommonEvent] Publish success')
           }
       })
      

线程模型

线程模型概述

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

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

除主线程外,还有一类与主线程并行的独立线程Worker,主要用于执行耗时操作,但不可以直接操作UI。Worker线程在主线程中创建,与主线程相互独立。最多可以创建8个Worker

基于HarmonyOS的线程模型,不同的业务功能运行在不同的线程上,业务功能的交互就需要线程间通信。线程间通信目前主要有EmitterWorker两种方式,其中Emitter主要适用于线程间的事件同步Worker主要用于新开一个线程执行耗时任务

  • 注意: Stage模型只提供了主线程和Worker线程,Emitter主要用于主线程内或者主线程和Worker线程的事件同步。

使用Emitter进行线程间通信

Emitter主要提供线程间发送和处理事件的能力,包括对持续订阅事件或单次订阅事件的处理、取消订阅事件、发送事件到事件队列等。

  • 开发步骤
    1. 订阅事件
       import emitter from "@ohos.events.emitter";
      
       // 定义一个eventId为1的事件
       let event = {
           eventId: 1
       };
      
       // 收到eventId为1的事件后执行该回调
       let callback = (eventData) => {
           console.info('event callback');
       };
      
       // 订阅eventId为1的事件
       emitter.on(event, callback);
      
    2. 发送事件
       import emitter from "@ohos.events.emitter";
      
       // 定义一个eventId为1的事件,事件优先级为Low
       let event = {
           eventId: 1,
           priority: emitter.EventPriority.LOW
       };
      
       let eventData = {
           data: {
               "content": "c",
               "id": 1,
               "isEmpty": false,
           }
       };
      
       // 发送eventId为1的事件,事件内容为eventData
       emitter.emit(event, eventData);
      

使用Worker进行线程间通信

Worker是与主线程并行的独立线程。创建Worker的线程被称为宿主线程,Worker工作的线程被称为Worker线程。创建Worker时传入的脚本文件在Worker线程中执行,通常在Worker线程中处理耗时的操作,需要注意的是,Worker中不能直接更新Page。

  • 开发步骤
    1. 在工程的模块级build-profile.json5文件的buildOption属性中添加配置信息。
         "buildOption": {
         "sourceOption": {
           "workers": [
             "./src/main/ets/workers/worker.ts"
           ]
         }
       }
      
    2. 根据build-profile.json5中的配置创建对应的worker.ts文件。
       import worker from '@ohos.worker';
      
       let parent = worker.workerPort;
      
       // 处理来自主线程的消息
       parent.onmessage = function(message) {
           console.info("onmessage: " + message)
           // 发送消息到主线程
           parent.postMessage("message from worker thread.")
       }
      
    3. 主线程中使用如下方式初始化和使用worker。
      • Stage模型
         import worker from '@ohos.worker';
        
         let wk = new worker.ThreadWorker("entry/ets/workers/worker.ts");
        
         // 发送消息到worker线程
         wk.postMessage("message from main thread.")
        
         // 处理来自worker线程的消息
         wk.onmessage = function(message) {
             console.info("message from worker: " + message)
        
             // 根据业务按需停止worker线程
             wk.terminate()
         }
        
  • 说明:
    • build-profile.json5中配置的worker.ts的相对路径都为./src/main/ets/workers/worker.ts时,在Stage模型下创建worker需要传入路径entry/ets/workers/worker.ts;在FA模型下创建worker需要传入路径../workers/worker.ts。

    • 主线程与Worker线程间支持的数据类型参考序列化支持类型

    • 传递通过自定义class创建出来的object时,不会发生序列化错误,但是自定义class的属性(如Function)无法通过序列化传递。

    • Worker存在数量限制,当前支持最多同时存在8个Worker。

    • 主动销毁Worker可以调用新创建Worker对象的terminate()或workerPort.close()方法。

    • 自API version 9版本开始,若Worker处于已经销毁或正在销毁等非运行状态时,调用其功能接口,会抛出相应的BusinessError。

    • Worker的创建和销毁耗费性能,建议管理已创建的Worker并重复使用。

    • 创建Worker工程时,new worker.Worker构造函数和new worker.ThreadWorker构造函数不能同时使用,否则将导致工程中Worker的功能异常。自API version 9版本开始,建议使用new worker.ThreadWorker构造函数,在API version 8及之前的版本,建议使用new worker.Worker构造函数。

    • 创建Worker工程时,在Worker线程的文件中(比如本文中worker.ts)不能导入任何有关构建UI的方法(比如ETS文件等),否则会导致Worker的功能失效。排查方式:解压生成的Hap包,在创建Worker线程的文件目录中找到"worker.js",全局搜索"View"关键字。如果存在该关键字,说明在worker.js中打包进去了构建UI的方法,会导致Worker的功能失效,建议在创建Worker线程的文件中修改 "import “xxx” from src"中src的目录层级。

    • 线程间通信时传递的数据量最大限制为16M。

UI开发

方舟开发框架(ArkUI)概述

开发布局

  • 线性布局(Row、Column)
    如果布局内子元素超过1个,且能够以某种方式线性排列时优先考虑此布局。

  • 层叠布局(Stack)
    组件需要有堆叠效果时优先考虑此布局,层叠布局的堆叠效果不会占用或影响其他同容器内子组件的布局空间。例如Panel作为子组件弹出时将其他组件覆盖更为合理,则优先考虑在外层使用堆叠布局。

  • 弹性布局(Flex)
    弹性布局是与线性布局类似的布局方式。区别在于弹性布局默认能够使子组件压缩或拉伸。在子组件需要计算拉伸或压缩比例时优先使用此布局,可使得多个容器内子组件能有更好的视觉上的填充容器效果。

  • 相对布局(RelativeContainer)
    相对布局是在二维空间中的布局方式,不需要遵循线性布局的规则,布局方式更为自由。通过在子组件上设置锚点规则(AlignRules)使子组件能够将自己在横轴、纵轴中的位置与容器或容器内其他子组件的位置对齐。设置的锚点规则可以天然支持子元素压缩、拉伸,堆叠或形成多行效果。在页面元素分布复杂或通过线性布局会使容器嵌套层数过深时推荐使用。

  • 栅格布局(GridRow、GridCol)
    栅格是多设备场景下通用的辅助定位工具,通过将空间分割为有规律的栅格。栅格不同于网格布局固定的空间划分,它可以实现不同设备下不同的布局,空间划分更随心所欲,从而显著降低适配不同屏幕尺寸的设计及开发成本,使得整体设计和开发流程更有秩序和节奏感,同时也保证多设备上应用显示的协调性和一致性,提升用户体验。推荐手机、大屏、平板等不同设备,内容相同但布局不同时使用。

  • 媒体查询(@ohos.mediaquery)
    媒体查询可根据不同设备类型或同设备不同状态修改应用的样式。例如根据设备和应用的不同属性信息设计不同的布局,以及屏幕发生动态改变时更新应用的页面布局。

  • 列表(List)
    使用列表可以轻松高效地显示结构化、可滚动的信息。在ArkUI中,列表具有垂直和水平布局能力和自适应交叉轴方向上排列个数的布局能力,超出屏幕时可以滚动。列表适合用于呈现同类数据类型或数据类型集,例如图片和文本。

  • 网格(Grid)
    网格布局具有较强的页面均分能力,子组件占比控制能力,是一种重要自适应布局。网格布局可以控制元素所占的网格数量、设置子组件横跨几行或者几列,当网格容器尺寸发生变化时,所有子组件以及间距等比例调整。推荐在需要按照固定比例或者均匀分配空间的布局场景下使用,例如计算器、相册、日历等。

  • 轮播(Swiper)
    轮播组件通常用于实现广告轮播、图片预览、可滚动应用等。

添加组件

添加常用组件

添加气泡和菜单

设置页面路由和组件导航

页面路由router

组件导航

  • Navigation
    Navigation组件一般作为页面的根容器,包括单页面、分栏和自适应三种显示模式。同时,Navigation提供了属性来设置页面的标题栏、工具栏、导航栏等。

  • Tabs
    当页面信息较多时,为了让用户能够聚焦于当前显示的内容,需要对页面内容进行分类,提高页面空间利用率。Tabs组件可以在一个页面内快速实现视图内容的切换,一方面提升查找信息的效率,另一方面精简用户单次获取到的信息量。

    基本布局
    底部导航
    顶部导航
    侧边导航
    限制导航栏的滑动切换
    固定导航栏
    滚动导航栏
    自定义导航栏
    切换至指定页签
    滑动切换导航栏

显示图形

使用动画

页面内动画

支持交互事件

使用通用事件

  • 触屏事件
    • 点击事件
    • 拖拽事件
    • 触摸事件
  • 键鼠事件
    • 鼠标事件
    • 按键事件
  • 焦点事件

使用手势事件

Web相关

ArkTS语言基础类库概述

  • 并发
  • 容器类库

安全

网络与连接

电话服务

数据管理

文件管理

后台任务

设备管理

通知

窗口管理

WebGL

WebGL的全称为Web Graphic Library(网页图形库),主要用于交互式渲染2D图形和3D图形。目前HarmonyOS中使用的WebGL是基于OpenGL裁剪的OpenGL ES,可以在HTML5的canvas元素对象中使用,无需使用插件,支持跨平台。WebGL程序是由JavaScript代码组成的,其中使用的API可以利用用户设备提供的GPU硬件完成图形渲染和加速。更多信息请参考WebGL™标准。

媒体

自动化测试

Native Api相关

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容