# HarmonyOS4.0开发应用教程 **Repository Path**: kairen-13/HarmonyOS ## Basic Information - **Project Name**: HarmonyOS4.0开发应用教程 - **Description**: 1234567890 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2024-01-01 - **Last Updated**: 2024-11-14 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 前言 ![截图](f4f6a54540b7d460a62d5935eb4da0df.png) - HarmonyOs Design :用来做视觉设计,比如:应用中图标的设计,颜色的选择等待。 - ArkTS:鸿蒙开发的主力语言。 - ArkUI:基于ArkTS的UI框架。 - ArkCompiler:方舟编译器,它可以把我们编写的代码编写成字节码的形式,提高运行的效率。 - DevEco Studio:开发的工具,核心的代码部分都是用它来完成的。 - DevEco Testing: 测试工具。 - AppGallery Connect:它里面提供了很多云开发的功能,比如说:云函数,云存储,云数据库等等,这样能够大大降低开发和部署的成本,能够让应用快速上线。 # 授人以渔 [鸿蒙官网](https://developer.harmonyos.com/):基本UI设计->代码开发->上线。不懂的查看指南和API参考。 ![1.png](efda2d007dc912f04f3c7092fbf3075a.png) [OpenHarmony官网](https://openharmony.cn/mainPlay) ![截图](ba86499c0385ac68804df7ae8bb54591.png) [TypeScript官网](https://www.typescriptlang.org/):ark的语法是基于TypeScript的,官网顶部"Playground"可以进行在线测试。 [ArkUI实战](https://arkui.club/) [Openharmony主仓库](https://gitee.com/openharmony) [Openharmony三方库](https://gitee.com/openharmony-tpc/tpc_resource) [ArkUI沟通地图](https://3ms.huawei.com/km/blogs/details/13044953) ### 环境安装与配置 懂的都懂 # 了解ArkTS语言 如果用网页开发技术要实现一个按钮,每点击一次,自增1的效果,就得同时会三种不同的开发语言 ![截图](8ca07bc76b0ba7928743767761fdb34c.png) ![截图](ca96d75a7d97e29b5936054b04461a60.png) # ArkUI的关键特性 ## 极简的UI信息语法 ArkUI开发框架采用基于 TypeScript 扩展的极简的声明式UI描述界面语法,提供了类自然语言的UI描述和组合,开发者只需用几行简单直观的声明式代码,即可完成界面功能。 ## 丰富的内置UI组件 ArkUI开发框架内置了丰富而精美的多态组件,可满足大部分应用界面开发的需求,开发者可以轻松地向几乎任何UI控件添加动画并选择一系列框架内置的动画能力,可为用户带来平滑而自然的体验。其中多态是指UI描述是统一的,UI呈现在不同类型设备上会有所不同。比如 Button 组件在手机和手表会有不同的样式和交互方式。 ## 多维度的状态管理机制 ArkUI开发框架为开发者提供了跨设备数据绑定功能和多维度的状态管理机制(组件内/组件间/全局/分布式数据驱动UI变更),支持灵活的数据驱动的UI变更,帮助开发者节省70%代码完成跨端界面应用开发。 ## 支持多设备开发 ArkUI开发框架除了提供UI开发套件外还围绕着多设备开发提供了多维度的解决方案,进一步简化开发: - 基础开发能力:包括基础的分层参数配置(比如色彩、字号、圆角、间距等),栅格系统,原子化布局能力(比如拉伸、折行、隐藏等)。 - 零部件组件层:包括多态控件,统一交互能力,以及在此基础上的组件组合。 - 面向典型场景:提供分类的页面组合模板以及示例代码。 ## 原生性能体验 ArkUI开发框架内置了许多核心的UI控件和动效,如图片、列表、网格、属性动画、转场动画等,加持自研的 ArkCompiler 方舟编译器和 ArkRuntime 方舟运行时深度优化,这些都可以在 HarmonyOS / OpenHarmony 设备上达到移动原生应用一样的性能体验。 ## 实时预览机制 ArkUI开发框架支持实时界面预览特性可帮助开发快速的所见即所得的开发和调测界面,无需连接真机设备就可以显示应用界面在任何 HarmonyOS / OpenHarmony 设备上的UI效果,预览的关键特性主要包括: - 一致性渲染:和目标设备一致的UI呈现效果。 - 实时性预览:改动相应的代码,实时呈现出相应UI效果。另外,代码能够和UI双向联动,代码改动的同时UI也实时变更,UI改动的同时代码也相应地变更。 - 多维度预览:支持页面级预览、组件级预览、多设备预览。 # TypeScript基本语法 ## 变量声明 TypeScript在JavaScript的基础上加入了静态类型检查功能,因此每一个变量都有固定的数 据类型。 ```typescript //string:字符串,可以用单引号或双引号 let msg: string = 'hello world' //number:数值,整数、浮点数都可以 let age: number = 21 //boolean:布尔 let finished: boolean = true //any:不确定类型,可以是任意类型 let a: any ='jack' a = 21 //union:联合类型,可以是多个指定类型中的一种 let u: string|number|boolean 'rose' u = 18 //Object:对象 let p = {name: 'Jack', age: 21} console.log(p.name) console.log(p['name']) //Array:数组,元素可以是任意其它类型 let names: Array = ['Jack','Rose'] let ages:number[] = [21,18] console.log(names [0]) ``` ## 条件控制 #### if-else TypeScript与大多数开发语言类似,支持基于if-else和switch的条件控制 ```typescript //定义数字 let num:number = 21 //判断是否是偶数 if(num % 2 === 0){ console.log(num +'是偶数') }else{ console.log(num +'是奇数') } //判断是否是正数 if(num > 0){ console.log(num + ' 是正数') } else if(num < 0){ console.log(num + ' 是负数') } else console.log(num + ' 为0') } ``` ==:相等比较运算符 - 它会尝试进行类型转换,然后比较值是否相等。 - 不推荐在TypeScript中使用,因为它可能导致一些意外的类型转换,而且可能引入错误。 ===:严格相等比较运算符 - 它不会进行类型转换,只有在值和类型都相等的情况下才返回true。 - 推荐在TypeScript中使用,因为它更安全,避免了一些潜在的问题。 > 在TypeScript中,空字符串、数字0、null、undefined都被认为是false,其他则为true。 #### switch ```typescript let grade:string = 'A' switch (grade) { case 'A': { console.log('优秀') break case 'B': { console.og('合格') break } case 'C': { console.log('不合格') break } default: { console.log('非法输入') break } } ``` ## 循环迭代 TypeScript支持for和while循环,并且为一些内置类型如Array等提供了快捷迭代语法。 ```typescript //普通for for(let i= 1; i <= 10; i++){ console.Log('点赞'+i+'次') } //while let i=1; while(i <10){ console.Log('点赞'+i+'次') i++; } //定义数组 let names: string[] = ['Jack','Rose'] //for in送代器,遍历得到数组角标 for (const i in names) { console.log(i + ':' + names[i]) } //for of送代器,直接得到元素 for (const name of names) { console.log(name) } ``` ## 函数 TypeScript:通常利用function关键字声明函数,并且支持可选参数、默认参数、箭头函数等特殊语法。 ```typescript //无返回值函数,返回值void可以省略 function sayHello(name: string): void{ console.log('你好,' + name + '!') } sayHello('Jack') //有返回值函数 function sum(x: number, y: number): number { return x + y } let result = sum(21,18) console.log('21 + 18 =' + result) //可选参数,在参数名后加?,表示该参数是可选的 function sayHello(name?: string){ //判断ame是否有值,如果无值则给一个默认值 name=name ? name : '陌生人' console.log('你好,' + name + '!') } sayHello('Jack') sayHello() //参数默认值,在参数后面赋值,表示参数默认值 //如果调用者没有传参,则使用默认值 function sayHello(name:string = '陌生人'){ console.log('你好,' + name + '!') } sayHello('Jack') sayHello() //箭头函数 let sayHi (name: string)=>{ console.log('你好,' + name + '!') } sayHi('Rose') ``` ## 类和接口 TypeScript具备面向对象编程的基本语法,例如interface、class、enum等。也具备封装、继承、多态等面向对象基本特征。 #### 多态 ```typescript //定义枚举 enum Msg{ HI 'Hi', HELLO 'Hello' } //定义接口,抽象方法接收枚举参数 interface A { say(msg:Msg):void } //实现接口 class B implements A { say(msg: Msg): void { console.log(msg + ', I am B') } } //初始化对象 //子类对象B赋值给了父类类型A,实现多态 let a:A = new B() //凋用方法,传递枚举参数 a.say(Msg.HI) ``` #### 封装和继承 ```typescript //定义矩形类 class Rectangle { //成员变量 private width:number private length:number //构造函数 constructor(width: number, length: number) { this.width = width this.length = length } //成员方法 public area(): number{ return this.width * this.length } } //定义正方形,继承矩形 class Square extends Rectangle{ constructor(side: number) { //调用父类构造 super(side,side) } } let s = new Square(10) console.log('正方形面积为: ' + s.area()) ``` > 在 TypeScript(以及 JavaScript 中),super 的使用有两个主要方面: > > - 调用父类的构造函数: > > 在子类的构造函数中使用 super() 来调用父类的构造函数,以确保父类的初始化逻辑也被执行。 > > ```typescript > class ChildClass extends ParentClass { > constructor() { > super(); // 调用父类的构造函数 > } > } > ``` > > - 访问父类的方法和属性: > > 在子类的方法中使用 super.methodName() 或 super.propertyName 来访问父类的方法或属性。 > > ```typescript > class ChildClass extends ParentClass { > // 访问父类的方法 > someMethod() { > super.someMethod(); > } > > // 访问父类的属性 > get parentProperty() { > return super.parentProperty; > } > } > ``` ## 模块开发 应用复杂时,我们可以把通用功能抽取到单独的ts文件中,每个文件都是一个模块(module)。模块可以相互加载,提高代码复用性。 ```typescript //*****************************************rectangle.ts //定义矩形类,并通过export导出 export class Rectangle { //成员变量 public width: number public length: number //构造函数 constructor(width: number, length: number) { this.width = width this.length = length } } //定义工具方法,求矩形面积,并通过export导出 export function area(rec: Rectangle): number { return rec.width rec.length } ```
```typescript //*****************************************index.ts //通过mport语法导入,from后面写文件的地址 import {Rectangle, area} from '../rectangle' //创建歌ectangle,对象 let r = new Rectangle(10,20) //凋用area方法 console.log('面积为:' + area(r)) ``` # 快速入门 ## 创建ets工程 1. 打开 DevEco Studio,点击 File > New > Create Project,由于我们的hap是结合C+ +来实现的,在 Choose Your Ability Template 下选择模板"Application",选中 Native C++ 模板,点击 Next 进入下一步配置。 2. 进入配置工程界面,Project name 填写项目名称,默认是 MyApplication,Bundle name 填写包名,比如 com.example.myapplication 等,"Compile SDK" 和 "Compatible SDK" 选择 11,Module name 保持默认值即可,Model 选择 Stage,其他参数保持默认设置即可. 3. 点击 Finish ,工具会自动生成示例代码和相关资源,等待工程创建完成。 ## 工程项目文件简介 - entry :OpenHarmony 工程模块,编译构建生成一个Hap包 - src > main> cpp > ×××.cpp :index.d.ts 文件中声明的方法的 C++ 实现源码。 - src > main> cpp > CMakeLists.txt:是cmake用来生成Makefile文件需要的一个描述编译链接的脚本文件。 - src > main> cpp > types > libentry > index.d.ts : 对 ts 提供的方法声明。 - src > main> cpp > types > libentry > package.json:打包的配置文件。 - src > main > ets :用于存放ets源码。 - src > main > ets > entryability :应用/服务的入口。 - src > main > ets > pages :EntryAbility 包含的页面。 - src > main > resources :用于存放应用/服务所用到的资源文件,如图片、字符串、布局文件等。 - src > main > module.json5 :模块配置文件。主要包含HAP包的配置信息、应用/服务在具体设备上的配置信息以及应用/服务的全局配置信息。 - build-profile.json5 :模块的模块信息 、编译信息配置项,包括 buildOption target 配置等。 - hvigorfile.js :模块级编译构建任务脚本,开发者可以自定义相关任务和代码实现。 - build-profile.json5 :应用级配置信息,包括签名、产品配置等。 - hvigorfile.js :应用级编译构建任务脚本。 > CMakeLists.txt 文件还会在 build-profile.json5 里做配置,代码如下所示: > > ```json > { > "apiType": 'stageMode', > "buildOption": { > "externalNativeOptions": { // CPP相关配置 > "path": "./src/main/cpp/CMakeLists.txt", // CMake 文件的路径 > "arguments": "", // 传递给 CMake 的可选参数 > "cppFlags": "", // 传递给 C++编译器的可选参数 > } > } > } > ``` > > externalNativeOptions 是对 CPP 的相关配置,path 表示 CMake 文件的配置路径,arguments 表示传递给 CMake 的可选编译参数,cppFlags 表示传递给 C++ 编译器的可选参数。 ## 构建第一个界面 1. 使用文本组件 工程同步完成后,在 Project 窗口,点击 entry > src > main > ets > pages ,打开 Index.ets 文件,可以看到页面由 Row 、 Column 、 Text 组件组成。 index.ets 文件的示例如下: ![截图](4b1343cada12941f2cb9f06e303ec97b.png) 2. 添加按钮 在默认页面基础上,我们添加一个 Button 组件,作为按钮接受用户点击的动作,从而实现计数器自增操作。 "index.ets" 文件的示例如下: ```typescript @Entry @Component struct Index { @State count: number = 0; // 状态数据 build() { Stack({alignContent: Alignment.BottomEnd}) { // 堆叠式布局 Text(this.count.toString()) // 显示文本 .fontSize(50) // 文字大小 .margin(50) // 外边距 .size({width: '100%', height: '100%'}) // 控件大小 Button('+') // 显示一个+按钮 .size({width: 80, height: 80}) // 按钮大小 .fontSize(50) // 按钮文字大小 .onClick(() => { // 按钮点击事件 this.count++; // count累加,触发build()方法回调 }) .margin(50) } .width('100%') .height('100%') } } ``` 3. 打开预览器 在编辑窗口右上角的侧边工具栏,点击 Previewer ,然后点击页面加号按钮,页面运行效果如下图所示: 图片在公司了再传。。。 根据运行截图,我们点击了加号按钮,触发按钮的 onClick 事件回调,由于在回调里执行了 count++ 操作导致了 count 的值发生了改变,又因为 count 被 @State 修饰符修饰,所以ArkUI开发框架就会重新调用 build() 方法更新各组件的属性值, Text 组件会更新 count 的值,然后页面刷新,计数器的功能就实现了。 ## 页面的构建流程原理(不看) 上述示例的页面刷新过程可以分为两个过程,一个是页面渲染完毕没有点击按钮过程,另一个是点击点击按钮后页面数据变化过程: 1. 页面初次显示过程 ①、index.ets 源代码通过编译工具链编译为带有类型标志的目标文件,同时也包含了如何创建UI结构信息的指令流。 ②、通过跨语言调用并生成了 C++ 层面的 Component 树(UI描述层)。 ③、通过 Component 树进一步生成 Element 树。 Element 是 Component 的实例,表示一个具体的组件节点,它形成的 Element 树负责维持界面在整个运行时的树形结构,方便计算更新时的局部更新算法等。 ④、对于每个可显示的 Element 都会为其创建对应的 RenderNode 。 RenderNode 负责一个节点的显示信息,它形成的 Render 树维护着整个界面渲染需要用到的信息,包括位置、大小、绘制命令等。后续的布局、绘制都是在 Render 树上进行的。 ⑤、实现真正的渲染并显示绘制结果。 2. 点击按钮显示过程 ⑥、点击屏幕,事件传递到组件上,组件的 onClick 事件方法被触发执行。 ⑦、由于 onClick 事件方法中 @State 修饰的变量值改变了,相应的 getter / setting 函数会被触发。 ⑧、状态管理模块定位出与之关联的UI组件。 ⑨、状态管理模块更新相应的 Element 树的信息。 ⑩、状态管理模块更新相应的 RenderNode 树的渲染信息。 ⑪、刷新界面并显示绘制结果。 # 基础类组件 ## ArkUI组件-Image组件 Image 用来加载并显示图片的基础组件,它支持从内存、本地和网络加载图片,当从网络加载图片的时候,需要申请网络访问权限: [ohos.permission.INTERNET](https://https://docs.openharmony.cn/pages/v4.0/zh-cn/application-dev/security/permission-list.md/#ohospermissioninternet)。 [访问控制权限申请指导](https://docs.openharmony.cn/pages/v4.0/zh-cn/application-dev/security/accesstoken-guidelines.md/) 1. 声明Image组件并设置图片源: ``` Image(src: string|PixelMap|Resource) ``` ① string格式,通常用来加载网络图片,需要申请网络访问权限: [ohos.permission.INTERNET](https://https://docs.openharmony.cn/pages/v4.0/zh-cn/application-dev/security/permission-list.md/#ohospermissioninternet)。 ```typescript Image('https://xxx.png') ``` ② PixelMap格式,可以加载像素图,常用在图片编辑中 ```typescript Image(pixelMapobject) ``` ③ Resource格式,加载本地图片,推荐使用 ```typescript Image($r('app.media.mate60')) Image($rawfile('mate60.png')) ``` ![截图](2bef035e47c20ab4ce84784b698d6830.png) 2. 添加图片属性 ```typescript Image(sr('app.media.icon') .width(100) // 宽度 ---- .height(120) // 高度 |--组件通用属性 .borderRadius(10) // 边框圆角 ---- .interpolation(ImageInterpolation.High) // 图片插值 ``` ## ArkUI组件-Text组件 Text 是显示文本的基础组件之一,它可以包含子组件 Span ,当包含 Span 时不生效,只显示 Span 的内容。 1. 声明Text组件并设置文本内容 ``` Text(content?: string|Resource) ``` ① string格式,直接填写文本内容 ```typescript Text('图片宽度') ``` ② Resource格式,读取本地资源文件 ```typescript Text($r('app.string.width_label')) ``` ![1704547635040.png](ecc147411962d3114899da96256cb3c5.png) > 优先找限定词目录下的string.json, 找不到才在base目录下找 2. 添加文本属性 ```typescript Text('注册账号') .lineHeight (32) // 行高 .fontsize(20) // 字体大小 .fontCo1or('#ff1876f8') // 字体颜色 .fontWeight(FontWeight.Medium) // 字体粗细 ``` ## ArkUI组件-TextInput ArkUI开发框架提供了 2 种类型的输入框: TextInput 和 TextArea ,前者只支持单行输入,后者支持多行输入。 ![截图](977207294cb792912a95368ec8a02f50.png) TextArea 和 TextInput 都属于输入框,只是 TextArea 允许多行输入,它们的属性也都大致是一样的,只是目前 TextArea 还不支持 maxLength 属性,这里就不再介绍 TextArea 的属性了。 ## ArkUI组件-Button Button 组件也是基础组件之一,和其它基础组件不同的是 Button 组件允许添加一个子组件来实现不同的展示样式 ![截图](f18342a2a816564ff184edf73d4acd7e.png) ## ArkUI组件-Slider 项目开发中可能会有设置设备音量大小,调节屏幕亮度等需求,实现类似需求一般都会使用到滑动条,ArkUI开发框架提供了滑动组件 Slider ![截图](4187c6e98bfd97da7bdd3bcb63687d60.png) # 容器类组件 ## 线性布局容器(Row、Column) 线性容器类表示按照水平方向或者竖直方向排列子组件的容器,ArkUI开发框架通过 Row 和 Colum 来实现线性布局。 ### 主轴和交叉轴概念 什么是主轴和纵轴?对于线性容器来说,有主轴和交叉轴之分,如果布局是沿水平方向,那么主轴就指水平方向,而交叉轴就是垂直方向;如果布局是沿垂直方向,那么主轴就是指垂直方向,而交叉轴就是水平方向。 ### Row Row 按照水平方向布局子组件,主轴为水平方向,交叉轴为竖直方向。 ![截图](1dc6ec9af1f68691027b6f469acabf0a.png) #### Row定义 ```typescript interface RowInterface { (value?: { space?: string | number }): RowAttribute; } ``` - value:可选参数, space 表示设置 Row 的子组件在水平方向上的间距。 #### Row属性介绍 ```typescript declare class RowAttribute extends CommonMethod { alignItems(value: VerticalAlign): RowAttribute; justifyContent(value: FlexAlign): RowAttribute; } ``` - alignItems:参数类型为 VerticalAlign ,表示子组件在竖直方向上的布局方式, VerticalAlign 定义了以下三种对其方式: - Top:设置子组件在竖直方向上居顶部对齐。 - Center(默认值):设置子组件在竖直方向上居中对其。 - Bottom:设置子组件在竖直方向上居底部对齐。 ![截图](2749e602f5d8ac7d251d5ad804d9cd21.png) - justifyContent:设置子组件在水平方向上的对齐方式, FlexAlign 定义了一下几种类型: - Start:元素在主轴方向首端对齐, 第一个元素与行首对齐,同时后续的元素与前一个对齐。 - Center:元素在主轴方向中心对齐,第一个元素与行首的距离与最后一个元素与行尾距离相同。 - End:元素在主轴方向尾部对齐, 最后一个元素与行尾对齐,其他元素与后一个对齐。 - SpaceBetween:主轴方向均匀分配弹性元素,相邻元素之间距离相同。 第一个元素与行首对齐,最后一个元素与行尾对齐。 - SpaceAround:主轴方向均匀分配弹性元素,相邻元素之间距离相同。 第一个元素到行首的距离和最后一个元素到行尾的距离时相邻元素之间距离的一半。 - SpaceEvenly:主轴方向元素等间距布局, 相邻元素之间的间距、第一个元素与行首的间距、最后一个元素到行尾的间距都完全一样。 ![截图](cf579a46bf450f808c7d2fb9d2812857.png) ### Column Column 按照竖直方向布局子组件,主轴为竖直方向,交叉轴为水平方向。 ![截图](a6e67e0b22ead8040137b0c41440627f.png) #### Column定义 ```typescript interface ColumnInterface { (value?: { space?: string | number }): ColumnAttribute; } ``` value:可选参数, space 表示设置 Column 的子组件在竖直方向上的间距,参数和 Row 一样。 #### Column属性介绍 ```typescript declare class ColumnAttribute extends CommonMethod { alignItems(value: HorizontalAlign): ColumnAttribute; justifyContent(value: FlexAlign): ColumnAttribute; } ``` - alignItems:设置子组件在水平方向上的布局方式, HorizontalAlign 定义了以下三种对其方式: - Start:设置子组件在水平方向上按照语言方向起始端对齐。 - Center(默认值):设置子组件在水平方向上居左对齐。 - End:设置子组件在水平方向上按照语言方向末端对齐。 ![截图](40bf84168c86a147a0b3bfdf2accc5d3.png) - justifyContent:设置子组件在竖直方向上的对齐方式, FlexAlign 定义了一下几种类型: - Start:元素在主轴方向首端对齐, 第一个元素与行首对齐,同时后续的元素与前一个对齐。 - Center:元素在主轴方向中心对齐,第一个元素与行首的距离与最后一个元素与行尾距离相同。 - End:元素在主轴方向尾部对齐, 最后一个元素与行尾对齐,其他元素与后一个对齐 - SpaceBetween:元素在主轴方向均匀分配弹性元素,相邻元素之间距离相同。 第一个元素与行首对齐,最后一个元素与行尾对齐。 - SpaceAround:元素在主轴方向均匀分配弹性元素,相邻元素之间距离相同。 第一个元素到行首的距离和最后一个元素到行尾的距离时相邻元素之间距离的一半。 - SpaceEvenly:元素在主轴方向元素等间距布局, 相邻元素之间的间距、第一个元素与行首的间距、最后一个元素到行尾的间距都完全一样。 ![截图](5eebc72775f1a330ca1ccf3512c9b5f2.png) ### Blank Blank 表示空白填充组件,它用在 Row 和 Column 组件内来填充组件在主轴方向上的剩余尺寸的能力。 #### Blank定义 ```typescript interface BlankInterface { (min?: number | string): BlankAttribute; } ``` - min: Blank 组件在容器主轴上的最小尺寸。 #### Blank属性介绍 ```typescript declare class BlankAttribute extends CommonMethod { color(value: ResourceColor): BlankAttribute; } ``` - color:设置空白填充的填充颜色。 > Blank 具有以下特性: > > 只在 Row 和 Column 中生效。 除了 color 外不支持通用属性。 只在 Row 和 Column 有剩余空间才生效。 适合用在多设备适配场景中。 # 渲染控制语法 ArkUI开发框架是一套构建 HarmonyOS / OpenHarmony 应用界面的声明式UI开发框架,它支持程序使用 if/else 条件渲染, ForEach 循环渲染以及 LazyForEach 懒加载渲染。 ## ForEach循环渲染 ArkUI开发框架提供循环渲染(ForEach组件)来迭代数组,并为每个数组项创建相应的组件。 ForEach 定义如下: ```typescript interface ForEach {( arr: Array, itemGenerator: (item: any, index?: number) => void, keyGenerator?: (item: any, index?: number) => string ): ForEach; } ``` - arr:必须是数组,允许空数组,空数组场景下不会创建子组件。 - itemGenerator:子组件生成函数,为给定数组项生成一个或多个子组件。 - keyGenerator:匿名参数,用于给定数组项生成唯一且稳定的键值。 下面是一个案例: ![无标题.png](75719faf2194c141ff65ec4df463c8ff.png) ![截图](891a4b1f3cb4cbc9d85843f3ae6cceb6.png) ```typescript class Item { name: string image: ResourceStr price: number constructor(name: string, image: ResourceStr, price: number, discount: number = 0) { this.name = name this.image = image this.price = price } } @Entry @Component struct ItemPage { // 商品数据 private items: Array = [ new Item('华为Mate60', $r('app.media.mate60'),6999), new Item('MateBookProX', $r('app.media.mateBookProX'),13999), new Item('WatchGT4', $r('app.media.watchGT4'),1438), new Item('FreeBuds Pro3', $r('app.media.freeBudsPro3'),1499), new Item('Mate X5', $r('app.media.mateX5'),12999) ] build() { Column({space: 8}){ // 标题部分 Header({title: '商品列表'}) .margin({bottom: 20}) ForEach( this.items, (item: Item) => { Row({space: 10}){ Image(item.image) .width(100) Column({space: 4}){ Text(item.name) .fontSize(20) .fontWeight(FontWeight.Bold) Text('¥ ' + item.price) .fontColor('#F36') .fontSize(18) } .height('100%') .alignItems(HorizontalAlign.Start) } }) } } ``` ## if/else条件渲染 使用 if/else 进行条件渲染需要注意以下情况: - if 条件语句可以使用状态变量。 - 使用 if 可以使子组件的渲染依赖条件语句。 - 必须在容器组件内使用。 - 某些容器组件限制子组件的类型或数量。将if放置在这些组件内时,这些限制将应用于 if 和 else 语句内创建的组件。例如,Grid 组件的子组件仅支持 GridItem 组件,在 Grid 组件内使用条件渲染时,则 if 条件语句内仅允许使用 GridItem 组件。 ![无标题.png](96745f3f5554b89859aa86631bcbc210.png) ```typescript class Item { name: string image: ResourceStr price: number discount: number constructor(name: string, image: ResourceStr, price: number, discount: number = 0) { this.name = name this.image = image this.price = price this.discount = discount } } @Entry @Component struct ItemPage { // 商品数据 private items: Array = [ new Item('华为Mate60', $r('app.media.mate60'),6999, 500), new Item('MateBookProX', $r('app.media.mateBookProX'),13999), new Item('WatchGT4', $r('app.media.watchGT4'),1438), new Item('FreeBuds Pro3', $r('app.media.freeBudsPro3'),1499), new Item('Mate X5', $r('app.media.mateX5'),12999) ] build() { Column({space: 8}){ // 标题部分 Header({title: '商品列表'}) .margin({bottom: 20}) Column({space: 4}){ if(item.discount){ Text(item.name) .fontSize(20) .fontWeight(FontWeight.Bold) Text('原价:¥' + item.price) .fontColor('#CCC') .fontSize(14) .decoration({type: TextDecorationType.LineThrough}) Text('折扣价:¥' + (item.price - item.discount)) .fontColor('#F36') .fontSize(18) Text('补贴:¥' + item.discount) .fontColor('#F36') .fontSize(18) }else{ Text(item.name) .fontSize(20) .fontWeight(FontWeight.Bold) Text('¥' + item.price) .fontColor('#F36') .fontSize(18) } } .height('100%') .alignItems(HorizontalAlign.Start) } .width('100%') .backgroundColor('#FFF') .borderRadius(20) .height(120) .padding(10) } } } ``` ## LazyForEach循环渲染 ArkUI开发框架提供数据懒加载( LazyForEach 组件)从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。 **LazyForEach 定义如下:** ```typescript // LazyForEach定义 interface LazyForEach {( dataSource: IDataSource, itemGenerator: (item: any, index?: number) => void, keyGenerator?: (item: any, index?: number) => string ): LazyForEach; } // IDataSource定义 export declare interface IDataSource { totalCount(): number; getData(index: number): any; registerDataChangeListener(listener: DataChangeListener): void; unregisterDataChangeListener(listener: DataChangeListener): void; } // DataChangeListener定义 export declare interface DataChangeListener { onDataReloaded(): void; onDataAdded(index: number): void; onDataMoved(from: number, to: number): void; onDataDeleted(index:number): void; onDataChanged(index:number): void; } ``` - itemGenerator:子组件生成函数,为给定数组项生成一个或多个子组件。 - keyGenerator:匿名参数,用于给定数组项生成唯一且稳定的键值。 - dataSource:实现 IDataSource 接口的对象,需要开发者实现相关接口。 **IDataSource 定义如下:** ```typescript export declare interface IDataSource { totalCount(): number; getData(index: number): any; registerDataChangeListener(listener: DataChangeListener): void; unregisterDataChangeListener(listener: DataChangeListener): void; } ``` - totalCount:获取数据总数。 - getData:获取索引对应的数据。 - registerDataChangeListener:注册改变数据的监听器。 - unregisterDataChangeListener:注销改变数据的监听器。 **DataChangeListener 定义如下:** ```typescript export declare interface DataChangeListener { onDataReloaded(): void; onDataAdded(index: number): void; onDataMoved(from: number, to: number): void; onDataDeleted(index:number): void; onDataChanged(index:number): void; } ``` - onDataReloaded:item重新加载数据时的回调。 - onDataAdded:item新添加数据时的回调。 - onDataMoved:item数据移动时的回调。 - onDataDeleted:item数据删除时的回调。 - onDataChanged:item数据变化时的回调。 **简单样例如下:** ```typescript // 定义Student class Student { public sid: number; public name: string; public age: number public address: string public avatar: string constructor(sid: number = -1, name: string, age: number = 16, address: string = '北京', avatar: string = "") { this.sid = sid; this.name = name; this.age = age; this.address = address; this.avatar = avatar; } } // 定义DataSource abstract class BaseDataSource implements IDataSource { private mDataSource: T[] = new Array(); constructor(dataList: T[]) { this.mDataSource = dataList; } totalCount(): number { return this.mDataSource == null ? 0 : this.mDataSource.length } getData(index: number): T|null { return index >= 0 && index < this.totalCount() ? this.mDataSource[index] : null; } registerDataChangeListener(listener: DataChangeListener) { } unregisterDataChangeListener(listener: DataChangeListener) { } } // class StudentDataSource extends BaseDataSource { constructor(students: Student[]) { super(students) } } function mock(): Student[] { var students = []; for(var i = 0; i < 20; i++) { students[i] = new Student(i, "student:" + i, i + 10, "address:" + i, "app.media.test") } return students; } @Entry @Component struct ComponentTest { // mock数据 private student: Student[] = mock(); // 创建dataSource private dataSource: StudentDataSource = new StudentDataSource(this.student); build() { Column({space: 10}) { List() { LazyForEach(this.dataSource, (item: Student) => {// LazyForEach使用自定义dataSource ListItem() { Row() { Image($r("app.media.test")) .height('100%') .width(80) Column() { Text(this.getName(item)) // 调用getName验证懒加载 .fontSize(20) Text('address: ' + item.address) .fontSize(17) } .margin({left: 5}) .alignItems(HorizontalAlign.Start) .layoutWeight(1) } .width('100%') .height('100%') } .width('100%') .height(60) }) } .divider({ strokeWidth: 3, color: Color.Gray }) .width('90%') .height(160) .backgroundColor(Color.Pink) } .width('100%') .height('100%') .padding(10) } getName(item: Student): string { console.log("index: " + item.sid); // 打印item下标日志 return 'index:' + item.sid + ", " + item.name; } } ``` # 滚动类组件 ## List List 是很常用的滚动类容器组件之一,它按照水平或者竖直方向线性排列子组件, List 的子组件必须是 ListItem ,它的宽度默认充满 List 的宽度。 列表(List)是一种复杂容器,具备下列特点: ① 列表项(ListItem)数量过多超出屏幕后,会自动提供滚动功能 ② 列表项(ListItem)既可以纵向排列,也可以横向排列 ![截图](8163215096775084d71a0944db9d55fe.png) ### List定义介绍 ```typescript interface ListInterface { (value?: { initialIndex?: number; space?: number | string; scroller?: Scroller }): ListAttribute; } ``` - initialIndex:默认值为 0 ,设置 List 第一次加载数据时所要显示的第一个子组件的下标,如果超过最后一个子组件的下标,则设置不生效。 - space:设置列表间的间隔距离。 - scroller:设置滚动控制器。 简单样例如下所示: ![截图](920cb65190b8c006a990a5a46abdbf48.png) # 自定义组件 组件是 OpenHarmony 页面最小显示单元,一个页面可由多个组件组合而成,也可只由一个组件组合而成,这些组件可以是ArkUI开发框架自带系统组件,比如 Text 、 Button 等,也可以是自定义组件。 ![截图](658c5d303a6e5dc41c875869b0b64fc6.png) ## 语法和生命周期 ### 定义组件 自定义一个组件,首先要定义好名称,尽量做到见名知意,比如定义一个标题栏组件,笔者把它命名为 TitleBar ,为了让系统知道这是一个组件,需要使用 @Component 修饰符和 struct 关键字修饰,格式:【@Component struct + 组件名称】,如下所示: ```typescript @Component struct TitleBar { build() { // 省略 } } @Entry @Component struct Index { build() { // 省略 } } ``` - struct:表示 TitleBar 是一个结构体,使用 struct 关键字必须实现 build() 方法,否则编译器报错:Require build function for struct 。 - @Component:表示 TitleBar 这个结构体具有组件化的能力,也就是说它可以成为一个独立的组件。 - @Entry:表示当前组件是页面的总入口,简单理解就是页面的根节点,一个页面有且仅有一个 @Entry 修饰符,只有被 @Entry 修饰的组件或者子组件才会在页面上显示。 > 自定义组件禁止添加构造函数,否则编译器报错。 ### 刷新组件 使用 struct 关键字修饰完 TitleBar 后必须实现 build() 方法,该方法满足 Builder 构造器接口定义,用于定义组件的声明式 UI 描述,在组件创建或者组件内 @State 修饰的变量更新时系统都会自动调用 build() 方法。 ```typescript @Component struct TitleBar { @State count: number = 0; build() { Flex() { Text("index:" + this.number) // …… } .width('100%') .height('100%') .backgroundColor("#aabbcc") } } ``` 上述样例中当 count 的值发生了变化,系统会自动调用 build() 方法更新相关属性值,实现 UI 刷新的目的。 ### 导出组件 自定义完组件后,提供给外界使用时还要允许该组件可以导出,导出组件使用关键字 export ,如下所示: ```typescript @Component export struct TitleBar { // 使用export关键字导出TitleBar组件 build() { Flex() { } .width('100%') .height('100%') .backgroundColor("#aabbcc") } } ``` ### 使用组件 使用自定义组件用关键字 import 导入即可,例如使用自定义组件 TibleBar ,导入如下所示: ```typescript import {TitleBar} from "../../common/widgets/titlebar" // 导入TitleBar @Entry @Component struct Index { build() { Column() { TitleBar({titleBarAttribute: { // 使用TitleBar // 添加相关属性 }}) } .padding({bottom: 5}) .backgroundColor('#010101') .width('100%') .height('100%') } } ``` 自定义组件的使用和系统组件使用无差别,直接引用即可,如果自定义组件需要传值,方式是在组件的构造方法中传递一个匿名对象 {} 进去,且该匿名对象中的属性名称和类型要和自定义组件中的属性保持一致 ### 组件生命周期 ArkUI开发框架赋予了组件独有的生命周期方法,对于系统组件来讲,生命周期方法是 ***onAppear*** 和 ***onDisAppear*** ```typescript export declare class CommonMethod { onAppear(event: () => void): T; onDisAppear(event: () => void): T; } ``` 给组件设置挂载和卸载事件的回调,设置该回调后,当组件从组件树上挂载或者是卸载时会触发该回调。各 API 方法说明如下: - onAppear:组件从组件树上挂载的回调。 - onDisAppear:组件从组件树上卸载的回到。 简单样例如下所示: ```typescript @Entry @Component struct Index { @State textShow: boolean = false; // 默认状态 build() { Column() { Column() { if (this.textShow) { Text('挂载/卸载') .fontSize(22) .onAppear(() => { console.log("哈哈,我被挂载了") }) .onDisAppear(() => { console.log("呜呜,我被卸载了") }) } } .width('100%') .height(60) Button(this.textShow ? "卸载" : "挂载") .stateStyles({ pressed: { .backgroundColor(Color.Pink) // 设置点击时的样式 } }) .onClick(() => { // 依次挂载卸载Text组件 this.textShow = !this.textShow; }) } .width('100%') .height('100%') } } ``` #### 组件的生命周期 使用 @Component 修饰的组件,ArkUI开发框架会自动为其赋予私有的生命周期方法 aboutToAppear() 和 aboutToDisappear() ,它们用于通知开发者该自定义组件的生命周的变更。 - aboutToAppear:函数在创建自定义组件的新实例后,在执行其 build() 函数之前执行。允许在该函数中改变状态变量,更改将在后续执行 build() 函数中生效。 - aboutToDisappear:函数在自定义组件析构消耗之前执行。不允许在该函数中改变状态变量,特别是 @Link 变量的修改可能会导致应用程序行为不稳定。 #### 页面的生命周期 页面本质上也是一个组件,只是页面对于组件来讲多了一个修饰符 @Entry,该修饰符表示当前组件是一个页面,它需要在 config.json 中做配置,页面除了具有组件的生命周期外,它还有自己独有的生命周期方法: - onPageShow:页面显示时触发一次,包括路由过程、应用进入前后台等场景,仅 @Entry 修饰的自定义组件生效。 - onPageHide:页面消失时触发一次,包括路由过程、应用进入前后台等场景,仅 @Entry 修饰的自定义组件生效。 - onBackPress:当用户点击返回按钮时触发,仅 @Entry 修饰的自定义组件生效。该方法返回 boolean 类型的值,说明如下: - 返回 true 表示页面自己处理返回逻辑, 不进行页面路由。 - 返回 false 表示使用默认的返回逻辑。 - 不返回值会作为 false 处理。 组件生命周期制作表格对比说明如下: |函数名|描述| |--|--| |onAppear|系统组件独有的方法,组件从组件树上挂载的回调。| |onDisAppear|系统组件独有的方法,组件从组件树上卸载的回到。| |aboutToAppear|函数在创建自定义组件的新实例后,在执行其 build() 函数之前执行。允许在该函数中改变状态变量,更改将在后续执行 build() 函数中生效。| |aboutToDisappear|函数在自定义组件析构消耗之前执行。不允许在该函数中改变状态变量,特别是 @Link 变量的修改可能会导致应用程序行为不稳定。| |onPageShow|页面显示时触发该回调,包括路由过程、应用进入前后台等场景。仅 @Entry 修饰的自定义组件生效。| |onPageHide|页面消失时触发该回调,包括路由过程、应用进入前后台等场景。仅 @Entry 修饰的自定义组件生效。| |onBackPress|当用户点击返回按钮时触发,该方法返回 boolean 类型,true:表示页面自己处理返回逻辑, 不进行页面路由。false:表示使用默认的返回逻辑。不返回值会作为 false 处理。仅 @Entry 修饰的自定义组件生效。| > 这些回调函数是私有的,在运行时由开发框架在特定的时间进行调用,不能从应用程序中手动调用这些回调函数。 📢:允许在生命周期函数中使用 Promise 和异步回调函数,比如网络资源获取,定时器设置等;不允许在生命周期函数中使用 async await 。 ### 再按一次,退出应用 我们在使用第三方 APP 的时候会遇见点击返回键提示再按一次退出应用的场景,比如在短时间内不按,就不会退出 APP 达到留住用户的目的,接下来我们实现这个再按一次退出应用的例子。 根据页面生命周期的方法可知,点击返回键的时候会调用 onBackPress() 方法,因此判断是否是第一次点击,如果是则返回 true 并给用户提示,如果不是则判断两次点击的时间间隔,若间间隔小于 2 秒,那么就直接退出 APP ,否则给用户提示. ```typescript import app from '@system.app'; @Entry @Component struct Index { private lastExitTime: number = -1; // 记录点击时间 @State count: number = 0; // 状态数据 build() { Stack({alignContent: Alignment.BottomEnd}) { // 堆叠式布局 Text(this.count.toString()) // 显示文本 .fontSize(50) // 文字大小 .textAlign(TextAlign.Center) // 居中对齐 .size({width: '100%', height: '100%'}) // 控件大小 Button('+') // 显示一个+按钮 .size({width: 80, height: 80}) // 按钮大小 .fontSize(50) // 按钮文字大小 .onClick(() => { // 按钮点击事件 this.count++; // count累加,触发build()方法回调 }) .margin(50) } .width('100%') .height('100%') } onBackPress() { if (-1 == this.lastExitTime) { // 第一次点击返回键,提示toast this.lastExitTime = new Date().getTime(); prompt.showToast({ message: "再按一次退出应用" }) return true; } else { let currentTime = new Date().getTime(); if(currentTime - this.lastExitTime > 2000) { // 时间大于2000提示 prompt.showToast({ message: "再按一次退出应用" }) this.lastExitTime = currentTime; return true; } else { // 2秒内点击,退出APP app.terminate(); } } return false; } } ``` 以上就是自定义一个组件需要遵循的语法规范,自定义组件具有以下特点: - **可组合**:允许开发人员组合使用内置组件和其他组件,以及公共属性和方法。 - **可重用**: 可以被其他组件重用,并作为不同的实例在不同的父组件或容器中使用; - **有生命周期**: 生命周期的回调方法可以在组件中配置,用于业务逻辑处理; - **数据驱动更新**: 可以由状态数据驱动,实现UI自动更新。 ## 自定义构建函数 可定义在全局或组件内 ### 全局自定义构建函数 ```typescript // 全局自定义构建函数 @Builder function XxxBuilder(){ // UI描述 } ``` 1. 定义在组件外,可以无入参也可传参 2. 需在函数名前加 ”function“ 3. 用的时候,直接调用。 ![截图](b9c7617b7c7c535faea19faeecb6faf7.png) ![截图](817c44690dd41aa787d284c0622445c3.png) ### 组件内自定义构造函数 ```typescript @Component struct XxxComponent { // 组件内自定义构建函数 @Builder YyyBuilder(){ // UI描述 } builder() { XxxBuilder() this.YyyBuilder() } } ``` 1. 定义在组件内,可传可不传参。 2. 函数名和Builder之间不加“function” 3. 调用时需用this.调用 ## @Styles装饰器 仅可封装组件**通用属性** ### 全局公共样式 ```typescript @Styles function fillScreen(){ .width('100%') .height('100%') } ``` - 需在函数名前加“function” - "." + 函数名直接调用 ### 组件内公共样式 ```typescript @Entry @Component struct XxxPage { // 组件内公共样式 @Styles normalBackground(){ .backgroundColor('#36D') .padding(14) } build() { Row() {// ...} .fillScreen() .normalBackground() } } ``` - 函数名前不加"function" - "." + 函数名直接调用 ## @Extend装饰器 仅可定义在全局,用来设置组件的**特有属性** ```typescript @Extend(Text) function priceText(){ .fontColor('#36D') .fontSize(18) } ``` - "()"内写你要设置的组件的名字 # 状态管理 在声明式UI中,是以状态驱动视图更新: ![截图](9ed5a76db27402b308c768209955cd07.png) - 状态(State):指驱动视图更新的数据(被装饰器标记的变量) - 视图(View):基于UI描述渲染得到用户界面 当状态发生了变更,对应的视图就会重新渲染。 ![截图](87dca322bd7850e4b0aba60cd7a2fe81.png) ## @State装饰器 ==@State== 装饰的变量是组件内部的状态数据,当这些状态数据被修改时,将会调用所在组件的 build() 方法刷新UI。==@State==状态数据具有以下特征: - 支持多种数据类型:允许class 、 number 、 boolean 、 string 强类型的按值和按引用类型。允许这些强类型构成的数组,即Array、Array、Array、Array。==不允许 object 和 any==。 - 内部私有:标记为 @State 的属性是私有变量,只能在组件内访问。 - 支持多个实例:组件不同实例的内部状态数据独立。 - 需要本地初始化:必须为所有 @State 变量分配初始值,将变量保持未初始化可能导致框架行为未定义,初始值需要是有意义的值,比如设置 class 类型的值为 null 就是无意义的,会导致编译报错。 - 创建自定义组件时支持通过状态变量名设置初始值:在创建组件实例时,可以通过变量名显式指定 @State 状态属性的初始值。 - ==嵌套类型以及数组中的对象属性无法触发视图更新。== ```typescript import { Header } from '../common/components/CommonComponents'; class Person{ name: string age: number gf: Person constructor(name: string, age: number, gf?: Person) { this.name = name this.age = age this.gf = gf } } @Entry @Component struct StatePage2 { idx: number = 1 @State p: Person = new Person('Jack', 21, new Person('柔丝', 18)) @State gfs: Person[] = [ new Person('柔丝', 18), new Person('露西', 19), ] build() { Column({space: 10}) { Header() Text(`${this.p.name} : ${this.p.age}`) .fontSize(50) .fontWeight(FontWeight.Bold) .onClick(() => { this.p.age++ }) Text(`${this.p.gf.name} : ${this.p.gf.age}`) .fontSize(50) .fontWeight(FontWeight.Bold) .onClick(() => { // 嵌套类型以及数组中的对象属性无法触发视图更新 this.p.gf.age++ }) Button('添加') .onClick(() => { // 添加元素 this.gfs.push(new Person('女友' + this.idx++, 20)) }) Text('=女友列表=') .fontSize(50) .fontWeight(FontWeight.Bold) ForEach( this.gfs, (p, index) => { Row(){ Text(`${p.name} : ${p.age}`) .fontSize(30) .onClick(() => { // 重新赋值 this.gfs[index] = new Person(p.name, p.age+1) }) Button('删除') .onClick(() => { // 删除元素 this.gfs.splice(index, 1) }) } .width('100%') .justifyContent(FlexAlign.SpaceBetween) } ) } .width('100%') .height('100%') .padding(10) } } @Component struct StatePage { @State name: string = 'Jack' @State age: number = 21 build() { Column() { Text(`${this.name} : ${this.age}`) .fontSize(50) .fontWeight(FontWeight.Bold) .onClick(() => { this.age++ }) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) } } ``` 本嵌套结构的示例中,女友列表(数组)添加或删除元素,以及赋值的时候,才会触发@State装饰器,才会刷新UI。只修改嵌套结构里的属性是不行的。 ## @Prop修饰符 **当父子组件之间需要数据同步时,可以使用@Prop和@Link装饰器** ![截图](56f4a63165b981e56f31e270e8b9a60f.png) ==@Prop== 与 ==@State== 有相同的语义,但初始化方式不同, ==@Prop== 装饰的变量可以和父组件的 ==@State== 变量建立单向的数据绑定。即 ==@Prop== 修饰的变量必须使用其父组件提供的== @State== 变量进行初始化,允许组件内部修改 ==@Prop ==变量值但更改不会通知给父组件。 > 父组件向子组件拷贝数据的时候,其实是把父组件里的数据拷贝了一份传给子组件,且之后每一次父组件数据发生变更时,都会传递一份新的拷贝过去,覆盖子组件的数据。由于子组件拿到的只是拷贝,所以不管怎么去修改,都影响不到父组件。 ![截图](184e6d0e8ffdd583d915733e96e5f400.png) == @Prop ==状态数据具有以下特征: - 支持简单数据类型:仅支持 ==number ==、== string== 、 ==boolean== 简单类型; - 内部私有:标记为 ==@Prop== 的属性是私有变量,只能在组件内访问。 - 支持多个实例:组件不同实例的内部状态数据独立。 - 不支持内部初始化:在创建组件的新实例时,必须将值传递给 ==@Prop== 修饰的变量进行初始化,不支持在组件内部进行初始化。 简单样例如下所示: ```typescript @Entry @Component struct ComponentTest { @State date: string = "时间:" + new Date().getTime(); build() { Column({space: 10}) { Text(`父组件【${this.date}】`) .fontSize(20) .backgroundColor(Color.Pink) Item({time: this.date}) // 必须初始化子组件的time字段 Item({time: this.date}) // 必须初始化子组件的time字段 Button('更新时间') .onClick(() => { this.date = "时间:" + new Date().getTime();// 父组件的更改影响子组件 }) } .width('100%') .height('100%') .padding(10) } } @Component struct Item { @Prop time: string; // 不允许本地初始化 build() { Text(`子组件【${this.time}】`) .fontSize(20) .backgroundColor(Color.Grey) .onClick(() => { this.time = "时间:" + new Date().getTime(); // 子组件的更改不影响父组件 }) } } ``` 下面是一个用@State实现的任务统计案例 ```typescript import { Header } from '../common/components/CommonComponents'; // 任务类 @Observed class Task{ static id: number = 1 // 任务名称 name: string = `任务${Task.id++}` // 任务状态:是否完成 finished: boolean = false } // 统一的卡片样式 @Styles function card(){ .width('95%') .padding(20) .backgroundColor(Color.White) .borderRadius(15) .shadow({radius: 6, color: '#1F000000', offsetX: 2, offsetY: 4}) } // 任务完成样式 @Extend(Text) function finishedTask(){ .decoration({type:TextDecorationType.LineThrough}) .fontColor('#B1B2B1') } // 任务统计信息 class StatInfo { totalTask: number = 0 finishTask: number = 0 } @Entry @Component struct PropPage { // 统计信息 @Provide stat: StatInfo = new StatInfo() build() { Column({space: 10}){ Header() // 1.任务进度卡片 TaskStatistics() // 2.任务列表 TaskList() } .width('100%') .height('100%') .backgroundColor('#F1F2F3') } } @Component struct TaskStatistics { @Consume stat: StatInfo build() { Row(){ Text('任务进度:') .fontSize(30) .fontWeight(FontWeight.Bold) Stack(){ Progress({ value: this.stat.finishTask, total: this.stat.totalTask, type: ProgressType.Ring }) .width(100) Row(){ Text(this.stat.finishTask.toString()) .fontSize(24) .fontColor('#36D') Text(' / ' + this.stat.totalTask.toString()) .fontSize(24) } } } .card() .margin({top: 5, bottom: 10}) .justifyContent(FlexAlign.SpaceEvenly) } } @Component struct TaskList { // 总任务数量 @Consume stat: StatInfo // 任务数组 @State tasks: Task[] = [] handleTaskChange(){ // 1.更新任务总数量 this.stat.totalTask = this.tasks.length // 2.更新已完成任务数量 this.stat.finishTask = this.tasks.filter(item => item.finished).length } build() { Column(){ // 2.新增任务按钮 Button('新增任务') .width(200) .margin({bottom: 10}) .onClick(() => { // 1.新增任务数据 this.tasks.push(new Task()) // 2.更新任务总数量 this.handleTaskChange() }) // 3.任务列表 List({space: 10}){ ForEach( this.tasks, (item: Task, index) => { ListItem(){ TaskItem({item: item, onTaskChange: this.handleTaskChange.bind(this)}) } .swipeAction({end: this.DeleteButton(index)}) } ) } .width('100%') .layoutWeight(1) .alignListItem(ListItemAlign.Center) } } @Builder DeleteButton(index: number){ Button(){ Image($r('app.media.ic_public_delete_filled')) .fillColor(Color.White) .width(20) } .width(40) .height(40) .type(ButtonType.Circle) .backgroundColor(Color.Red) .margin(5) .onClick(() => { this.tasks.splice(index, 1) this.handleTaskChange() }) } } @Component struct TaskItem { @ObjectLink item: Task onTaskChange: () => void build() { Row(){ if(this.item.finished){ Text(this.item.name) .finishedTask() }else{ Text(this.item.name) } Checkbox() .select(this.item.finished) .onChange(val => { // 1.更新当前任务状态 this.item.finished = val // 2.更新已完成任务数量 this.onTaskChange() }) } .card() .justifyContent(FlexAlign.SpaceBetween) } } ``` 可实现以下效果 ![截图](6a52d928d72bce9f7c87a83cf0103cae.png) ## @Link修饰符 ==@Link== 与 ==@State== 有相同的语义,但初始化方式不同,== @Link== 装饰的变量可以和父组件的 ==@State== 变量建立双向的数据绑定。即 ==@Link== 修饰的变量必须使用其父组件提供的 @State 变量进行初始化,允许组件内部修改 @Link 变量值且更改会通知给父组件(类似C++里的引用)。 ![截图](934a042a88deaaceefc838b699c3d78f.png) @Link 状态数据具有以下特征: - 支持多种数据类型: ==@Link== 变量的值与 ==@State== 变量的类型相同,即== class ==、== number== 、 ==string ==、 ==boolean== 或这些类型的数组。 - 内部私有:标记为 ==@Link== 的属性是私有变量,只能在组件内访问。 - 支持多个实例:组件不同实例的内部状态数据独立。 - 不支持内部初始化:在创建组件的新实例时,必须将值传递给 ==@Link== 修饰的变量进行初始化,不支持在组件内部进行初始化。初始化使用![截图](34e1852835d1f4d54d2f673273bf992e.png)符号,例如:$propertiesName。 样例如下: ```typescript @Entry @Component struct ComponentTest { @State date: string = "时间:" + new Date().getTime(); // 定义@State变量 build() { Column({space: 10}) { Text(`父组件【${this.date}】`) .fontSize(20) .backgroundColor(Color.Pink) Item({time: $date}) // 初始化子组件time属性使用$符号 Item({time: $date}) // 初始化子组件time属性使用$符号 Button('更新时间') .onClick(() => { this.date = "时间:" + new Date().getTime(); // 变更date,子组件的对应属性也变化 }) } .width('100%') .height('100%') .padding(10) } } @Component struct Item { @Link time: string; build() { Text(`子组件【${this.time}】`) .fontSize(20) .backgroundColor(Color.Grey) .onClick(() => { this.time = "时间:" + new Date().getTime(); // 变更time,父组件的对应属性也变化 }) } ``` ## @Provide和@Consume修饰符 @Provide和@Consume可以跨组件提供类似于@State和@Link的双向同步,但不需要像它们一样显式地传参。 ![截图](5a079375d35253ac496c30d1f4b4c63a.png) ## @Observed和@ObjectLink装饰器 @ObjectLink和@Observed装饰器用于在涉及==嵌套对象==或==数组元素为对象==的场景中进行双向数据同步 ![无标题.png](a4d2cdd5b1559eae52c648108806caad.png) ①给嵌套的结构或数组加上@Observed ②给调用嵌套结构的方法加上@ObjectLink(该方法需为组件)。例如: 我们需要监控Task类中的finished对象的状态,则需在上方加上@Observed ![截图](7deb9b7e8c0caf86ed16e6ba94e6b46d.png) 在下方,我们调用了Task类中的finished,由于item是个方法的参数,则需把调用item.finished的部分单独移出去,封装成一个组件,在该组件前加上@ObjectLink,并将item作为参数传入该组件,即可实现双向数据同步 ![截图](52aa8ea436b45759fbead5a52db759ca.png) ### 总结 利用上述所学的所有装饰器,可将可将@State中的任务统计案例改写为如下: ```typescript import { Header } from '../common/components/CommonComponents'; // 任务类 @Observed class Task{ static id: number = 1 // 任务名称 name: string = `任务${Task.id++}` // 任务状态:是否完成 finished: boolean = false } // 统一的卡片样式 @Styles function card(){ .width('95%') .padding(20) .backgroundColor(Color.White) .borderRadius(15) .shadow({radius: 6, color: '#1F000000', offsetX: 2, offsetY: 4}) } // 任务完成样式 @Extend(Text) function finishedTask(){ .decoration({type:TextDecorationType.LineThrough}) .fontColor('#B1B2B1') } // 任务统计信息 class StatInfo { totalTask: number = 0 finishTask: number = 0 } @Entry @Component struct PropPage { // 统计信息 @Provide stat: StatInfo = new StatInfo() build() { Column({space: 10}){ Header() // 1.任务进度卡片 TaskStatistics() // 2.任务列表 TaskList() } .width('100%') .height('100%') .backgroundColor('#F1F2F3') } } @Component struct TaskStatistics { @Consume stat: StatInfo build() { Row(){ Text('任务进度:') .fontSize(30) .fontWeight(FontWeight.Bold) Stack(){ Progress({ value: this.stat.finishTask, total: this.stat.totalTask, type: ProgressType.Ring }) .width(100) Row(){ Text(this.stat.finishTask.toString()) .fontSize(24) .fontColor('#36D') Text(' / ' + this.stat.totalTask.toString()) .fontSize(24) } } } .card() .margin({top: 5, bottom: 10}) .justifyContent(FlexAlign.SpaceEvenly) } } @Component struct TaskList { // 总任务数量 @Consume stat: StatInfo // 任务数组 @State tasks: Task[] = [] handleTaskChange(){ // 1.更新任务总数量 this.stat.totalTask = this.tasks.length // 2.更新已完成任务数量 this.stat.finishTask = this.tasks.filter(item => item.finished).length } build() { Column(){ // 2.新增任务按钮 Button('新增任务') .width(200) .margin({bottom: 10}) .onClick(() => { // 1.新增任务数据 this.tasks.push(new Task()) // 2.更新任务总数量 this.handleTaskChange() }) // 3.任务列表 List({space: 10}){ ForEach( this.tasks, (item: Task, index) => { ListItem(){ //这里需加.bind,把父组件中的this绑定到这个函数中 TaskItem({item: item, onTaskChange: this.handleTaskChange.bind(this)}) } .swipeAction({end: this.DeleteButton(index)}) } ) } .width('100%') .layoutWeight(1) .alignListItem(ListItemAlign.Center) } } @Builder DeleteButton(index: number){ Button(){ Image($r('app.media.ic_public_delete_filled')) .fillColor(Color.White) .width(20) } .width(40) .height(40) .type(ButtonType.Circle) .backgroundColor(Color.Red) .margin(5) .onClick(() => { this.tasks.splice(index, 1) this.handleTaskChange() }) } } @Component struct TaskItem { @ObjectLink item: Task // 子组件中需定义一个方法用于接受父组件中的方法。 onTaskChange: () => void build() { Row(){ if(this.item.finished){ Text(this.item.name) .finishedTask() }else{ Text(this.item.name) } Checkbox() .select(this.item.finished) .onChange(val => { // 1.更新当前任务状态 this.item.finished = val // 2.更新已完成任务数量 this.onTaskChange() }) } .card() .justifyContent(FlexAlign.SpaceBetween) } } ``` # 功能型组件 ## 页面路由 ==页面路由==是指在应用程序中实现不同页面之间的跳转和数据传递。 ![截图](fbc8e1af91f5c13ab68144b738a94617.png) - 页面栈的最大容量上限为页面栈的最大容量上限为32个页面,使用==router.clear()==方法可以清空页面栈,释放内存 - Router有两种页面跳转模式,分别是: - ==router.pushUrl()==:目标页不会替换当前页,而是压入页面栈,因此可以用router.back()返回当前页 - ==router.replaceUrl()==:目标页替换当前页,当前页会被销毁并释放资源,无法返回当前页 - Router有两种页面实例模式,分别是: - Standard:标准实例模式,每次跳转都会新建一个目标页并压入栈顶。==默认就是这种模式== - Single: 单实例模式,如果目标页已经在栈中,则离栈顶最近的同UrL页面会被移动到栈顶并重新加载 ### router使用介绍 1. 首先要导入Router模块: ```typescript import router from '@ohos.router'; ``` 2. 然后利用router实现跳转、返回等操作: ```typescript //跳转到指定路径,并传递参数 router.pushUrl( { // RouterOptions url:'pages/ImagePage', // - url: 目标页面路径(需在main_pages.json里配置) params:{id:1} // - params: 传递的参数(可选) }, router.RouterMode.Single, // 页面模式:RouterMode枚举 err => { if(err){ // 异常响应回调函数,错误码: console.Log('路由失败.') // - 100001: 内部错误,可能是渲染失败 } // - 100002: 路由地址错误 } // - 100003:路由栈中页面超过32 } ``` //目标页 ```typescript //获取传递过来的参数 params:any = router.getParams() //返回上一页 router.back() //返回到指定页,并携带参数 router.back( { url:'pages/Index', params:{id:10} } ) ``` # 动画 ![截图](22fa10e967a2926075d6ac5bcedf38c8.png) ![截图](233cfdf5b57e1dadaf6236206bb538d7.png) 我们利用ArkUI实现动画,只需要组件开始和结束的状态,以及动画播放时的基本信息,比如动画播放时长、速度等等。 ## 属性动画 属性动画是通过设置组件的animation,属性来给组件添加动画,当组件的width、height、Opacity、 backgroundColor、scale、rotate、translate等属性==变更==时,可以实现渐变过渡效果。 ```typescript Text('^_^') .position({ X:10,//x轴坐标 y:0//y轴坐标 }) .rotate({ angle:0,//旋转角度 centerX:'50%',//旋转中心横坐标 centerY:'50%'//旋转中心纵丝标 }) .animation ({ duration:1000, curve:Curve.EaseInOut }) ``` > animation这个属性一定要放在需要有动画属性的样式之后。 ![截图](919406d4ffcedde0148e584374bce5b0.png) ## 显式动画 显式动画是通过全局animateTol函数来修改组件属性,实现属性变化时的渐变过渡效果。 ```typescript 修改组件属性关联的状态变量Text('^_^') .position({ x: 10, // x轴坐标 y: 0 // y轴坐标 }) .rotate({ angle:0, // 旋转角度 centerX: '50%', // 旋转中心横坐标 centerY: '50%' // 旋转中心纵坐标 }) // 显式调用animateTo函数触发动画 animateTo( {duration: 1000}, // 动画参数 () => { // 修改组件属性关联的状态变量 } ) ``` - 不再需要animation属性,而是调用animateTo函数,在这个函数里指定动画播放参数,同时修改组件属性关联的状态变量,相比属性动画方式更加灵活。 ## 转场动画 组件转场动画是在组件插入或移除时的过渡动画,通过组件的transition,属性来配置。 ![截图](684920781d8b487381a92451cb6630ed.png)要实现转场动画有两件事 **① 给要实现转场动画的组件加上.transition属性 ** ```typescript if(this.isShow){ Text('^_^') .transition({//转场动画参数 opcity:0, rotate:{angle:-360}, scale:{x:0,y:0} }) } ``` ②让组件去做一个显示与否的控制,一般是用布尔值来控制它显示与否,但是这个布尔值的修改,需要放到**animateTo()**里面,利用显式动画的方式,来改变bool值,进而改变组件入场和离场 ```typescript //显式调用animateTo函数触发动画 animateTo( {duration:1000},//动画参数 () => { this.isShow false } ) ``` # Stage模型 ## Stage模型概述 ![截图](d32378c2c525250588e65268c0db1843.png) **一个应用可以有很多模块,我们可以把不同的能力,放到不同的模块开发。**比如微信,它的核心功能就是社交,像聊天、朋友圈等等,这部分能力,我们就可以把它放到一个模块里。后来,随着微信的发展,它又增加了一些其他的能力,比如:小程序、视频号等,这些能力之间也是相互独立的,所以它们也能放到独立的Ability Module里面去开发,这样一来,整个应用的能力,就被清晰地划分出来了,我们管理起来也更加的方便。 在开发Ability Module的过程中,它们势必会有一些通用的工具或者资源、配置、组件等等。如果每一个模块都各自去开发,显然是一种重复和浪费,所以**我们就可以把这些通用的东西给它抽取起来,放到一个单独的模块里去。这样的模块,我们把它称之为Library Module**,顾名思义,就是一种共享的依赖类型的模块。我们的Ability Module就可以去引用Library类型的module。 ![截图](6cb396bdb66f1326caf7cd87cdc71127.png) 在Stage模型中,为了降低不同功能模块之间的耦合,每一个模块都是可以独立去编译和运行的。所有的Ability类型的module,将来就会被编译成.hap(harmony ability package)文件。所有的Library类型的模块,则会被编译成HSP(harmony shared package)文件。hap包,在运行过程中,就可以去引用和依赖HSP包。 **一个应用内部,可能会包含很多的不同的能力,也就是会分成很多的ability module,因此往往就会有多个hap文件**。尽管都是HAP类型的文件,但它们也有差异。像entry类型的模块,就是项目的入口。这个模块,主要开发的就是项目的一些入口的界面等信息,以及整个项目或应用的主能力模块,而剩下的一些拓展模块,像微信小程序、视频号,就会放在Feature模块的HAP中。所以,**一个应用的内部有且只能有一个entry类型的HAP,但是Feature类型的HAP可以有多个**。 虽然可能会有多个HAP文件,最终肯定还是要合并在一起的,合并在一起之后,我们把它称之为Bundle。这个Bundle有一个自己的名字,叫bundle name。这个**bundle name可以理解成整个应用的唯一标识**,将来整个Bundle会合并打包,最后变成一个APP。 上面就是**Stage模型在编译期的结构**,之所以要采用多Hap文件的这种打包模式,**①为了降低不同模块之间的耦合。**每个HAP都可以独立编译运行②在下载安装应用的时候,可以选择性地安装,比如先安装它的核心模块entry,其他的feature可以选择性地安装,这样就可以**降低应用在安装时所占用的体积**。 ![截图](e545f4c083f79296b9693d29d62e1ad6.png) 接下来我们来看一下Stage模型在**运行期**中的一些核心的概念。我们知道,每一个HAP都是可以独立运行,而HAP在运行的时候,为了展示我们看到的一些界面和他的一些能力,就会创建一个名为ability stage的实例。而AbilityStage又有很多类型,比较常见的就是UI ability这种应用组件。顾名思义,就是应用的一些UI界面的组件,是系统调度的基本单元。其他的应用组件,比如说ExtensionAbility,也就是拓展的能力组件,比如我们一些应用卡片:桌面卡片、特殊的输入法展示模式。 主流的应用,都是基于UI ability开发的。作为用于展示的UI界面,UI ability在展示组件的时候,他首先会去持有一个WindowStage的实例对象。(UI组件内部,首先要有一个窗口,这个窗口是要展示在这个“舞台”上的,所以WindowStage的上面就会持有一个Window(窗口)对象。之后我们的UI界面,就在Window中展示) 综上,Stage模型,采用的是这种“舞台”的机制。先给你一个用于展示组件的AbilityStage->在这个Stage上,创建UIAbility(应用组件)->这个组件要渲染UI,首先会持有一个WindoStage->在窗口的Stage上,会有窗口->我们就可以在窗口中去绘制UI页面了。正是因为有很多Stage,所以这种模型被称为Stage模型。 ![截图](7aafa8be7835f3fbb26d056dc7a0691a.png) 为什么要用这种“舞台”的机制呢?正是由于这种机制,ability和窗口就分割开,它们之间就解耦了,将来在开发跨设备的一些应用的时候,就可以针对不同的设备,对它的窗口单独地去做裁剪等,以适配不同的设备。 ## 应用配置文件 ![截图](92097a527933d91bf0166e0189be16bd.png) ![截图](e2fcf1543e4c079e67b08267b20bbb82.png) > 并非应用在桌面的图标和名称,而是应用在应用列表的图标和名称(设置中能查看) ![截图](a50ecb611b100efd96ddf3ce246822a3.png) deviceTypes: 设备类型。比如A模块是给手机用的,B模块是给手机和平板用的,C模块是给电视用的。这样能实现一套代码,多端都适用。 deliverWithInstall:模块有entry和feature,shared之分,entry是必定安装的,feature类型则可以按需去安装。此处若为true,则当前feature跟随整个app一起安装,是必须安装的;若为false,则可安装也可不安装。 pages:当前模块包含的所有页面 abilities:一个模块下,可以有多个ability。 - name:ability的名字; - srcEntry:ability对应的源码; - description:ability的描述; - icon:当前ability的图标; - label:当前ability的标签(**若当前模块为入口entry模块,且这个ability是entry模块的入口ability。则当前ability的图标和label,即为当前app的入口图标和label**) - startWindowIcon:应用启动时展示的图标 - startWindowBackground:应用启动时展示的背景 - skills:ability代表一个应用组件,一个应用组件负责一定的能力或功能,所负责的能力或功能就需要在skills下去指定。skills跟ability之间的跳转有关系。 - ”entity.system.home“:代表此ability是入口ability,即项目入口。 为了避免一个一个文件去改相关参数,可以打开编辑器搜索关键字段,快捷更改 ![截图](45dcd5d0027400d8f6b0661089ecf764.png) ![截图](8846f245f9f466525dea49256ea9a50b.png) ## UIAbility生命周期 ![截图](dba877681eeb63512eeb3e66982a8695.png) ![截图](ee60ffa51ee91d4218dab4fa61ec2916.png) ![截图](57723c5cc5fa3caee8869b0757558d33.png) 随着UIAbility的创建,并不会立刻切换到前台。而是 ①创建一个WindowStage ②把UIAbility切换到前台,此时WindowStage的状态由不可见(InVisible)变成可见(Visible),并获取焦点(Active)。 ③将来UIAbility展示出来后,我们还可能会把应用切到后台,这个时候WindowStage的状态会变成失焦和不可见,接着再把整个UIAbility连着WindowStage一起挪到后台去。 ④在销毁时,会先销毁WindowStage,再销毁UIAbility。 ## 页面及组件生命周期 ![截图](7251c96cbff614c474857d5d060fa690.png) ## UIAbility的启动模式 ![截图](0b47107d59fc1b51ade51c637b0d0cc0.png) ![截图](fae8e92002f1883a2c2446e890eddca0.png) multition模式:会销毁旧的实例。(回到桌面后,重新打开应用,只看到一个UIAbility) standard模式:不会销毁旧的实例(回到桌面后,重新打开应用,原来那个UIAbility还在) ![截图](c5e0eb30e2e42d6143305222f4ace4b3.png) 启动其他UIAbility ①传入key的标识 ![截图](06ee488c513c6cdd3b03f6036deea249.png) ②根据key标识,生成key实例 ③配置AbilityStage路径。 ![截图](39e97b9a37ad83f2ece68dd868babd27.png) # 网络连接 ## Http数据请求 ![截图](d205ffeb876bf7a9451aa6689ac10a91.png) ## 第三方库axios #### 1. [下载和安装ohpm](https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V3/ide-command-line-ohpm-0000001490235312-V3) 安装第三方库,首先要安装ohpm。ohpm是openharmony的package manager的缩写,所有鸿蒙第三方库的包的管理工具。 ![截图](10d30b0ddf088af542f9e2bde4c2d06d.png) #### 2. 下载和安装axios [OpenHarmony三方库中心仓](https://ohpm.openharmony.cn/#/cn/home) ![截图](1a1886d040e0b9cca6a1ca6daa8bc94c.png) ①在IDE终端执行安装命令 ![截图](0a89fd0512d09974e33124a6af444402.png) ②待安装完成后,最外层的oh-package.json5中的依赖字段有axios显示,就说明安装成功了 ![截图](c1077d1fe388b06c974d786f27ec88df.png) 同时,见此过滤清除后 ![截图](bfce32bf2fb03721253e3e83b0becc5f.png) 目录中就会多出来一个oh_modules目录,此目录即为我们安装的第三方库还有三方库依赖的一些库。 ![截图](f49384e2d0cd4bd6eb972dab317486e9.png) #### 3. 使用axios ![截图](32fd8492afb469c8e5b8991a84a8af83.png) # 数据持久化 ## 用户首选项 **用户首选项(Preference)为应用提供==Key-Value键值型==的数据处理能力,支持应用持久化==轻量级数据==** ![截图](68758ba154d3533e5a492aa18444d2ba.png) 像小说阅读器页面这种场景,就适合用用户首选项,进行数据持久化保存。 那么用户首选项,是怎样保证数据持久化的呢? ![截图](ce91ba23426f5100f30dee33e65fab0e.png) 在应用里面,如果你想去使用用户首选项,你得先 **创建一个用户首选项实例**,而一个应用内部,可以创建多个首选项实例。这样,在应用中,我们就可以不同模块用不同的实例,避免项目之间干扰。而每一个首选项实例,都会对应应用沙箱的一个持久化文件。我们去调用用户首选项相关接口去做数据读写的时候,它就会去操作应用沙箱的持久化文件,从而实现对于数据持久化以及读写操作。 ![截图](8692df9d0a889fd254f5cef34d277e8d.png) > 说明 > > - Key为string类型,要求非空且长度不超过80字节。 > - Value可以是string、number、boolean及以上类型数组,大小不超过8192字节 > - 数据量建议不超过一万条 ## 关系型数据库 ![截图](7702557107f97e9fb6e701c4fc3c4dfd.png) 关系型数据库(RDB)是基于SQLit组件提供的本地数据库,用于管理应用中的结构化数据。例如:记账本、备忘录 ①初始化数据库 ![截图](adce133ea92e7d01bb34b8f094ca9fbc.png) ②增、删、改数据 ![截图](f42b27358460dc78be4e32f930e35212.png) ③查询数据 ![截图](62e03ff46a785ce18ed373e455aceee4.png) > 提示 > > rdbStore里有一个特殊行号-1,表示1.2位置result指针初始指向的一个位置(未指向任何一行)。 # 通知 ## 基础通知 应用可以通过通知接口发送通知消息,提醒用户关注应用中的变化。用户可以在通知栏查看和操作通知内容. ![截图](fea76b56e18fbdebe34d7d00ccf0fb83.png) 通知类型 ![截图](5135a40742a71d659abba99d96081f45.png) ①普通文本型 ![截图](a7e112d3735f2bc3ef3f9851eef2c938.png) ②长文本型 ![截图](a8cada82f5bb27be6bea2e41894a459b.png) longText,briefText,expandedTitle默认不展示.当用户点击展开按钮后,就会用longText和expandedTitle替换原通知中的内容. ![截图](630917b6d18b03c8fd4a78c20afcf473.png) ③多行文本型 ![截图](b2133b591081c66c120c85a6821e8ca3.png) 多行文本型和长文本型类似,也是三个标准参数,三个特殊参数 ![截图](b373d946a43f6cecb06edeca4902703e.png) ④图片型 ![截图](2373693cd925502df26b4fe81ec84552.png) 其他 ![截图](85b9f0531f012237edaa5a68d2ffc9a4.png) ## 进度条通知 进度条通知会展示一个动态的进度条,主要用于文件下载、长任务处理的进度显示。 ![截图](0b92bd22c79bb80a1da9639268cfa60c.png) ## 通知意图 我们可以给通知或其中的按钮设置的行为意图(want),从而实现拉起应用组件或发布公共事件等能力。 ![截图](bc56d752d915a2ed6f192b8e8c79ced2.png) # NAPI ## NAPI是什么? NAPI 类似于 Java 里的 JNI,它是 OpenHarmony 系统提供的一套原生模块扩展开发框架,它遵循了 Node.js 的 NAPI 接口规范并在方舟引擎内(ArkNativeEngine)内部做了自有实现,NAPI 为开发者提供了 JS 与 C/C++ 模块之间相互调用的交互能力,交互流程如下图所示: ![截图](a7f03037bbae35028f5527bc54c572e9.png) ## NAPI项目简述 要把arkTs代码和Napi层联系起来,要确保 ### ①ets页面引入了lib×××.so ### ②cpp的lib×××中的index.d.ts里要写上和ets文件相关联的函数头(名字相同),并在函数头前加export导出改方法。 ![截图](79fab8d69d269ab79bc68a772d003844.png) ### ③lib×××目录下,oh-package.json5文件,该文件是打包的配置文件,内容如下所示: ```json { "name": "libplayerSample.so", "types": "./index.d.ts", "version": "1.0.0", "decription": "Player sample interface." } ``` 设置 libplayerSample.so 库和 index.d.ts 相关联,便于在 TS 文件中引入 libplayerSample.so 时调用库中的相关方法。 ### ④配置 oh-package.json5 在"dependencies"处添加你的so路径 ### ⑤配置 CMakeLists.txt ```makefile # the minimum version of CMake. # 声明使用 CMAKE 的最小版本号 cmake_minimum_required(VERSION 3.4.1) # 声明项目的名称 project(oh_0400_napi) # set命令,格式为set(key value),表示设置key的值为value,其中value可以是路径,也可以是许多文件。 # 本例中设置NATIVERENDER_ROOT_PATH的值为${CMAKE_CURRENT_SOURCE_DIR} set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR}) # 添加项目编译所需要的头文件的目录 include_directories(${NATIVERENDER_ROOT_PATH} ${NATIVERENDER_ROOT_PATH}/include) # 生成目标库文件libentry.so,entry表示最终的库名称,SHARED表示生成的是动态链接库, # hello.cpp表示最终生成的libentry.so中所包含的源码 # 如果要生成静态链接库,把SHARED该成STATIC即可 add_library(entry SHARED hello.cpp) # 把libentry.so链接到libace_napi.z.so上 target_link_libraries(entry PUBLIC libace_napi.z.so) ``` ### ⑥cpp目录下的NAPI规范的cpp文件,也需要和前端相关联 - #### 引入头文件 ```c_cpp #include "napi/native_api.h" #include #include ``` 引入头文件,作用和 TS 里的 import 类似,不再详述。 - #### 注册napi模块 ```c_cpp static napi_module demoModule = { .nm_version =1, .nm_flags = 0, .nm_filename = nullptr, .nm_register_func = Init, .nm_modname = "entry", .nm_priv = ((void*)0), .reserved = { 0 }, }; extern "C" __attribute__((constructor)) void RegisterEntryModule(void) { napi_module_register(&demoModule); } ``` ==extern "C"== 简单理解就是告诉编译器这部分代码按照 C 语言进行编译而不是 C++ 语言编译。 *==_\_attribute\_\_((constructor))==* 声明方法的执行时机,它表示 ==RegisterEntryModule()==方法在==main()==方法执行前执行,简单理解就是当前 CPP 文件被编译成动态链接库 so 后,在调用==dlopen()==方法加载该库时会先执行 ==RegisterEntryModule()==方法。该方法内又调用了==napi_module_register()==方法,==napi_module_register()== 方法是 NAPI 提供的模块注册方法,表示把定义的 demoModule 模块注册到 JS 引擎中。 ** napi_module 结构体,各字段说明如下:** - nm_version:nm版本号,默认值为 1。 - nm_flags:nm标记符,默认值为 0。 - nm_filename:暂不关注,使用默认值即可。 - ==nm_register_func:指定nm的入口函数。== - ==nm_modname:指定 TS 页面导入的模块名,例如:在ets文件中,import testNapi from 'libentry.so' 中的 testNapi 就是当前的nm_modname。== - nm_priv:暂不关注,使用默认值即可。 - reserved:暂不关注,使用默认值即可。 #### 方法定义 ```c_cpp EXTERN_C_START static napi_value Init(napi_env env, napi_value exports) { napi_property_descriptor desc[] = { {"initVideoNative", nullptr, PlayerSampleNative::InitVideo, nullptr, nullptr, nullptr, napi_default, nullptr}, }; napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc); return exports; } EXTERN_C_END ``` ==Init()==方法内声明了 napi_property_descriptor 结构体,结构体的定义看第一个和第三个参数即可,第一个参数 initVideoNative 表示应用层 ==JS==声明的方法,PlayerSampleNative::InitVideo 表示==C++==实现的方法,然后调用 NAPI 的 napi_define_properties() 方法将 ==initVideoNative ==和 ==PlayerSampleNative::InitVideo== 这俩方法做做个映射,最后通过 exports 变量对外导出,实现 JS 端调用 initVideoNative 方法时进而调用到 C++ 的 InitVideo() 方法。 完整的napi_property_descriptor 结构体定义如下: ```c_cpp typedef struct { // One of utf8name or name should be NULL. const char* utf8name; // 代表名称 napi_value name; // 代表名称 napi_callback method; // C++的方法 napi_callback getter; napi_callback setter; napi_value value; // 如果是数据属性则设置这个值 napi_property_attributes attributes; // 可读、枚举等属性 void* data; // method/getter/setter回调函数的参数 } napi_property_descriptor; ``` #### 方法实现 ```c_cpp static napi_value Add(napi_env env, napi_callback_info info) { // 获取 2 个参数,napi_value是对 JS 类型的封装 size_t requireArgc = 2; size_t argc = 2; napi_value args[2] = {nullptr}; // 参数保存的地方 // 调用napi_get_cb_info方法,从 info 中读取传递进来的参数放入args里 napi_get_cb_info(env, info, &argc, args , nullptr, nullptr); // 获取参数并校验类型 napi_valuetype valuetype0; napi_typeof(env, args[0], &valuetype0); napi_valuetype valuetype1; napi_typeof(env, args[1], &valuetype1); // 调用napi_get_value_double把 napi_value 类型转换成 C++ 的 double 类型 double value0; napi_get_value_double(env, args[0], &value0); double value1; napi_get_value_double(env, args[1], &value1); // 调用napi_create_double方法把 C++类型转换成 napi_value 类型 napi_value sum; napi_create_double(env, value0 + value1, &sum); //计算并返回参数 // 返回 napi_value 类型 return sum; } ``` ==Add()==方法注释的很清楚,首先从 napi_callback_info 中读取 napi_value 类型的参数放入到 args 中,然后从 args 中读取参数并把 napi_value 类型转换成 C++ 类型后进行加操作,最后把相加的结果转换成 napi_value 类型并返回。 ```c_cpp napi_value PlayerSampleNative::InitVideo(napi_env env, napi_callback_info info) { int32_t inputFd = true; // C++类型参数 int64_t inputOffset = 0; int64_t inputSize = 0; bool apiFlag = false; size_t argc = 4; // 参数个数 napi_value args[4] = {nullptr}; // 参数列表 // 调用napi_get_cb_info方法,从 info 中读取传递进来的参数放入args里 napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); // 调用napi_get_value_×××方法,把 napi_value 类型转换成 C++ 类型 napi_get_value_int32(env, args[0], &inputFd); napi_get_value_int64(env, args[1], &inputOffset); napi_get_value_int64(env, args[2], &inputSize); napi_get_value_bool(env, args[3], &apiFlag); // 将deffered对象和JS的回调:Promise对象进行绑定 napi_value promise; napi_deferred deferred; napi_create_promise(env, &deferred, &promise); // 回调部分 AsyncCallbackInfo *asyncCallbackInfo = new AsyncCallbackInfo(); asyncCallbackInfo->env = env; asyncCallbackInfo->asyncWork = nullptr; asyncCallbackInfo->deferred = deferred; asyncCallbackInfo->inputFd = inputFd; asyncCallbackInfo->inputOffset = inputOffset; asyncCallbackInfo->inputSize = inputSize; asyncCallbackInfo->apiFlag = apiFlag; // 异步调用 napi_value resourceName; napi_create_string_latin1(env, "videoTest", NAPI_AUTO_LENGTH, &resourceName); napi_create_async_work(env, nullptr, resourceName, [](napi_env env, void *data) { NativeInitVideo(env, data); }, [](napi_env env, napi_status status, void *data) { DealCallBack(env, data); }, (void *)asyncCallbackInfo, &asyncCallbackInfo->asyncWork ); napi_queue_async_work(env, asyncCallbackInfo->asyncWork); return promise; } ``` #### 解释 1. javaScript采用一对对象来实现promise,每次调用napi_create_promise对象来创建一个JavaScript promise后,同时会创建一个deferred对象,该deferred对象和Promise对象进行绑定,并且是用来解析(resolve)或者拒绝(reject)Promise的唯一方法,分别使用==napi_resolve_deferred==以及==napi_reject_deferred()==.在这两个方法中,deffered对象被释放,promise对象返回给js。简单例子: ```javascript napi_deferred deferred; napi_value promise; napi_status status; // Create the promise. status = napi_create_promise(env, &deferred, &promise); if(status != napi_ok) return NULL; //Pass the deferred to a function that performs an asynchronous action. do_something_asynchronous(deferred); // 调用异步方法,传入deferred对象,promise直接返回 // Return the promise to JS return promise; ``` 2. napi_create_async_work 该方法分配一个napi_async_work对象,来异步执行处理逻辑,并被napi_delete_async_work回收。 ```javascript // Methods to manage simple async operations NAPI_EXTERN napi_status napi_create_async_work( // 方法调用者的运行环境,包含 JS 引擎等。 napi_env env, // 可选对象,和异步work关联,将被传递到async_works中,一般都传null napi_value async_resource, // 标识提供给async_works的资源名称 napi_value async_resource_name, // native函数,用以执行异步的处理逻辑,启动worker线程,可以和eventloop线程并行 napi_async_execute_callback execute, // native函数,异步处理逻辑执行完成或者被中断执行,该方法是从main eventloop线程调用的 napi_async_complete_callback complete, // 用户提供的数据,作为上下文,会被传递到execute和complete方法中 void* data, // 指向最新创建的napi_async_work napi_async_work* result); ``` ⑦(私货)检查Xcomponent的libraryname字段是否写对 ## napi_value数据类型 OpenHarmony NAPI 将 ECMAScript 标准中定义的 Boolean、Null、Undefined、Number、BigInt、String、Symbol和 Object 这八种数据类型以及函数对应的 Function 类型统一封装成了 napi_value 类型,它是 JS 数据类型和 C/C++ 数据类型之间的桥梁,[napi_value](https://nodejs.org/api/n-api.html#napi_value)官网说明如下: > // This is an opaque pointer that is used to represent a JavaScript value. napi_value 表示 JS 值的不透明指针,在 C/C++ 端要使用 JS 端传递的数据类型,都是通过 NAPI 提供的相关方法把 napi_value 转换成 C/C++ 类型后再使用,同理当需要把 C/C++的数据传递给 JS 应用层也要通过 NAPI 提供的方法把 C/C++ 端的数据转换成 napi_value 再向上传递。 ![截图](4b266e61fcb3c862df4fc562ddf1458f.png) ### C/C++转napi_value NAPI提供了 napi_create_ 开头的方法表示把 C/C++ 类型转换成 napi_value 类型,常见方法如下所示: #### int类型转换 ```typescript NAPI_EXTERN napi_status napi_create_int32(napi_env env, int32_t value, napi_value* result); NAPI_EXTERN napi_status napi_create_uint32(napi_env env, uint32_t value, napi_value* result); NAPI_EXTERN napi_status napi_create_int64(napi_env env, int64_t value, napi_value* result); ``` 把 C/C++ 的 int32_t、uint32_t 以及 int64_t 类型转换成 napi_value 类型,参数说明如下: - env:方法调用者的运行环境,包含 JS 引擎等。 - value:C/C++端的 int 类型的值。 - result:napi_value,返回给 JS 应用层的数据 #### double类型转换 ```typescript NAPI_EXTERN napi_status napi_create_double(napi_env env, double value, napi_value* result); ``` 把 C/C++ 端的 double 类型转换成 napi_value 类型,参数说明如下: - env:方法调用者的运行环境,包含 JS 引擎等。 - value:C/C++ 端的 double 类型的值。 - result:napi_value,返回给 JS 应用层的数据。 #### string类型转换 ```typescript NAPI_EXTERN napi_status napi_create_string_latin1(napi_env env, const char* str, size_t length, napi_value* result); NAPI_EXTERN napi_status napi_create_string_utf8(napi_env env, const char* str, size_t length, napi_value* result); NAPI_EXTERN napi_status napi_create_string_utf16(napi_env env, const char16_t* str, size_t length, napi_value* result); ``` 把 C/C++ 端的 char 类型转换成 napi_value 类型,参数说明如下: - env:方法调用者的运行环境,包含 JS 引擎等。 - str:C/C++端的字符串类型的值。 - size_t:str 的长度。 - result:napi_value,返回给 JS 应用层的数据。 ### napi_value转C/C++ NAPI提供了 napi_get_value_ 开头的方法表示把 napi_value 转换成 C/C++ 类型,常见方法如下所示: #### int类型转换 ```typescript NAPI_EXTERN napi_status napi_get_value_int32(napi_env env, napi_value value, int32_t* result); NAPI_EXTERN napi_status napi_get_value_uint32(napi_env env, napi_value value, uint32_t* result); NAPI_EXTERN napi_status napi_get_value_int64(napi_env env, napi_value value, int64_t* result); ``` 把 JS 端的 number 类型转换成 C/C++ 的对应数据类型,参数说明如下: - env:方法调用者的运行环境,包含 JS 引擎等。 - value:JS 端传递进来的数据。 - result:接收 value 的值。 #### double类型转换 ```typescript NAPI_EXTERN napi_status napi_get_value_double(napi_env env, napi_value value, double* result); ``` 把 JS 端的 number 类型转换成 C/C++ 的 double 类型,参数说明如下: - env:方法调用者的运行环境,包含 JS 引擎等。 - value:JS 端传递进来的数据。 - result:接收 value 的值。 #### string类型转换 ```typescript NAPI_EXTERN napi_status napi_get_value_string_latin1(napi_env env, napi_value value, char* buf, size_t bufsize, size_t* result); // Copies UTF-8 encoded bytes from a string into a buffer. NAPI_EXTERN napi_status napi_get_value_string_utf8(napi_env env, napi_value value, char* buf, size_t bufsize, size_t* result); // Copies UTF-16 encoded bytes from a string into a buffer. NAPI_EXTERN napi_status napi_get_value_string_utf16(napi_env env, napi_value value, char16_t* buf, size_t bufsize, size_t* result); ``` 把 JS 端的 string 类型转换成 C/C++ 的 char 类型,参数说明如下: - env:方法调用者的运行环境,包含 JS 引擎等。 - value:napi_value,JS 端传递进来的数据。 - buf:char数组,用来存放napi_value中的 string 值 - bufsize:char数组长度 - result:接收 value 的值。 #### boolean类型转换 ```typescript NAPI_EXTERN napi_status napi_get_value_bool(napi_env env, napi_value value, bool* result); ``` 把 JS 端的 boolean 类型转换成 C/C++ 的 bool 类型,参数说明如下: - env:方法调用者的运行环境,包含 JS 引擎等。 - value:JS 端传递进来的数据。 - result:接收 value 的值。 ## NAPI异步编程 ### 约定编程规范 ArkUI 开发框架对外提供的 API 命名是需遵守一定规范的,以==@ohos.display==模块提供的 API 为例,源码如下所示: ```typescript declare namespace display { function getDefaultDisplay(callback: AsyncCallback): void; function getDefaultDisplay(): Promise; function getDefaultDisplaySync(): Display; } ``` 根据该模块提供的方法,根据方法的命名规则可以得出 2 条规范: - 同步调用: - 方法名+ Sync 关键字,如:==getMd5Sync():string== - 异步调用: - 需要提供 AsyncCallback 和 Promise 的实现,如:==getMd5(): Promise==、==getMd5(callback: AsyncCallback)== 因此,我们在==index.d.ts==中声明 NAPI 方法时也按照系统约定的规范来。 ### 定义异步方法 ```typescript // index.d.ts export const add: (a: number, b: number) => number; // 声明异步方法 export function getMd5(value: string, callback: (md5: string) => void): void; export function getMd5(value: string): Promise; // 声明同步方法 export function getMd5Sync(value: string): string; ``` ==getMd5Sync()==表示同步实现 MD5 的计算,==getMd5() ==表示异步实现 MD5 的调用。 ### 实现异步方法 声明完 JS 端的方法后,接着在 cpp文件中实现对应的方法,步骤如下: - 添加映射 在 hello.cpp 的 Init() 方法里添加 JS 端的方法映射,代码如下所示: ```c_cpp static napi_value Init(napi_env env, napi_value exports) { napi_property_descriptor desc[] = { {"add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr}, {"getMd5Sync", nullptr, GetMd5Sync, nullptr, nullptr, nullptr, napi_default, nullptr}, {"getMd5", nullptr, GetMd5, nullptr, nullptr, nullptr, napi_default, nullptr}, }; napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc); return exports; } ``` - 方法实现 getMd5() 的 C++ 端代码如下所示: ```c_cpp // 定义异步线程执行中需要的上下文环境 struct Md5Context { // 异步 worker napi_async_work work; // 对应 JS 端的 callback 函数 napi_ref callback; // 对应 JS 端的 promise 对象 napi_deferred promise; // 传递进来的参数 string params; // 计算后的结果 string result; }; // 在子线程中执行 static void doInBackground(napi_env env, void *data) { Md5Context *md5Context = (Md5Context *)data; // 模拟耗时操作,进行 MD5 计算 string md5 = MD5(md5Context->params).toStr(); // 计算后的 MD5 字存储到 result 中 md5Context->result = md5; // 模拟耗时操作,让当前线程休眠 3 秒钟 std::this_thread::sleep_for(std::chrono::seconds(3)); } // 切换到主线程 static void onPostExecutor(napi_env env, napi_status status, void *data) { Md5Context *md5Context = (Md5Context *)data; napi_value returnValue; if (napi_ok != napi_create_string_utf8(env, md5Context->result.c_str(), md5Context->result.length(), &returnValue)) { delete md5Context; md5Context = nullptr; napi_throw_error(env, "-111", "napi_create_string_utf8: error"); return; } if (md5Context->callback) { // 取出缓存的 js 端的 callback napi_value callback; if (napi_ok != napi_get_reference_value(env, md5Context->callback, &callback)) { delete md5Context; md5Context = nullptr; napi_throw_error(env, "-111", "napi_get_reference_value error"); return; } napi_value tempValue; // 调用 callback,把值回调给 JS 端 napi_call_function(env, nullptr, callback, 1, &returnValue, &tempValue); // 删除 callback napi_delete_reference(env, md5Context->callback); } else { // 以 promise 的形式回调数据 if (napi_ok != napi_resolve_deferred(env, md5Context->promise, returnValue)) { delete md5Context; md5Context = nullptr; napi_throw_error(env, "-111", "napi_resolve_deferred error"); } } // 删除异步任务并释放资源 napi_delete_async_work(env, md5Context->work); delete md5Context; md5Context = nullptr; } static napi_value GetMd5(napi_env env, napi_callback_info info) { // 1、从 info 中读取 JS 传递过来的参数放入 args 里 size_t argc = 2; napi_value args[2] = {nullptr}; if (napi_ok != napi_get_cb_info(env, info, &argc, args, nullptr, nullptr)) { napi_throw_error(env, "-1001", "napi_get_cb_info error"); return nullptr; } // 2、读取传入的参数类型 napi_valuetype stringType = napi_undefined; if (napi_ok != napi_typeof(env, args[0], &stringType)) { napi_throw_error(env, "-1002", "napi_typeof string error"); return nullptr; } // 3、传入的 string 如果为 null 或者 undefined 则抛异常 if (napi_null == stringType || napi_undefined == stringType) { napi_throw_error(env, "-1003", "input params null or undefined"); return nullptr; } // 4、读取传入的 string 内容长度 size_t length = 0; if (napi_ok != napi_get_value_string_utf8(env, args[0], nullptr, 0, &length)) { napi_throw_error(env, "-1004", "get string length error"); return nullptr; } // 5、判断传入的 string 长度是否符合 if (0 == length) { napi_throw_error(env, "-1005", "string length can't be zero"); return nullptr; } // 6、读取传入的 string 长度读取内容 char *buffer = new char[length + 1]; if (napi_ok != napi_get_value_string_utf8(env, args[0], buffer, length + 1, &length)) { delete[] buffer; buffer = nullptr; napi_throw_error(env, "-1006", "napi_get_value_string_utf8 string error"); return nullptr; } // 7、读取 JS 有没有传递 callback,如果 callback 为 null 就表示是 promise 的回调方式 napi_valuetype callbackType = napi_undefined; napi_status callbackStatus = napi_typeof(env, args[1], &callbackType); if (napi_ok != callbackStatus && napi_invalid_arg != callbackStatus) { delete[] buffer; buffer = nullptr; napi_throw_error(env, "-1004", "napi_typeof function error"); return nullptr; } // 8、创建一个异步线程需要的数据 model,把传递过来的参数加入进去做下缓存 auto context = new Md5Context(); context->params = buffer; napi_value returnValue = nullptr; // 9、判断是 callback 的回调方式还是 promise 的回调方式 if (napi_function == callbackType) { // 如果是 callback 的回调方式,需要创建 callback 的引用 napi_ref callback; if (napi_ok != napi_create_reference(env, args[1], 1, &callback)) { delete[] buffer; delete context; buffer = nullptr; context = nullptr; napi_throw_error(env, "-11", "napi_create_reference error"); return nullptr; } // 缓存 callback context->callback = callback; // 临时返回一个 undefined 值给 JS 端 napi_get_undefined(env, &returnValue); } else { // promise 的回调方式,创建一个 Promise 的引用 napi_deferred promise; if (napi_ok != napi_create_promise(env, &promise, &returnValue)) { delete[] buffer; delete context; buffer = nullptr; context = nullptr; napi_throw_error(env, "-11", "napi_create_promise error"); return nullptr; } // 缓存 promise context->promise = promise; } napi_value resourceName; if (napi_ok != napi_create_string_utf8(env, "GetMd5", NAPI_AUTO_LENGTH, &resourceName)) { delete[] buffer; delete context; buffer = nullptr; context = nullptr; napi_throw_error(env, "-11", "napi_create_string_utf8 resourceName error"); return nullptr; } // 10、创建一个异步任务 napi_async_work asyWork; napi_status status = napi_create_async_work(env, nullptr, resourceName, doInBackground, onPostExecutor, (void *)context, &asyWork); if (napi_ok != status) { delete[] buffer; delete context; buffer = nullptr; context = nullptr; napi_throw_error(env, "-11", "napi_create_async_work error"); return nullptr; } // 11、保存异步任务 context->work = asyWork; // 12、添加进异步队列 napi_queue_async_work(env, asyWork); return returnValue; } ``` getMd5() 部分的代码,前 6 个小步骤是对传递进来的参数做基础校验,第 7 步是根据参数判断当前异步执行的回调方式是 Promise 还是 Callback。第 8 步创建了一个==Md5Context==对象,它的作用是把当前相关参数缓存下来目的是接下来在异步线程里使用这些参数,第 9 步根据异步回调的方法创建 Promise 或者 Callback 然后把他们保存在==Md5Context== 对象里。第 10 步创建一个异步任务,然后把异步任务添加进异步队列中。
napi_create_async_work() 方法的第 3 、 4 个参数需要注意,==doInBackground()==方法是在异步线程中执行的,onPostExecutor() 方法在异步线程结束后切换到主线程中执行。 ### 完整代码 ```c_cpp #include #include #include "napi/native_api.h" #include #include #include #include #include #include #include "./md5/md5.h" // 定义异步线程执行中需要的上下文环境 struct Md5Context { // 异步 worker napi_async_work work; // 对应 JS 端的 callback 函数 napi_ref callback; // 对应 JS 端的 promise 对象 napi_deferred promise; // 传递进来的参数 string params; // 计算后的结果 string result; }; static void doInBackground(napi_env env, void *data) { Md5Context *md5Context = (Md5Context *)data; // 模拟耗时操作,进行 MD5 计算 string md5 = MD5(md5Context->params).toStr(); // 计算后的 MD5 字存储到 result 中 md5Context->result = md5; // 模拟耗时操作,让当前线程休眠 3 秒钟 std::this_thread::sleep_for(std::chrono::seconds(3)); } static void onPostExecutor(napi_env env, napi_status status, void *data) { Md5Context *md5Context = (Md5Context *)data; napi_value returnValue; if (napi_ok != napi_create_string_utf8(env, md5Context->result.c_str(), md5Context->result.length(), &returnValue)) { delete md5Context; md5Context = nullptr; napi_throw_error(env, "-111", "napi_create_string_utf8: error"); return; } if (md5Context->callback) { // 取出缓存的 js 端的 callback napi_value callback; if (napi_ok != napi_get_reference_value(env, md5Context->callback, &callback)) { delete md5Context; md5Context = nullptr; napi_throw_error(env, "-111", "napi_get_reference_value error"); return; } napi_value tempValue; // 调用 callback,把值回调给 JS 端 napi_call_function(env, nullptr, callback, 1, &returnValue, &tempValue); // 删除 callback napi_delete_reference(env, md5Context->callback); } else { // 以 promise 的形式回调数据 if (napi_ok != napi_resolve_deferred(env, md5Context->promise, returnValue)) { delete md5Context; md5Context = nullptr; napi_throw_error(env, "-111", "napi_resolve_deferred error"); } } // 删除异步任务并释放资源 napi_delete_async_work(env, md5Context->work); delete md5Context; md5Context = nullptr; } static napi_value GetMd5(napi_env env, napi_callback_info info) { // 1、从 info 中读取 JS 传递过来的参数放入 args 里 size_t argc = 2; napi_value args[2] = {nullptr}; if (napi_ok != napi_get_cb_info(env, info, &argc, args, nullptr, nullptr)) { napi_throw_error(env, "-1001", "napi_get_cb_info error"); return nullptr; } // 2、读取传入的参数类型 napi_valuetype stringType = napi_undefined; if (napi_ok != napi_typeof(env, args[0], &stringType)) { napi_throw_error(env, "-1002", "napi_typeof string error"); return nullptr; } // 3、传入的 string 如果为 null 或者 undefined 则抛异常 if (napi_null == stringType || napi_undefined == stringType) { napi_throw_error(env, "-1003", "input params null or undefined"); return nullptr; } // 4、读取传入的 string 内容长度 size_t length = 0; if (napi_ok != napi_get_value_string_utf8(env, args[0], nullptr, 0, &length)) { napi_throw_error(env, "-1004", "get string length error"); return nullptr; } // 5、判断传入的 string 长度是否符合 if (0 == length) { napi_throw_error(env, "-1005", "string length can't be zero"); return nullptr; } // 6、读取传入的 string 长度读取内容 char *buffer = new char[length + 1]; if (napi_ok != napi_get_value_string_utf8(env, args[0], buffer, length + 1, &length)) { delete[] buffer; buffer = nullptr; napi_throw_error(env, "-1006", "napi_get_value_string_utf8 string error"); return nullptr; } // 7、读取 JS 有没有传递 callback,如果 callback 为 null 就表示是 promise 的回调方式 napi_valuetype callbackType = napi_undefined; napi_status callbackStatus = napi_typeof(env, args[1], &callbackType); if (napi_ok != callbackStatus && napi_invalid_arg != callbackStatus) { delete[] buffer; buffer = nullptr; napi_throw_error(env, "-1004", "napi_typeof function error"); return nullptr; } // 8、创建一个异步线程需要的数据 model,把传递过来的参数加入进去做下缓存 auto context = new Md5Context(); context->params = buffer; napi_value returnValue = nullptr; // 9、判断是 callback 的回调方式还是 promise 的回调方式 if (napi_function == callbackType) { // 如果是 callback 的回调方式,需要创建 callback 的引用 napi_ref callback; if (napi_ok != napi_create_reference(env, args[1], 1, &callback)) { delete[] buffer; delete context; buffer = nullptr; context = nullptr; napi_throw_error(env, "-11", "napi_create_reference error"); return nullptr; } // 缓存 callback context->callback = callback; // 临时返回一个 undefined 值给 JS 端 napi_get_undefined(env, &returnValue); } else { // promise 的回调方式,创建一个 Promise 的引用 napi_deferred promise; if (napi_ok != napi_create_promise(env, &promise, &returnValue)) { delete[] buffer; delete context; buffer = nullptr; context = nullptr; napi_throw_error(env, "-11", "napi_create_promise error"); return nullptr; } // 缓存 promise context->promise = promise; } napi_value resourceName; if (napi_ok != napi_create_string_utf8(env, "GetMd5", NAPI_AUTO_LENGTH, &resourceName)) { delete[] buffer; delete context; buffer = nullptr; context = nullptr; napi_throw_error(env, "-11", "napi_create_string_utf8 resourceName error"); return nullptr; } // 10、创建一个异步任务 napi_async_work asyWork; napi_status status = napi_create_async_work(env, nullptr, resourceName, doInBackground, onPostExecutor, (void *)context, &asyWork); if (napi_ok != status) { delete[] buffer; delete context; buffer = nullptr; context = nullptr; napi_throw_error(env, "-11", "napi_create_async_work error"); return nullptr; } // 11、保存异步任务 context->work = asyWork; // 12、添加进异步队列 napi_queue_async_work(env, asyWork); return returnValue; } static napi_value GetMd5Sync(napi_env env, napi_callback_info info) { // 1、从info中取出JS传递过来的参数放入args size_t argc = 1; napi_value args[1] = {nullptr}; if (napi_ok != napi_get_cb_info(env, info, &argc, args, nullptr, nullptr)) { napi_throw_error(env, "-1000", "napi_get_cb_info error"); return nullptr; } // 2、获取参数的类型 napi_valuetype stringType; if (napi_ok != napi_typeof(env, args[0], &stringType)) { napi_throw_error(env, "-1001", "napi_typeof error"); return nullptr; } // 3、如果参数为null或者undefined,则抛异常 if (napi_null == stringType || napi_undefined == stringType) { napi_throw_error(env, "-1002", "the param can't be null"); return nullptr; } // 4、获取传递的string长度 size_t length = 0; if (napi_ok != napi_get_value_string_utf8(env, args[0], nullptr, 0, &length)) { napi_throw_error(env, "-1003", "napi_get_value_string_utf8 error"); return nullptr; } // 5、如果传递的是"",则抛异常 if (length == 0) { napi_throw_error(env, "-1004", "the param length invalid"); return nullptr; } // 6、读取传递的string参数放入buffer中 char *buffer = new char[length + 1]; if (napi_ok != napi_get_value_string_utf8(env, args[0], buffer, length + 1, &length)) { delete[] buffer; buffer = nullptr; napi_throw_error(env, "-1005", "napi_get_value_string_utf8 error"); return nullptr; } // 7、计算MD5加密操作 std::string str = buffer; str = MD5(str).toStr(); // 8、把C++数据转成napi_value并返回 napi_value value = nullptr; const char *md5 = str.c_str(); if (napi_ok != napi_create_string_utf8(env, md5, strlen(md5), &value)) { delete[] buffer; buffer = nullptr; napi_throw_error(env, "-1006", "napi_create_string_utf8 error"); return nullptr; } // 9、资源清理 delete[] buffer; buffer = nullptr; return value; } static napi_value Add(napi_env env, napi_callback_info info) { size_t requireArgc = 2; size_t argc = 2; napi_value args[2] = {nullptr}; napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); napi_valuetype valuetype0; napi_typeof(env, args[0], &valuetype0); napi_valuetype valuetype1; napi_typeof(env, args[1], &valuetype1); double value0; napi_get_value_double(env, args[0], &value0); double value1; napi_get_value_double(env, args[1], &value1); napi_value sum; napi_create_double(env, value0 + value1, &sum); return sum; } EXTERN_C_START static napi_value Init(napi_env env, napi_value exports) { napi_property_descriptor desc[] = { {"add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr}, {"getMd5Sync", nullptr, GetMd5Sync, nullptr, nullptr, nullptr, napi_default, nullptr}, {"getMd5", nullptr, GetMd5, nullptr, nullptr, nullptr, napi_default, nullptr}, }; napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc); return exports; } EXTERN_C_END static napi_module demoModule = { .nm_version = 1, .nm_flags = 0, .nm_filename = nullptr, .nm_register_func = Init, .nm_modname = "entry", .nm_priv = ((void *)0), .reserved = {0}, }; extern "C" __attribute__((constructor)) void RegisterEntryModule(void) { napi_module_register(&demoModule); } ``` ==Index.ets==的测试代码如下: ```typescript import testNapi from 'libentry.so'; @Entry @Component struct Index { @State message: string = 'Hello,OpenHarmony' build() { Column({ space: 10 }) { Text(this.message) .fontSize(20) Button("同步回调") .onClick(() => { this.message = testNapi.getMd5Sync("Hello, OpenHarmony") }) Button("异步 Callback 回调") .onClick(() => { this.message = "计算中..."; testNapi.getMd5("Hello, OpenHarmony", (md5: string) => { this.message = md5; }); }) Button("异步 Promise 回调") .onClick(() => { this.message = "计算中..."; testNapi.getMd5("Hello, OpenHarmony").then((md5: string) => { this.message = md5; }).catch((error: Error) => { this.message = "error: " + error; }) }) } .padding(10) .width('100%') .height("100%") } } ```