ArkTS是HarmonyOS优选的主力应用开发语言。ArkTS围绕应用开发在[TypeScript](https://www.typescriptlang.org/)(简称TS)生态基础上做了进一步扩展,保持了TS的基本风格,同时通过规范定义强化开发期静态检查和分析,提升程序执行稳定性和性能。
ArkTS的一大特性是它专注于低运行时开销。ArkTS对TypeScript的动态类型特性施加了更严格的限制,以减少运行时开销,提高执行效率。通过取消动态类型特性,ArkTS代码能更有效地被运行前编译和优化,从而实现更快的应用启动和更低的功耗。
注意:从API version 10开始,ArkTS进一步通过规范强化静态检查和分析对比标准TS的差异可以参考:
当前,在UI开发框架中,ArkTS主要扩展了如下能力:
ArkTS定义了声明式UI描述、自定义组件和动态扩展UI元素的能力,再配合ArkUI开发框架中的系统组件及其相关的事件方法、属性方法等共同构成了UI开发的主体。
#d
ArkTS提供了多维度的状态管理机制。在UI开发框架中,与UI相关联的数据可以在组件内使用,也可以在不同组件层级间传递,比如父子组件之间、爷孙组件之间,还可以在应用全局范围内传递或跨设备传递。另外,从数据的传递形式来看,可分为只读的单向传递和可变更的双向传递。开发者可以灵活地利用这些能力来实现数据和UI的联动。
ArkTS提供了渲染控制的能力。条件渲染可根据应用的不同状态,渲染对应状态下的UI内容。循环渲染可从数据源中迭代获取数据,并在每次迭代过程中创建相应的组件。数据懒加载从数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。
范式?意思就是我们要统一遵守的UI写法
自定义组件部分和UI描述,系统组件,事件方法,属性方法我们也一般把他们成为ArkUI自定义变量不能与基础通用属性/事件名重复。
用于装饰类、结构、方法以及变量,并赋予其特殊的含义。如上述示例中@Entry、@Component和@State都是装饰器,
@Component:表示自定义组件,
@Entry:表示该自定义组件为入口组件,
@State:表示组件中的状态变量,状态变量变化会触发UI刷新。
以声明式的方式来描述UI的结构,例如build()方法中的代码块。
可复用的UI单元,可组合其他组件,如上述被@Component装饰的struct Hello。
ArkUI框架中默认内置的基础和容器组件,可直接被开发者调用,比如示例中的Column、Text、Divider、Button。
组件可以通过链式调用配置多项属性,如fontSize()、width()、height()、backgroundColor()等。
组件可以通过链式调用设置多个事件的响应逻辑,如跟随在Button后面的onClick()。
当然除了以上内容还有其他的一些装饰器和组件,我们后面会逐渐讲到
我们创建组件,不需要和TypeScript一样new一个对象,而是直接使用组件名称
创建组件一般我们有两种情况,一种是**有参**,一种是**无参**
//如果组件的接口定义没有包含必选构造参数,则组件后面的“()”不需要配置任何内容。
Column() {
}**如何快速甄别是否可以无参创建组件?**
**我们可以按住键盘ctrl,此时会出现一个可点击的效果,我们点击进去看看**

此时这里Column有一个类型,我们继续ctrl点击进入

这里我们就可以看到具体类型定义的interface结构了,我们可以看到这里面都是通过**?问号**也就是可选参数,没有必填的参数,也就意味着定义的时候,可以不传构造参数进去。
同理如果有没使用?问号的,也就是必传参数,也就出现咱们下面的有参创建组件

总结:如果源码中,接口定义的函数中,传入的参数都为可选参数,或者没有参数,那么定义创建组件使用的时候,就可以用无参创建
Image('https://xyz/test.jpg')同样我们进去看一下,为什么必须传参数才能创建?


在这里我们就可以看出,我们用ctrl鼠标移动到组件上,他就会提示上面的定义方式,我们可以快速看出,它的构造必须传一个src的参数,这也就是我们为什么使用image组件必须有参创建的原因
//函数类型接口,里面只有函数签名,不包含属性
interface ImageInterface {
//调用签名
(src: image.PixelMap | ResourceStr | DrawableDescriptor): ImageAttribute;
}上面看到有参无参创建方式,我们这里就要提一下**函数类型接口**和**调用签名**
**什么是调用签名?**
**调用签名是描述函数类型的一种语法,用于指定函数的参数类型和返回类型**
**当然一个接口里面可以包含一个或者多个调用签名**
**也就是上面interface里面的函数描述 (src: image.PixelMap | ResourceStr | DrawableDescriptor): ImageAttribute;**
**反之函数类型接口就是我们的接口定义啦,只不过里面包含的内容是函数,一个或者多个**
因为这里涉及到JavaScript的基础知识,形参和实参
//这里面就是相当于调用接口的函数,给函数传值叫做实参
Image('https://xyz/test.jpg')interface ImageInterface {
//这里的参数就是形参,
(src: image.PixelMap | ResourceStr | DrawableDescriptor): ImageAttribute;
}简单直观来说就是这样
function createUser(name, age, email) {
console.log(`Name: ${name}, Age: ${age}, Email: ${email}`);
}
createUser("Alice", 30, "alice@example.com");当我们函数定义需要**使用对象作为参数**的时候,需要指定参数名
function createUser({ name, age, email }) {
console.log(`Name: ${name}, Age: ${age}, Email: ${email}`);
}
createUser({ name: "Alice", age: 30, email: "alice@example.com" });同理如果我们给Column组件传入一个非必须的参数的时候,因为它定义的参数是一个对象
Column({space:10}){
}
属性方法以“.”链式调用的方式配置系统组件的样式和其他属性,建议每个属性方法单独写一行
**配置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)事件方法以“.”链式调用的方式配置系统组件支持的事件,建议每个事件方法单独写一行。
**使用箭头函数配置组件的事件方法。**
当然里面传进去的函数是一个匿名函数表达式,要求使用“**() => {...}”**
Button('Click me')
.onClick(() => {
this.myText = 'ArkUI';
})**如果不想使用匿名函数怎么办?**
**使用普通的成员函数:**需要bind this。
myClickHandler(): void {
this.counter += 2;
}
...
Button('add counter')
.onClick(this.myClickHandler.bind(this))**单独声明的箭头函数:**可以直接调用,不需要bind this。
fn = () => {
console.info(`counter: ${this.counter}`)
this.counter++
}
...
Button('add counter')
.onClick(this.fn)在ArkTS中,由于箭头函数this指向问题,**箭头函数中不允许使用this**,箭头函数内部的this是词法作用域,由上下文确定。匿名函数可能会有this指向不明确问题,在ArkTS中不允许使用。

