基本概念
ArkTS
因为ArkTS是基于Type Script扩展而来,是Type Script的超集,所以也可以关注一下Type Script的语法来理解ArkTS的语法
ArkUI
HarmonyOS提供了一套UI开发框架,即方舟开发框架(ArkUI框架)。方舟开发框架可为开发者提供应用UI开发所必需的能力,比如多种组件、布局计算、动画能力、UI交互、绘制等。
开发范式
方舟开发框架针对不同目的和技术背景的开发者提供了两种开发范式
- 基于ArkTS的声明式开发范式(简称“声明式开发范式”)
- 兼容JS的类Web开发范式(简称“类Web开发范式”)。以下是两种开发范式的简单对比。
总结:Flutter、SwiftUI等新的UI框架用的都是声明式开发范式;oc/swift/java/kotlin传统的UI框架用的是命令式开发范式。
应用模型
概念
应用模型是HarmonyOS为开发者提供的应用程序所需能力的抽象提炼,它提供了应用程序必备的组件和运行机制。有了应用模型,开发者可以基于一套统一的模型进行应用开发,使应用开发更简单、高效。请见应用模型的构成要素。
随着系统的演进发展,HarmonyOS先后提供了两种应用模型:
- FA(Feature Ability)模型: HarmonyOS API 7开始支持的模型,已经不再主推。FA模型开发可见FA模型开发概述。
- Stage模型: HarmonyOS API 9开始新增的模型,是目前主推且会长期演进的模型。在该模型中,由于提供了AbilityStage、WindowStage等类作为应用组件和Window窗口的“舞台”,因此称这种应用模型为Stage模型。Stage模型开发可见Stage模型开发概述。
FA模型和Stage模型的整体架构和设计思想等更多区别,请见应用模型解读。
快速入门提供了一个含有两个页面的开发实例,并使用了不同的开发语言或不同的应用模型进行开发,以便开发者理解以上基本概念及应用开发流程。
说明:我们用的是基于ArkTS声明式开发范式,和Stage模型架构。
构建第一个ArkTS应用(Stage模型)
应用程序包基础知识
了解应用程序包基础知识,对于理解工程结构、编译构建、模块化等有帮助。
华为官方参考文档:文档中心
Stage模型应用程序包结构
多HAP构建视图
共享包概述
HAP,HAR静态共享包(构建多份嵌入到各个HAP),HSP动态共享包(只构建一份)
语言入门
Type Script 基础教程
因为ArkTS是基于Type Script扩展而来,是Type Script的超集,所以也可以关注一下Type Script的语法来理解ArkTS的语法
Type Script官方教程:TypeScript 教程 | 菜鸟教程
主要关注点:
1.基础语法,编译产物
2.基础数据类型
3.变量声明
4.联合类型
5.接口,类
6.命名空间
ArkTS基础
ArkTS华为官方文档:文档中心
ArkTS语言,是TypeScript的超集
ArkTS是HarmonyOS优选的主力应用开发语言。ArkTS围绕应用开发在TypeScript(简称TS)生态基础上做了进一步扩展,继承了TS的所有特性,是TS的超集。因此,在学习ArkTS语言之前,建议开发者具备TS语言开发能力。
ArkTS 特性约束
- 强制使用静态类型,不支持any类型
- 显式初始化类的属性
ts:
class Person {
name: string // undefined
setName(n: string): void {
this.name = n
}
getName(): string {
// 开发者使用"string"作为返回类型,这隐藏了name可能为"undefined"的事实。
// 更合适的做法是将返回类型标注为"string | undefined",以告诉开发者这个API所有可能的返回值的类型。
return this.name
}
}
let buddy = new Person()
// 假设代码中没有对name的赋值,例如没有调用"buddy.setName('John')"
console.log(buddy.getName().length); // 运行时异常:name is undefined
arkts:
class Person {
name: string = ''
// name?: string 可以为undefined
setName(n: string): void {
this.name = n
}
// 类型为"string",不可能为"null"或者"undefined"
getName(): string {
return this.name
}
}
let buddy = new Person()
// 假设代码中没有对name的赋值,例如没有调用"buddy.setName('John')"
console.log(buddy.getName().length); // 0, 没有运行时异常
- 允许.ets文件import.ets/.ts/.js文件源码, 不允许.ts/.js文件import.ets文件源码
- 禁止运行时改变对象类型
class Point {
public x: number = 0
public y: number = 0
constructor(x: number, y: number) {
this.x = x
this.y = y
}
}
// 无法从对象中删除某个属性,从而确保所有Point对象都具有属性x
let p1 = new Point(1.0, 1.0)
delete p1.x // 在TypeScript和ArkTS中,都会产生编译时错误
delete (p1 as any).x // 在TypeScript中不会报错;在ArkTS中会产生编译时错误
// Point类没有定义命名为z的属性,在程序运行时也无法添加该属性
let p2 = new Point(2.0, 2.0)
p2.z = 'Label'; // 在TypeScript和ArkTS中,都会产生编译时错误
(p2 as any).z = 'Label' // 在TypeScript中不会报错;在ArkTS中会产生编译时错误
// 类的定义确保了所有Point对象只有属性x和y,并且无法被添加其他属性
let p3 = new Point(3.0, 3.0)
let prop = Symbol(); // 在TypeScript中不会报错;在ArkTS中会产生编译时错误
(p3 as any)[prop] = p3.x // 在TypeScript中不会报错;在ArkTS中会产生编译时错误
p3[prop] = p3.x // 在TypeScript和ArkTS中,都会产生编译时错误
// 类的定义确保了所有Point对象的属性x和y都具有number类型,因此,无法将其他类型的值赋值给它们
let p4 = new Point(4.0, 4.0)
p4.x = 'Hello!'; // 在TypeScript和ArkTS中,都会产生编译时错误
(p4 as any).x = 'Hello!' // 在TypeScript中不会报错;在ArkTS中会产生编译时错误
// 使用符合类定义的Point对象:
function distance(p1: Point, p2: Point): number {
return Math.sqrt(
(p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y)
)
}
let p5 = new Point(5.0, 5.0)
let p6 = new Point(6.0, 6.0)
console.log('Distance between p5 and p6: ' + distance(p5, p6))
- 使用let,不使用var。let关键字可以在块级作用域中声明变量,帮助程序员避免错误。因此,ArkTS不支持var,请使用let声明变量。
- ArkTS不支持any和unknown类型。显式指定具体类型。
- 使用箭头函数而非函数表达式
ts:
let f = function (s: string) {
console.log(s)
}
arkts:
let f = (s: string) => {
console.log(s)
}
ArkUI基础
声明式UI
声明式UI:使用代码直接编写的UI组件,执行代码得到对应的渲染组件执行渲染。
命令式UI:首先需要解析XML内容,映射到对应的View再进行渲染。
- 当前,ArkTS在TS基础上主要扩展了声明式UI能力,让开发者以更简洁、更自然的方式开发高性能应用。当前扩展的声明式UI包括如下特性。
-
- 基本UI描述:ArkTS定义了各种装饰器、自定义组件、UI描述机制,再配合UI开发框架中的UI内置组件、事件方法、属性方法等共同构成了UI开发的主体。
- 状态管理:ArkTS提供了多维度的状态管理机制,在UI开发框架中,和UI相关联的数据,不仅可以在组件内使用,还可以在不同组件层级间传递,比如父子组件之间、爷孙组件之间,也可以是全局范围内的传递,还可以是 跨设备传递。另外,从数据的传递形式来看,可分为只读的单向传递和可变更的双向传递。开发者可以灵活的利用这些能力来实现数据和UI的联动。
- 动态构建UI元素:ArkTS提供了动态构建UI元素的能力,不仅可以自定义组件内部的UI结构,还可复用组件样式,扩展原生组件。
- 渲染控制:ArkTS提供了渲染控制的能力。条件渲染可根据应用的不同状态,渲染对应状态下的部分内容。循环渲染可从数据源中迭代获取数据,并在每次迭代过程中创建相应的组件。
- 使用限制与扩展:ArkTS在使用过程中存在限制与约束,同时也扩展了双向绑定等能力。
- 未来,ArkTS会结合应用开发/运行的需求持续演进,逐步提供并行和并发能力增强、类型系统增强、分布式开发范式等更多特性。
组件
图解和代码示例
- 装饰器: 用于装饰类、结构、方法以及变量,赋予其特殊的含义,如上述示例中@Entry、@Component和@State都是装饰器。 具体而言,@Component表示这是个自定义组件;@Entry则表示这是个入口组件;@State表示组件中的状态变量,这个状态变换会引起UI变更。
- 自定义组件:可复用的UI单元,可组合其他组件,如上述被@Component装饰的struct Hello。
- UI描述:声明式的方法来描述UI的结构,例如build()方法中的代码块。
- 内置组件:ArkTS中默认内置的基本组件和布局组件,开发者可以直接调用,如Column、Text、Divider、Button等。
- 属性方法:用于组件属性的配置,如fontSize()、width()、height()、color()等,可通过链式调用的方式设置多项属性。
- 事件方法:用于添加组件对事件的响应逻辑,统一通过事件方法进行设置,如跟随在Button后面的onClick()。
代码实例:
@Entry
@Component
struct Demo {
@State message: string = 'Hello World'
build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
.onClick(()=>{
//点击事件
})
}
.width('100%')
}
.height('100%')
}
}
- struct:自定义组件可以基于struct实现,不能有继承关系,对于struct的实例化,可以省略new。
- build函数:作为UI描述的入口。
- 属性配置:width、 fontSize等。
- 事件配置:onClick事件。
自己的思考🤔
创建组件
组件参数:必选,可选
链式调用属性
链式调用事件:箭头函数不需要bind(this), 其他方式需要
嵌套组件/子组件:与flutter不同的是,ArkTS中的Column/Row之类的都属于容器组件(可以设置背景色),flutter中属于布局组件(无法设置背景色等容器属性)。
自定义组件
考虑代码可复用性、业务逻辑与UI分离,后续版本演进
自定义组件具有以下特点:
- 可组合:允许开发者组合使用系统组件、及其属性和方法。
- 可重用:自定义组件可以被其他组件重用,并作为不同的实例在不同的父组件或容器中使用。
- 数据驱动UI更新:通过状态变量的改变,来驱动UI的刷新。
两个装饰器:@Component, @Builder
示例
import { LVImageComponent } from '@alipay/product_content_common'
@Component
export struct AiGuideView {
imageSource?: string
onCloseEvent?: () => void
build() {
Stack() {
Column() {
Row() {
this.buildCloseButton()
}
.layoutWeight(1)
.alignItems(VerticalAlign.Top)
.justifyContent(FlexAlign.Center)
}
.backgroundColor(Color.Transparent)
.width("100%")
.height("100%")
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
.hitTestBehavior(HitTestMode.Transparent)
}
.width("100%")
.height("100%")
.alignContent(Alignment.Center)
}
@Builder
buildGuideImageView() {
LVImageComponent({
loadReq: {
source: this.imageSource ?? "",
businessId: "LIFETAB",
options: {
imageFit: ImageFit.ScaleDown
}
}
})
.width(315)
.height(322)
.hitTestBehavior(HitTestMode.Block)
}
}
状态管理
ArkTS提供了多维度的状态管理机制,在UI开发框架中,和UI相关联的数据,不仅可以在组件内使用,还可以在不同组件层级间传递,比如父子组件之间、爷孙组件之间,也可以是全局范围内的传递。另外,从数据的传递形式来看,可分为只读的单向传递和可变更的双向传递。开发者可以灵活的利用这些能力来实现数据和UI的联动。
页面级状态管理
装饰器 | 装饰内容 | 说明 |
@State | 基本数据类型,类,数组 | 修饰的状态数据被修改时会触发组件的build方法进行UI界面更新。 |
@Prop | 基本数据类型 | 修改后的状态数据用于在父组件和子组件之间建立单向数据依赖关系。修改父组件关联数据时,更新当前组件的UI。 |
@Link | 基本数据类型,类,数组 | 父子组件之间的双向数据绑定,父组件的内部状态数据作为数据源,任何一方所做的修改都会反映给另一方。 |
@Observed | 类 | @Observed应用于类,表示该类中的数据变更被UI页面管理。 |
@ObjectLink | 被@Observed所装饰类的对象 | 装饰的状态数据被修改时,在父组件或者其他兄弟组件内与它关联的状态数据所在的组件都会更新UI。 |
@Consume | 基本数据类型,类,数组 | @Consume装饰的变量在感知到@Provide装饰的变量更新后,会触发当前自定义组件的重新渲染。 |
@Provide | 基本数据类型,类,数组 | @Provide作为数据的提供方,可以更新其子孙节点的数据,并触发页面渲染。 |
应用级状态管理
AppStorage是整个UI应用程序状态的中心“数据库”,UI框架会针对应用程序创建单例AppStorage对象,并提供相应的装饰器和接口供应用程序使用。
-
- @StorageLink:@StorageLink(name)的原理类似于@Consume(name),不同的是,该给定名称的链接对象是从AppStorage中获得的,在UI组件和AppStorage之间建立双向绑定同步数据。
- @StorageProp:@StorageProp(name)将UI组件属性与AppStorage进行单向同步,AppStorage中值的更改会更新组件中的属性,但UI组件无法更改AppStorage中的属性值。
- AppStorage还提供用于业务逻辑实现的API,用于添加、读取、修改和删除应用程序的状态属性,此API所做的更改会导致修改的状态数据同步到UI组件上进行UI更新。
- PersistentStorage类提供了一些静态方法用来管理应用持久化数据,可以将特定标记的持久化数据链接到AppStorage中,并由AppStorage接口访问对应持久化数据,或者通过@StorageLink装饰器来访问对应key的变量。
- Environment是框架在应用程序启动时创建的单例对象,它为AppStorage提供了一系列应用程序需要的环境状态属性,这些属性描述了应用程序运行的设备环境。
渲染控制
- 条件渲染:可以通过条件渲染控制组件的显示隐藏。
Column() {
if (this.count < 0) {
Text('count is negative').fontSize(14)
} else if (this.count % 2 === 0) {
Text('count is even').fontSize(14)
} else {
Text('count is odd').fontSize(14)
}
}
- 循环渲染:渲染列表数据。
ForEach(
arr: any[],
itemGenerator: (item: any, index?: number) => void,
keyGenerator?: (item: any, index?: number) => string
)
- 数据懒加载:应用于list、gride等列表数据渲染,只渲染可是页面以及前后的item的预加载。
LazyForEach(
dataSource: IDataSource,
itemGenerator: (item: any) => void,
keyGenerator?: (item: any) => string
): void
动画
属性动画
属性动画只对写在animation前面的属性生效,且对组件构造器的属性不生效。
@State widthSize: number = 250;
@State heightSize: number = 100;
@State rotateAngle: number = 0;
@State flag: boolean = true;
// ...
Column({ space: this.space }) // 改变Column构造器中的space动画不生效
.onClick(() => {
if (this.flag) {
this.widthSize = 150;
this.heightSize = 60;
this.space = 20; // 改变this.space动画不生效
} else {
this.widthSize = 250;
this.heightSize = 100;;
this.space = 10; // 改变this.space动画不生效
}
this.flag = !this.flag;
})
.margin(30)
.width(this.widthSize) // 只有写在animation前面才生效
.height(this.heightSize) // 只有写在animation前面才生效
.animation({
duration: 2000,
curve: Curve.EaseOut,
iterations: 3,
playMode: PlayMode.Normal
})
显示动画
// xxx.ets
@Entry
@Component
struct AnimateToExample {
@State widthSize: number = 250
@State heightSize: number = 100
@State rotateAngle: number = 0
private flag: boolean = true
build() {
Column() {
Button('change size')
.width(this.widthSize)
.height(this.heightSize)
.margin(30)
.onClick(() => {
if (this.flag) {
// 建议使用this.getUIContext()?.animateTo()
animateTo({
duration: 2000,
curve: Curve.EaseOut,
iterations: 3,
playMode: PlayMode.Normal,
onFinish: () => {
console.info('play end')
}
}, () => {
this.widthSize = 150
this.heightSize = 60
})
} else {
// 建议使用this.getUIContext()?.animateTo()
animateTo({}, () => {
this.widthSize = 250
this.heightSize = 100
})
}
this.flag = !this.flag
})
Button('stop rotating')
.margin(50)
.rotate({ x: 0, y: 0, z: 1, angle: this.rotateAngle })
.onAppear(() => {
// 组件出现时开始做动画
// 建议使用this.getUIContext()?.animateTo()
animateTo({
duration: 1200,
curve: Curve.Friction,
delay: 500,
iterations: -1, // 设置-1表示动画无限循环
playMode: PlayMode.Alternate,
expectedFrameRateRange: {
min: 10,
max: 120,
expected: 60,
}
}, () => {
this.rotateAngle = 90
})
})
.onClick(() => {
// 建议使用this.getUIContext()?.animateTo()
animateTo({ duration: 0 }, () => {
// this.rotateAngle之前为90,在duration为0的动画中修改属性,可以停止该属性之前的动画,按新设置的属性显示
this.rotateAngle = 0
})
})
}.width('100%').margin({ top: 5 })
}
}
组件内转场动画 ------ transition
// xxx.ets
@Entry
@Component
struct TransitionEffectExample1 {
@State flag: boolean = true;
@State show: string = 'show';
build() {
Column() {
Button(this.show).width(80).height(30).margin(30)
.onClick(() => {
// 点击Button控制Image的显示和消失
if (this.flag) {
this.show = 'hide';
} else {
this.show = 'show';
}
this.flag = !this.flag;
})
if (this.flag) {
// Image的显示和消失配置为相同的过渡效果(出现和消失互为逆过程)
// 出现时从指定的透明度为0、绕z轴旋转180°的状态,变为默认的透明度为1、旋转角为0的状态,透明度与旋转动画时长都为2000ms
// 消失时从默认的透明度为1、旋转角为0的状态,变为指定的透明度为0、绕z轴旋转180°的状态,透明度与旋转动画时长都为2000ms
Image($r('app.media.testImg')).width(200).height(200)
.transition(TransitionEffect.OPACITY.animation({ duration: 2000, curve: Curve.Ease }).combine(
TransitionEffect.rotate({ z: 1, angle: 180 })
))
}
}.width('100%')
}
}
其他
关键帧动画、页面间转场、共享元素转场、组件内隐式转场、路径动画、粒子动画、显示动画立即下发
异步并发
Promise和async/await提供异步并发能力,是标准的JS异步语法。异步代码会被挂起并在之后继续执行,同一时间只有一段代码执行,适用于单次I/O任务的场景开发,例如一次网络请求、一次文件读写等操作。无需另外启动线程执行。
异步语法是一种编程语言的特性,允许程序在执行某些操作时不必等待其完成,而是可以继续执行其他操作。
Promise有三种状态:pending(进行中)、fulfilled(已完成)和rejected(已拒绝)。Promise对象创建后处于pending状态,并在异步操作完成后转换为fulfilled或rejected状态。
doPromise(): Promise<number> {
const promise: Promise<number> = new Promise((resolve: Function, reject: Function) => {
setTimeout(() => {
const randomNumber: number = Math.random();
if (randomNumber > 0.5) {
resolve(randomNumber);
} else {
reject(new Error('Random number is too small'));
}
}, 1000);
})
return promise
}
testPromise() {
let a = 1 + 2
let promise = this.doPromise()
promise.then((result: number) => {
console.info(`Random number is ${result}`);
}).catch((error: BusinessError) => {
console.error(error.message);
});
console.log(a)
}
对标flutter的事件队列机制
Promise -> Futrue
async/await -> async/await
多线程
存储、缓存
文件管理
user preference
沙箱概念等
数据库存储
网络请求
国际化
页面路由
开关
埋点
日志
任务管理
常量管理
事件中心
工程管理
依赖管理
打包
测试验收