下面的内容来自尚硅谷,版权属于尚硅谷,这里只做参考。尚硅谷课程链接:https://www.bilibili.com/video/BV1Ti4y1s79B?p=5
2.1 概述
HarmonyOS 应用的主要开发语言是 ArkTS,它由 TypeScript(简称TS)扩展而来,在继承TypeScript语法的基础上进行了一系列优化,使开发者能够以更简洁、更自然的方式开发应用。值得注意的是,TypeScript 本身也是由另一门语言 JavaScript 扩展而来。因此三者的关系如下图所示
2.2 TypeScript 快速入门
2.2.1 运行环境说明
2.2.1.1 线上Playground
TypeScript提供了一个线上的 Playground 供练习使用,地址为https://www.typescriptlang.org/zh/play。
2.2.1.2 本地运行环境
除去线上的运行环境,我们也可以在本地搭建一个 TS 的运行环境。
- 安装 VSCode 编辑器
VSCode是一款轻量级、开源且功能丰富的集成开发环境(IDE),支持多种编程语言,具有强大的插件系统。下载地址为:https://code.visualstudio.com/ - 安装Code Runner 插件
Code Runner是一款在VSCode中使用的插件,它提供了简便的代码执行功能,支持多种编程语言,使开发者能够快速运行和调试代码片段。
- 安装ts-node
ts-node是一个 TypeScript 的运行环境,它允许我们直接运行 TypeScript 代码。ts-node的安装和运行依赖于Node.js环境,因此在安装ts-node之前,我们需要准备好Node.js环境。
准备Node.js环境需要完成以下两步操作
(1)安装Node.js
由于前边在部署DevEco Studio时,已经下载并安装了Node.js,因此这一步可以略过。
(2)配置环境变量
为了方便在终端执行Node.js相关的命令,我们需要将Node.js的安装目录加入到Path环境变量下,具体操作如下
首先在DevEco Studio的设置界面查看Node.js的安装目录
然后打开环境变量配置面板,按下Win+R,唤起运行窗口,之后运行命令sysdm.cpl
之后点击高级选项卡,并点击环境变量
然后在系统变量中选中Path,并点击编辑
之后点击新建,并填入Node.js的安装目录,完成后点击确定。
在配置完Node.js环境后,便可在终端执行以下命令来安装ts-node了。
npm install -g ts-node
注:完成后需要重新启动VSCode,另其重新加载环境变量和相关依赖。
- 编写程序并运行
在完成上述环境的准备后,就可以编写Typescript程序并运行了,具体操作如下
首先在合适的位置创建一个工程目录,例如D:\workspace\hello-ts,然后使用VSCode打开目录
之后创建Typescript文件,点击New File
注意,文件的后缀为.ts
之后就可以编写Typescript代码并运行了
2.2.2 声明
2.2.2.1 变量生命
2.2.2.2 常量声明
let用于声明变量,而const用于声明常量,两者的区别是变量在赋值后可以修改,而常量在赋值后便不能再修改。
const b:number = 200;
2.2.2.3 类型推断
如果一个变量或常量的声明包含了初始值,TS 便可以根据初始值进行类型推断,此时我们就可以不显式指定其类型,例如
let c = 60;
console.log(typeof c); //number
2.2.3 常用数据类型
2.2.3.1 number
number表示数字,包括整数和浮点数,例如: 100、-33 、2.5、-3.9
let a :number = 100
let b :number = -33
let c :number = 2.5
let d :number = -3.9
2.2.3.2 string
string表示字符串,例如: 你好、hello
let a:string = '你好'
let b:string = "hello"
2.2.3.3 boolean
boolean表示布尔值,可选值为:true、false
let isOpen:boolean = true
let isDone:boolean = false
2.2.3.4 数组
数组类型定义由两部分组成,元素类型[],例如number[]表示数字数组,string[]表示字符串数组,数组类型的变量可由数组字面量——[item1,item2,item3]进行初始化。
let a: number[] = []
let b: string[] = ['你好', 'hello']
2.2.3.5 对象
在TS中,对象(object)是一种一系列由属性名称和属性值组成的数据结构,例如姓名:‘张三’, 年龄:10, 性别:‘男’。对象类型的声明需要包含所有属性的名称及类型,例如{name: string, age: number, gender: string},对象类型的变量可以通过对象字面量——{name:‘张三’, age:10, gender:‘男’}进行初始化。
let person: {name:string, age:number, gender:string} = {name:'张三', age:10, gender:'男'};
2.2.4 函数
2.2.4.1 函数声明语法
声明函数的基础语法如下
2.2.4.2 参数详解
2.2.4.2.1 特殊语法
- 可选参数
可选参数通过参数名后的?
进行标识,如以下案例中的gender?
参数。
function getPersonInfo(name: string, age: number, gender?: string): string {
if (gender === undefined) {
gender = '未知'
}
return `name:${name},age:${age},gender:${gender}`;
}
let p1 = getPersonInfo('zhagnsan', 10, '男')
let p2 = getPersonInfo('lisi', 15);
console.log(p1);
console.log(p2);
注:调用函数时,未传递可选参数,则该参数的值为undefined。
- 默认参数
可在函数的参数列表为参数指定默认值,如以下案例中的gender: string='未知’参数。
function getPersonInfo(name: string, age: number, gender: string='未知'): string {
return `name:${name},age:${age},gender:${gender}`;
}
let p1 = getPersonInfo('zhagnsan', 10, '男')
let p2 = getPersonInfo('lisi', 15);
console.log(p1);
console.log(p2);
2.2.4.2.2 特殊类型
- 联合类型
一个函数可能用于处理不同类型的值,这种情况可以使用联合类型,例如以下案例中的message: number | string
function printNumberOrString(message: number | string) {
console.log(message)
}
printNumberOrString('a')
printNumberOrString(1)
- 任意类型
若函数需要处理任意类型的值,则可以使用any类型,例如以下案例中的message: any
function print(message:any) {
console.log(message)
}
print('a')
print(1)
print(true)
2.2.4.3 返回值详解
2.2.4.3.1 特殊类型
若函数没有返回值,则可以使用void作为返回值类型,其含义为空。
function test(): void {
console.log('hello');
}
2.2.4.3.2 类型推断
函数的返回值类型可根据函数内容推断出来,因此可以省略不写。
function test() {
console.log('hello');
}
function sum(a: number, b: number) {
return a + b;
}
2.2.4.4 函数声明特殊用法
- 匿名函数
匿名函数的语法结构简洁,特别适用于简单且仅需一次性使用的场景。
let numbers: number[] = [1, 2, 3, 4, 5]
numbers.forEach(function (number) {
console.log(number);
})
注意:匿名函数能够根据上下文推断出参数类型,因此参数类型可以省略。
- 箭头函数
匿名函数的语法还可以进一步的简化,只保留参数列表和函数体两个核心部分,两者用=>符号连接。
let numbers: number[] = [1, 2, 3, 4, 5]
numbers.forEach((num) => { console.log(num) })
2.2.5 类(class)
2.2.5.1 概述
类(class)是面向对象编程语言中的一个重要概念。
面向对象编程(Object-Oriented Programming,简称OOP)是一种编程范式,其核心理念在于将程序中的数据与操作数据的方法有机地组织成对象,从而使程序结构更加模块化和易于理解。通过对象之间的协同合作,实现更为复杂的程序功能。
类(class)是对象的蓝图或模板,它定义了对象的属性(数据)和行为(方法)。通过类可以创建多个具有相似结构和行为的对象。例如定义一个 Person
类,其对象可以有 张三
、李四
等等。
2.2.5.2 语法说明
2.2.5.2.1 类的定义
定义类的语法如下图所示
代码如下:
class Person {
id: number;
name: string;
age: number = 18;
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
introduce(): string {
return `hello,I am ${this.name},and I am ${this.age} years old`
}
}
2.2.5.2.2 对象创建
- 语法
创建对象的关键字为new,具体语法如下
let person = new Person(1,'zhangsan');
- 对象属性的访问
console.log(person.name); //读
person.name = 'lisi'; //写
console.log(person.name);
- 对象方法的调用
对象创建后,便可通过对象调用类中声明的方法,如下
let intro = person.introduce();
console.log(intro);
2.2.5.2.3 静态成员
Typescript 中的类中可以包含静态成员(静态属性和静态方法),静态成员隶属于类本身,而不属于某个对象实例。静态成员通用用于定义一些常量,或者工具方法。
- 声明静态成员
定义静态成员需要使用static
关键字。
class Constants{
static count:number=1;
}
class Utils{
static toLowerCase(str:string){
return str.toLowerCase();
}
}
console.log(Constants.count);
console.log(Utils.toLowerCase('Hello World'));
- 使用静态成员
静态成员无需通过对象实例访问,直接通过类本身访问即可。
console.log(Constants.count);
console.log(Utils.toLowerCase('Hello World'));
2.2.5.3 继承
继承是面向对象编程中的重要机制,允许一个类(子类或派生类)继承另一个类(父类或基类)的属性和方法。子类可以直接使用父类的特性,并根据需要添加新的特性或覆盖现有的特性。这种机制赋予面向对象程序良好的扩展性。
下面通过一个例子演示继承的特性
class Student extends Person {
classNumber: string;
constructor(id: number, name: string, classNumber: string) {
super(id, name);
this.classNumber = classNumber;
}
introduce(): string {
return super.introduce()+`, and I am a student`;
}
}
let student = new Student(1,'xiaoming','三年二班');
console.log(student.introduce());
注意:
- 类的继承需要使用关键字extends
- 子类构造器中需使用super()调用父类构造器对继承自父类的属性进行初始化。
- 在子类中可以使用this关键字访问继承自父类的属性和方法。
- 在子类中可以使用super关键字访问父类定义的方法。
2.2.5.4 访问修饰符
访问修饰符(Access Modifiers)用于控制类成员(属性、方法等)的可访问性。TypeScript提供了三种访问修饰符,分别是private
、protected
和public
。
class Person {
private id: number;
protected name: string;
public age: number;
constructor(id: number, name: string, age: number) {
this.id = id;
this.name = name;
this.age = age;
}
}
class Student extends Person {
}
说明:
- private 修饰的属性或方法是私有的,只能在声明它的类中的被访问。
- protected 修饰的属性或方法是受保护的,只能在声明它的类和其子类中被访问。
- public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的。
2.2.6 接口(interface)
2.2.6.1 概述
接口(interface)是面向对象编程中的另一个重要概念。接口通常会作为一种契约或规范让类(class)去遵守,确保类实现某些特定的行为或功能。
2.2.6.2 语法说明
- 接口定义
接口使用interface
关键字定义,通常情况下,接口中只会包含属性和方法的声明,而不包含具体的实现细节,具体的细节由其实现类完成。
interface Person {
id: number;
name: string;
age: number;
introduce(): void;
}
- 接口实现
接口的实现需要用到implements
关键字,实现类中,需要包含接口属性的赋值逻辑,以及接口方法的实现逻辑。
class Student implements Person {
id: number;
name: string;
age: number;
constructor(id: number, name: string, age: number) {
this.id = id;
this.name = name;
this.age = age;
}
introduce(): void {
console.log('Hello,I am a student');
}
}
2.2.6.3 多态
多态是面相对象编程中的一个重要概念,它可以使同一类型的对象具有不同的行为。下面我们通过一个具体的案例来体会多态这一概念
首先,再创建一个 Person
接口的实现类 Teacher
,如下
class Teacher implements Person {
id: number;
name: string;
age: number;
constructor(id: number, name: string, age: number) {
this.id = id;
this.name = name;
this.age = age;
}
introduce(): void {
console.log('Hello,I am a teacher');
}
}
然后分别创建一个 Student
对象和一个 Teacher
对象,注意两个对象的类型均可以设置Person,如下
let p1: Person = new Student(1, 'zhangsan', 17);
let p2: Person = new Teacher(2, 'lisi', 35);
最后分别调用 p1
和 p2
的 introduce()
方法,你会发现,同样是 Person
类型的两个对象,调用同一个 introduce()
方法时,表现出了不同的行为,这就是多态。
p1.introduce();//Hello,I am a student
p2.introduce();//Hello,I am a teacher
2.2.6.4 接口的作用
在传统的面向对象编程的场景中,接口主要用于设计和组织代码,使代码更加容易扩展和维护。下面举例说明。
假如现在需要实现一个订单支付系统,按照面向对象编程的习惯,首先需要定义一个订单类(Order),如下
class Order {
totalAmount: number;
constructor(totalAmount: number) {
this.totalAmount = totalAmount;
}
pay() {
console.log(`AliPay:${this.totalAmount}`);
}
}
很容易预想到,这个系统将来可能需要支持其他的支付方式,为了方便代码支持新的支付方式,我们可以对代码进行如下改造。
首先定义一个支付策略的接口,接口中声明一个 pay
方法,用来规范实现类必须实现支付逻辑。
interface PaymentStrategy {
pay(amount: number): void;
}
然后在订单类中增加一个 PaymentStrategy
的属性,并且在订单类中的 pay
方法中调用 PaymentStrategy
的 pay
方法,如下
class Order {
totalAmount: number;
paymentStrategy: PaymentStrategy;
constructor(totalAmount: number, paymentStrategy: PaymentStrategy) {
this.totalAmount = totalAmount;
this.paymentStrategy = paymentStrategy;
}
pay() {
this.paymentStrategy.pay(this.totalAmount);
}
}
这样改造完之后,就可以很容易的在不改变现有代码的情况下,支持新的支付方式了。
比如现在需要支持 AliPay
,那我们就可以创建 AliPay
这个类(class)并实现(implement)PaymentStrategy
这个接口,如下
class AliPay implements PaymentStrategy {
pay(amount: number): void {
console.log(`AliPay:${amount}`);
}
}
这样一来,之后创建的订单就可以使用 AliPay
这个支付方式了。
let order = new Order(1000,new AliPay());
order.pay();
2.2.6.5 TS中的接口的特殊性
TypeScript 中的接口是一个非常灵活的概念,除了用作类的规范之外,也常用于直接描述对象的类型,例如,现有一个变量的定义如下
let person: {name:string, age:number, gender:string} = {name:'张三', age:10, gender:'男'};
可以看到变量的值为一个一般对象,变量的类型为 {name:string, age:number, gender:string}
,此时就可以声明一个接口来描述该对象的类型,如下
interface Person {
name: string;
age: number;
gender: string;
}
let person: Person = {name:'张三', age:10, gender:'男'};
2.2.7 枚举
2.2.7.1 概述
枚举(Enumeration)是编程语言中常见的一种数据类型,其主要功能是定义一组有限的选项,例如,方向(上、下、左、右)或季节(春、夏、秋、冬)等概念都可以使用枚举类型定义。
2.2.7.2 语法说明
- 枚举定义
枚举的定义需使用enum
关键字,如下
enum Season {
SPRING,
SUMMER,
AUTUMN,
WINTER
}
- 枚举使用
枚举的使用记住两个原则即可
○ 枚举值的访问
像访问对象属性一样访问枚举值,例如Season.SPRING
○ 枚举值的类型
枚举值的类型为enum
的名称,例如Season.SPRING
和Season.SUMMER
等值的类型都是Season
let spring:Season = Season.SPRING;
- 使用场景
现需要编写一个函数move
,其功能是根据输入的方向(上、下、左、右)进行移动,此时就可以先使用枚举定义好所有可能的输入选项,如下
enum Direction {
UP,
BOTTOM,
LEFT,
RIGHT
}
move
函数的实现如下
function move(direction: Direction) {
if(direction===Direction.UP){
console.log('向上移动');
}else if(direction===Direction.BOTTOM){
console.log('向下移动');
}else if(direction===Direction.LEFT){
console.log('向左移动');
}else{
console.log('向右移动');
}
}
move(Direction.UP);
2.2.7.3 赋值
在TypeScript 中,枚举实际上是一个对象,而每个枚举值都是该对象的一个属性,并且每个属性都有具体的值,属性值只支持两种类型——数字或字符串。
默认情况下,每个属性的值都是数字,并且从 0 开始递增,例如上述案例中的 Direction
枚举中,Direction.UP
的值为0,Direction.BOTTOM
的值为1,依次类推,具体如下
console.log(Direction.UP) //0
console.log(Direction.BOTTOM) //1
console.log(Direction.LEFT) //2
console.log(Direction.RIGHT) //3
除了使用默认的数字作为属性的值,我们还能手动为每个属性赋值,例如
enum Direction {
UP = 1,
BOTTOM = 2,
LEFT = 3,
RIGHT = 4
}
console.log(Direction.UP) //1
console.log(Direction.BOTTOM) //2
console.log(Direction.LEFT) //3
console.log(Direction.RIGHT) //4
再例如
enum Direction {
UP = 'up',
BOTTOM = 'bottom',
LEFT = 'left',
RIGHT = 'right'
}
console.log(Direction.UP) //up
console.log(Direction.BOTTOM) //bottom
console.log(Direction.LEFT) //left
console.log(Direction.RIGHT) //right
通过为枚举属性赋值,可以赋予枚举属性一些更有意义的信息,例如以下枚举
enum Color {
Red = 0xFF0000,
Green = 0x00FF00,
Blue = 0x0000FF
}
enum FontSize {
Small = 12,
Medium = 16,
Large = 20,
ExtraLarge = 24
}
2.2.8 模块化
2.2.8.1 概述
模块化是指将复杂的程序拆解为多个独立的文件单元,每个文件被称为一个模块。在 TypeScript 中,默认情况下,每个模块都拥有自己的作用域,这意味着在一个模块中声明的任何内容(如变量、函数、类等)在该模块外部是不可见的。为了在一个模块中使用其他模块的内容,必须对这些内容进行导入、导出。
2.2.8.2 语法说明
- 导出
导出须使用export
关键字,语法如下
export function hello() {
console.log('hello module A');
}
export const str = 'hello world';
const num = 1;
- 导入
import { hello, str } from './moduleA';
hello();
console.log(str);
2.2.8.3 避免命名冲突
若多个模块中具有命名相同的变量、函数等内容,将这些内容导入到同一模块下就会出现命名冲突。例如,在上述案例的基础上,又增加了一个 moduleC,内容如下
export function hello() {
console.log('hello module C');
}
export const str = 'module C';
moduleB 同时引入 moduleA 和 moduleC 的内容,如下,显然就会出命名冲突
import { hello, str } from "./moduleA";
import { hello, str } from "./moduleC";
hello() //?
console.log(str); //?
有多种方式可以用来解决命名冲突,下面逐一介绍
- 导入重命名
语法如下:
import { hello as helloFromA, str as strFromA } from "./moduleA";
import { hello as helloFromC, str as strFromC } from "./moduleC";
helloFromA();
console.log(strFromA);
helloFromC();
console.log(strFromC);
- 创建模块对象
上述导入重命名的方式能够很好的解决命名冲突的问题,但是当冲突内容较多时,这种写法会比较冗长。除了导入重命名外,还可以将某个模块的内容统一导入到一个模块对象上,这样就能简洁有效的解决命名冲突的问题了,具体语法如下
import * as A from "./moduleA";
import * as C from "./moduleC";
A.hello();
console.log(A.str);
C.hello();
console.log(C.str);
2.2.8.4 默认导入导出
除了上述导入导出的语法之外,还有一种语法,叫做默认导入导出,这种语法相对简洁一些。
- 默认导出
默认导出允许一个模块指定一个(最多一个)默认的导出项,语法如下
export default function hello(){
console.log('moduleA');
}
- 默认导入
由于每个模块最多有一个默认导出,因此默认导入无需关注导入项的原始名称,并且无需使用{}。
import helloFromA from "./moduleA";
由于默认导入时无需关注导入项的名称,所以默认导出支持匿名内容,比如匿名函数,语法如下
export default function () {
console.log('moduleB');
}
2.3 ArkTS 快速入门
ArkTS 在继承了Typescript语法的基础上,主要扩展了声明式UI开发相关的能力。
2.3.1 声明式UI
声明式UI是一种编写用户界面的范式。下面通过一个具体案例来学习这种开发范式,假如现在要实现如下界面
2.3.1.1 定义界面状态
按照声明式UI的开发范式,首先需要分析和定义页面的各种状态,并声明相应的状态变量用于表示不同的状态。
当前案例中,界面共有两个状态,分别是开灯和关灯状态,所以我们可以使用一个boolean类型的变量来表示这两个状态,true表示开灯,false表示关灯。如下:
@State isOn: boolean = false;
说明:@State用于声明该变量为状态变量。
2.3.1.2 描述界面显示效果
在分析完界面状态后,我们需要准确的描述界面在不同状态下的显示效果。
在当前案例中,具体逻辑如下图所示
2.3.1.3 改变状态
在明确了界面在不同状态下的显示效果后,我们只需修改状态变量的值,就能触发界面的更新。
在当前案例中,若我们将isOn的值改为true,那么界面上就会显示开灯的图片,否则就会显示关灯的图片。
为了实现点击按钮开/关灯的效果,我们可以为按钮绑定点击事件:
- 当用户点击开灯按钮时,我们就将isOn的值改为true。
- 当用于点击关灯按钮时,我们就将isOn的值改为false。
2.3.1.4 总结
以上就是声明式UI开发范式的大致流程,下面为大家总结一下声明式UI的核心思想
- 声明式描述
开发者只需描述在界面在不同状态下要呈现的最终效果,而无需关注界面变化的具体过程。
- 状态数据驱动界面更新
开发者只需修改状态变量的值,界面就会自动更新。
2.3.2 组件化
在鸿蒙开发中,组件是构成界面的最小单元,我们所看到的界面,都是由众多组件组合而成的,所以编写界面其实就是组合组件的过程,ArkTS提供了很多的内置组件,例如:Text
、Button
、Image
等等;并且ArkTS还支持自定义组件,让开发者可根据具体需求自定义组件中的内容。
2.3.3 入门案例
2.3.3.1 案例效果
案例的最终效果如下图所示
2.3.3.2 完整代码
@Entry
@Component
struct LightPage {
@State isOn: boolean = false;
build() {
Column({ space: 20 }) {
if (this.isOn) {
Image('pages/helloworld/light/solution/images/img_light.png')
.height(300)
.width(300)
.borderRadius(20)
} else {
Image('pages/helloworld/light/solution/images/img_dark.png')
.height(300)
.width(300)
.borderRadius(20)
}
Row({ space: 50 }) {
Button('关灯')
.onClick(() => {
this.isOn = false
})
Button('开灯')
.onClick(() => {
this.isOn = true;
})
}
}
.height('100%')
.width('100%')
.justifyContent(FlexAlign.Center)
}
}
2.3.4 语法说明
2.3.4.1 声明组件
下面通过一个相对简单的案例来系统的学习 ArkTS 声明组件的语法,案例的最终效果如下
声明组件的完整语法如下图所示
各部分语法说明如下
- 组件参数
如果组件的定义包含参数,可在组件名称后面的()中配置相应参数。各组件支持的参数,可查看 API 文档,查看方式如下
○ 首先将鼠标在相应组件悬停
○ 点击Show in API Reference,就会弹出 API 文档
- 子组件
如果组件支持子组件配置,可在()
后的{}
中添加子组件,若不支持子组件,则不需要写{}
。 - 属性方法
属性方法用于配置组件的样式和其他属性,可以在组件声明的末尾进行链式调用。各组件支持的属性可查看 API 文档,除去每个组件的专有属性,还有各组件都能配置的通用属性,通用属性也可通过 API 文档查看。
- 事件方法
事件方法用于为组件绑定交互事件,可以在组件声明的末尾进行链式调用。各组件的支持的事件可查看 API 文档,除去每个组件的专有事件,还有各组件都支持的通用事件,通用事件也可通过 API 文档查看。
2.3.4.2 自定义组件
除去系统预置的组件外,ArkTS 还支持自定义组件。使用自定义组件,可使代码的结构更加清晰,并且能提高代码的复用性。
2.3.4.2.1 语法说明
自定义组件的语法如下图所示
各部分语法说明如下:
struct
关键字
struct
是ArkTS新增的用于自定义组件或者自定义弹窗的关键字。其声明的数据结构和TS中的类十分相似,可包含属性和方法。build
方法
build()
方法用于声明自定义组件的UI结构。- 组件属性
组件属性可用作自定义组件的参数,使得自定义组件更为通用。 @Compnent
装饰器
@Component
装饰器用于装饰struct
关键字声明的数据结构。struct
被@Component
装饰后才具备组件化的能力。
注: 装饰器是Typescript中的一种特殊语法,常用于装饰类、方法、属性,用于修改或扩展其原有的行为。
在学完自定义组件的语法之后,我们会发现前文案例中的每个页面实际上都是一个自定义组件。但是和自定义组件的语法相比,前边的每个案例还会多出一个@Entry
装饰器,那@Entry
的作用又是啥呢?
在鸿蒙应用中,每个页面都是由一些列组件组合而成的,并且这些组件都是逐层嵌套的,因此这些组件最终形成了一个组件树的结构,如下图所示
我们前边所编写的每个页面就相当于是组件树的根节点,而@Entry
装饰器的作用就是标识该组件为组件树的根节点,也就是一个页面的入口组件。
2.3.4.2.2 案例实操
现在需要对前文的开/关灯的案例做出如下改造,由于两个按钮的结构十分相似,所以可考虑自定义一个按钮组件,然后进行复用。
@Entry
@Component
struct HelloWorldPage {
@State isOn: boolean = false;
build() {
Column({ space: 20 }) {
if (this.isOn) {
Image('pages/helloworld/custom/solution/images/img_light.png')
.height(300)
.width(300)
.borderRadius(20)
} else {
Image('pages/helloworld/custom/solution/images/img_dark.png')
.height(300)
.width(300)
.borderRadius(20)
}
Row({ space: 50 }) {
SwitchButton({ color: Color.Red })
.onClick(() => {
this.isOn = false
})
SwitchButton({ color: Color.Green })
.onClick(() => {
this.isOn = true;
})
}
}
.height('100%')
.width('100%')
.justifyContent(FlexAlign.Center)
}
}
@Component
struct SwitchButton {
color: Color = Color.Blue;
build() {
Button({ type: ButtonType.Circle }) {
Image('pages/helloworld/custom/solution/images/icon_switch.png')
.width(30)
.width(30)
}.width(50)
.height(50)
.backgroundColor(this.color)
}
}
2.3.4.3 渲染控制
2.3.4.3.1 条件渲染
概述
条件渲染可根据应用的不同状态渲染不同的UI界面,例如前文的开/关灯案例,以及以下的播放/暂停案例,均可使用条件渲染实现。
语法说明
条件渲染的语法如下
if (...){
//UI描述
}else if (...){
//UI描述
}else{
//UI描述
}
2.3.4.3.1 循环渲染
概述
循环渲染可使用ForEach语句基于一个数组来快速渲染一个组件列表,例如以下案例中的选项列表就可通过循环渲染实现。
代码如下:
@Entry
@Component
struct FruitPage {
@State options: string[] = ["苹果", "桃子", "香蕉", "橘子"];
@State answer: string = "____?";
build() {
Column({ space: 20 }) {
Row() {
Text("你最喜欢的水果是")
.fontSize(25)
.fontWeight(FontWeight.Bold)
Text(this.answer)
.fontSize(25)
.fontColor(Color.Green)
}
ForEach(this.options, (item: string) => {
Button(item)
.width(100)
.backgroundColor(Color.Green)
.onClick(() => {
this.answer = item;
})
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
语法说明
ForEach
循环渲染的语法如下
ForEach(
arr: any[],
itemGenerator: (item: any, index?: number) => void,
keyGenerator?: (item: any, index?: number) => string
)
各参数的含义如下
- arr
需要进行循环渲染的数据源,必须为数组类型,例如上述案例中的
@State options: string[] = ["苹果", "桃子", "香蕉", "橘子"];
- itemGenerator
组件生成函数,用于为arr
数组中的每个元素创建对应的组件。该函数可接收两个参数,分别是
○ item:arr
数组中的数据项
○ index(可选):arr
数组中的数据项的索引
例如上述案例中的
(item: string) => {
Button(item)
.width(100)
.backgroundColor(Color.Green)
.onClick(() => {
this.answer = item;
})
}
- keyGenerator(可选):
key生成函数,用于为arr
数组中的每个数据项生成唯一的key。
key的作用:
ForEach
在数组发生变化(修改数组元素或者向数组增加或删除元素)时,需要重新渲染组件列表,在重新渲染时,它会尽量复用原来的组件对象,而不是为每个元素都重新创建组件对象。key的作用就是辅助ForEach
完成组件对象的复用。
具体逻辑如下:
ForEach
在进行初次渲染时,会使用keyGenerator
为数组中的每个元素生成一个唯一的key,并将key作为组件对象的标识。当数组发生变化导致ForEach
需要重新渲染时,ForEach
会再次使用keyGenerator
为每个元素重新生成一遍key,然后ForEach
会检查新生成的key在上次渲染时是否已经存在,若存在,ForEach
就会认为这个key对应的数组元素没有发生变化,那它就会直接复用这个key所对应的组件对象;若不存在,ForEach
就会认为这个key对应的元素发生了变化,或者该元素为新增元素,此时,就会为该元素重新创建一个组件对象。
开发者可以通过keyGenerator函数自定义key的生成规则。如果开发者没有定义keyGenerator函数,则系统会使用默认的key生成函数,即
(item: any, index: number) => { return index + '__' + JSON.stringify(item); }
在某些情况下默认的key生成函数,会导致界面渲染效率低下,此时可考虑通过keyGenerator函数自定义生成逻辑,例如如下场景
状态变量数组定义如下
@State arr:string[] = ["zhangsan","lisi","wangwu"]
ForEach语句如下
Column(){
ForEach(this.arr,(item)=>{ Text(item) })
}
初次渲染时,每个元素对应的key依次为0__“zhagnsan”、1__“lisi”、2__“wangwu”。若现有一个操作是向arr数组头部插入新的元素,例如新元素为wanger,按照默认的key生成逻辑,插入新元素之后每个元素的key就会依次变为0__“wanger”、1__“zhagnsan”、2__“lisi”、3__“wangwu”,也就是所有元素的key都发生了变化,因此UI界面更新时需要为每个元素都重新创建组件对象,即便原有的元素没有发生变化也无法复用之前的组件,这样一来就导致了性能浪费。此时我们就可以考虑提供第三个参数,如下
Column(){
ForEach(this.arr, (item)=>{ Text(item) }, item => JSON.stringify(item))
}