如果组件支持子组件配置,则需在**尾随闭包"{...}"**中为组件添加子组件的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!');
})
}
}在编程中,尾随闭包是一种将函数的最后一个参数(如果是闭包)直接写在函数调用的圆括号外面的语法糖,使代码更简洁清晰。
最后我们可以通过组件文档,查看组件是否包含子组件
我们鼠标移动到组件上,就可以提示出“查阅API参考”的字样

\
在ArkUI中,**UI显示的内容均为组件**
**系统组件:**由框架直接提供的称为系统组件
**自定义组件:**由开发者定义的称为自定义组件。
**可组合:**允许开发者组合使用系统组件、及其属性和方法。
**可重用:**自定义组件可以被其他组件重用,并作为不同的实例在不同的父组件或容器中使用。
**数据驱动UI更新:**通过状态变量的改变,来驱动UI的刷新。
@Component
struct 组件名{
build(){}
}
@Component装饰器仅能装饰struct关键字声明的数据结构。struct被@Component装饰后具备组件化的能力,一个struct只能被一个@Component装饰。@Component可以接受一个可选的bool类型参数。
自定义组件基于struct实现,struct + 自定义组件名 + {...}的组合构成自定义组件,不能有继承关系。对于struct的实例化,可以省略new。
@Component
struct HelloPage {
build() {
Column(){
Button(){}
}
}
}注意:自定义组件名、类名、函数名不能和系统组件名相同。
build()函数用于定义自定义组件的声明式UI描述,自定义组件必须定义build()函数。并且struct中只能有一个build函数
@Component
struct MyComponent {
build() {
}
}@Entry装饰的自定义组件将作为UI页面的入口。在单个UI页面中,最多可以使用@Entry装饰一个自定义组件。
错误示例:一个UI页面中,只能有一个@Entry

/
命名路由跳转选项。
此配置项主要为了共享包har或者hsp的页面跳转,后面router会详细讲解
@Entry({ routeName : 'myPage' })
@Component
struct MyComponent {
}@Reusable装饰的自定义组件具备可复用能力,主要用于大数据量列表加载时,主要提高列表页面的加载速度和响应速度,后面会详细讲解
/
@Reusable
@Component
struct MyComponent {
}自定义组件可以包含成员变量,成员变量具有以下约束:
自定义组件的成员变量为私有的,且不建议声明成静态变量。
自定义组件的成员变量本地初始化有些是可选的,有些是必选的。具体是否需要本地初始化,是否需要从父组件通过参数传递初始化子组件的成员变量
@Component
struct MyComponent {
private countDownFrom: number = 0;
private color: Color = Color.Blue;
build() {
}
}build()函数中只能有一个根节点
@Component
struct ChildComponent {
build() {
// 根节点唯一且必要
Image('test.jpg')
}
}**@Entry修饰的组件和@Component修饰的组件中,对于根节点的要求不一样**
@Entry中,**根节点必须是容器组件**( 主要用于**布局和组织子组件** ,就是布局组件和有子组件的组件例如Button,Text等)
@Entry
@Component
struct MyComponent {
build() {
// 根节点唯一且必要,必须为容器组件
Row() {
ChildComponent()
}
}
}@Component中,**根节点可以是非容器组件,**也就是可以是任何的组件,比如Image组件(既不是布局组件,也不能有子组件)
@Component
struct ChildComponent {
build() {
// 根节点唯一且必要,可为非容器组件
Image('test.jpg')
}
}不允许声明本地变量,反例如下。
build() {
// 反例:不允许声明本地变量
let a: number = 1;
}不允许在UI描述里直接使用console.info,但允许在方法或者函数里使用,反例如下。
build() {
// 反例:不允许console.info
console.info('print debug log');
}不允许创建本地的作用域,反例如下。
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())
}
}
}不允许使用switch语法,如果需要使用条件判断,请使用if。反例如下。
build() {
Column() {
// 反例:不允许使用switch语法
switch (expression) {
case 1:
Text('...')
break;
case 2:
Image('...')
break;
default:
Text('...')
break;
}
}
}不允许使用表达式,反例如下。
build() {
Column() {
// 反例:不允许使用表达式
(this.aVar > 10) ? Text('...') : Image('...')
}
}不允许直接改变状态变量,反例如下。
@Component
struct CompA {
@State col1: Color = Color.Yellow;
@State col2: Color = Color.Green;
@State count: number = 1;
build() {
Column() {
// 应避免直接在Text组件内改变count的值
Text(`${this.count++}`)
.width(50)
.height(50)
.fontColor(this.col1)
.onClick(() => {
this.col2 = Color.Red;
})
Button("change col1").onClick(() =>{
this.col1 = Color.Pink;
})
}
.backgroundColor(this.col2)
}
}全量更新: ArkUI可能会陷入一个无限的重渲染的循环里,因为Text组件的每一次渲染都会改变应用的状态,就会再引起下一轮渲染的开启。 当 this.col2 更改时,都会执行整个build构建函数,因此,Text(
自定义组件通过“.”链式调用的形式设置通用样式。
注意:自定义组件定义后,尤其是父组件引用子组件的时候,只能使用通用的样式
@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
@Component
struct Father {
build() {
Column(){
Button(`Hello World`)
.padding({ top: '0.00vp', right: '16.00vp', bottom: '0.00vp', left: '30.00vp' })
Child()
}
}
}
@Component
struct Child {
build() {
Row() {
Text("子组件")
}
}
}子组件需要通过export或者export default导出,父组件需要import导入子组件才可使用
Father.ets
import { Child } from './Child'
@Entry
@Component
struct Father {
build() {
Column(){
Button(`Hello World`)
.padding({ top: '0.00vp', right: '16.00vp', bottom: '0.00vp', left: '30.00vp' })
Child()
}
}
}
Child.ets
@Component
export struct Child {
build() {
Row() {
Text("子组件")
}
}
}即被@Entry装饰的组件生命周期,提供以下生命周期接口:
页面每次显示时触发一次,包括路由过程、应用进入前台等场景。
一般用于数据的重新刷新
初始状态的恢复
动画启动
统计和分析数据
事件的一些监听
页面每次隐藏时触发一次,包括路由过程、应用进入后台等场景。
主要用来保存数据,释放资源等
onBackPress:当用户点击返回按钮时触发。
即一般用@Component装饰的自定义组件的生命周期,提供以下生命周期接口:
组件即将出现时回调该接口,具体时机为在创建自定义组件的新实例后,在执行其build()函数之前执行。
一般用于数据加载,初始化数据
设置页面的状态
预加载资源,比如加载一些图片,视频等
注册时间监听,比如键盘,网络状态变化等
更新数据内容,或者动态的一些数据比如计数器,时间等
组件build()函数执行完成之后回调该接口,不建议在onDidBuild函数中更改状态变量、使用animateTo等功能,这可能会导致不稳定的UI表现。
绑定事件、设置初始状态、调用其他方法
aboutToDisappear函数在自定义组件析构销毁之前执行。不允许在aboutToDisappear函数中改变状态变量,特别是@Link变量的修改可能会导致应用程序行为不稳定。
主要用于清理一些资源
示例:
// Index.ets
import router from '@ohos.router';
@Entry
@Component
struct MyComponent {
@State showChild: boolean = true;
@State btnColor:string = "#FF007DFF"
// 只有被@Entry装饰的组件才可以调用页面的生命周期
onPageShow() {
console.info('Index onPageShow');
}
// 只有被@Entry装饰的组件才可以调用页面的生命周期
onPageHide() {
console.info('Index onPageHide');
}
// 只有被@Entry装饰的组件才可以调用页面的生命周期
onBackPress() {
console.info('Index onBackPress');
this.btnColor ="#FFEE0606"
return true // 返回true表示页面自己处理返回逻辑,不进行页面路由;返回false表示使用默认的路由返回逻辑,不设置返回值按照false处理
}
// 组件生命周期
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('delete Child')
.margin(20)
.backgroundColor(this.btnColor)
.onClick(() => {
this.showChild = false;
})
// push到page页面,执行onPageHide
Button('push to next page')
.onClick(() => {
router.pushUrl({ url: 'pages/page' });
})
}
}
}
@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).margin(20).onClick(() => {
this.title = 'Hello ArkUI';
})
}
}如果需要通过测算的方式布局自定义组件内子组件的位置,建议使用以下接口
**onMeasureSize:**组件每次布局时触发,计算子组件的尺寸,其执行时间先于onPlaceChildren。
**onPlaceChildren:**组件每次布局时触发,设置子组件的起始位置。
// xxx.ets
@Entry
@Component
struct Index {
build() {
Column() {
CustomLayout({ builder: ColumnChildren })
}
}
}
// 通过builder的方式传递多个组件,作为自定义组件的一级子组件(即不包含容器组件,如Column)
@Builder
function ColumnChildren() {
ForEach([1, 2, 3], (index: number) => { // 暂不支持lazyForEach的写法
Text('S' + index)
.fontSize(30)
.width(100)
.height(100)
.borderWidth(2)
.offset({ x: 10, y: 20 })
})
}
@Component
struct CustomLayout {
@Builder
doNothingBuilder() {
};
@BuilderParam builder: () => void = this.doNothingBuilder;
@State startSize: number = 100;
result: SizeResult = {
width: 0,
height: 0
};
// 第一步:计算各子组件的大小
onMeasureSize(selfLayoutInfo: GeometryInfo, children: Array<Measurable>, constraint: ConstraintSizeOptions) {
let size = 100;
children.forEach((child) => {
let result: MeasureResult = child.measure({ minHeight: size, minWidth: size, maxWidth: size, maxHeight: size })
size += result.width / 2;
})
this.result.width = 100;
this.result.height = 400;
return this.result;
}
// 第二步:放置各子组件的位置
onPlaceChildren(selfLayoutInfo: GeometryInfo, children: Array<Layoutable>, constraint: ConstraintSizeOptions) {
let startPos = 300;
children.forEach((child) => {
let pos = startPos - child.measureResult.height;
child.layout({ x: pos, y: pos })
})
}
build() {
this.builder()
}
}后续讲解
自定义组件处于非激活状态时,状态变量将不响应更新,即@Watch不会调用,状态变量关联的节点不会刷新。通过freezeWhenInactive属性来决定是否使用冻结功能,不传参数时默认不使用。支持的场景有:页面路由,TabContent,LazyforEach,Navigation。
(路由中讲解)