鸿蒙开发基础入门
一、熟悉目录结构
二、ArkTS语法介绍
- ArkTS是为构建高性能应用设计的编程语言,语法继承TypeScript,并进行了优化,拥有更强的类型约束
- ArkTS提供了声明式UI范式,符合移动开发的最新趋势
- ArkTS摒弃了部分影响运行时的性能的语法,比如Any。取而代之的是显式类型定义或类型推断
- ArkTS提供了更强的并发编程能力
- ArkTS兼容JS/TS三方库
强的类型约束
- ArkTS要求所有类型在程序运行前都是已知的。避免运行时类型检查。
// ArkTS
const peter: Person = { // 明确类型
id: "001", name: "Peter", age: 10, gender: "male"
}
ArkTS在运行时不允许更改对象布局。
// TypeScript
class Person {
id: string = ""
name: string = ""
}
let peter = new Person()
(peter as any).desc = "XX"
delete (peter as any).desc
// ArkTS
class Person {
id: string = ""
name: string = ""
desc?: string
}
let peter = new Person()
peter.desc = "XX" // 可选属性赋值
peter.desc = undefined // 置空可选属性
声明式UI
- UI描述:装饰器、自定义组件和UI描述机制
- 状态管理:数据驱动UI,数据可在组件、页面、应用及跨设备传递
变量
// 声明变量
let count: number = 0
count = 40
// 声明常量
const MAX_COUNT: number = 100
MAX_COUNT = 200 // 报错,不能修改const声明的值
类型
基本类型:string,number,boolean,enum
引用类型:Array,自定义类
联合类型:Union
类型别名:Aliases
基本类型 string,number,boolean,enum
let name: string = "ZhangSan"
let age: number = 20
let isMale: boolean = true
enum Color {
Red,
Blue,
Green
}
let bgColor: Color = Color.Red
引用类型:Array,自定义类
let students: Array<string> = ["ZhangSan", "LiSi", "WangWu"]
let students: string[] = ["ZhangSan", "LiSi", "WangWu"]
class Person {…}
let person: Person = new Person()
联合类型:Union 允许变量的值为多个类型
let id: number | string = 1
id = "001"
类型别名:Aliases 允许给一个类型取别名,方便理解和复用
type Matrix = number[ ][ ]
type NullableObject = Object | null
空安全
基于联合类型,可以声明可空变量。
let name: string | null = null
console.log(name.length.toString()) // 报错,变量可能为null,无法获取null的length
但以上声明使用时不加判断,会报错,因为编译器认为name有可能为null。null无法调用length属性。
if (name != null) { … } // 1. 用if/else判断
const unwrapped = name ?? "" // 2. 空值合并表达式,??左边为空时返回??右边的值
let len = name?.length // 3. ?可选链,如果name为null,运算符返回undefined
有多种方法使用可空变量,if/else判断、空值合并表达式或者可选链。
类型判断与类型推断
ArkTS是类型安全的语言,编译器会进行类型检查
let name: string = "ZhangSan"
name = 20 // 报错,number不能赋值给string类型变量
以上代码中,name已经为string类型,不能赋值number类型。
ArkTS可以省略类型声明,此时会自动推导类型
let age = 20 // age自动推断为number类型
age没有声明类型,编译器自动推导为number型。
语句
条件语句及条件表达式
条件语句:根据条件真值(true或false)执行不同代码块
let score: number = 90
let passed: boolean = false
if (score >= 60) {
passed = true
} else {
passed = false
}
条件表达式:条件 ? 为真返回值 : 为假返回值
let score: number = 90
let passed: boolean = score >= 60 ? true : false
循环语句
用于重复执行的语句。有for循环、for…of循环以及while循环
给定一个数组
let students: string[] = ["ZhangSan", "LiSi", "WangWu"]
for循环
for (let i = 0; i < students.length; i++) {
console.log(students[i])
}
for…of循环
for (let student of students) {
console.log(student)
}
while循环
let index = 0
while (index < students.length) {
console.log(students[index])
index++
}
函数的声明和使用
函数是多条语句的组合,组成一个可重用的代码块。
用function声明函数
格式如下
function 函数名(参数1: 参数类型1, 参数2: 参数类型2): 返回类型 {
// 函数体
}
例:
function printStudentInfo(students: string[]): void {
for (let student of students) {
console.log(student)
}
}
printStudentInfo(["ZhangSan", "LiSi", "WangWu"])
箭头函数/lambda
简化声明,匿名函数。通常用于把函数作为参数传递。格式如下
(参数1: 参数类型1, 参数2: 参数类型2): 返回类型 => { // 函数体 }
例:
(name: string): void => { console.log(name) }
上例中,返回类型void可以省略。如果函数体只有一行、整个函数声明只有一行,可以省略花括号。
闭包函数
一个函数可以作为另一个函数的返回值。
type returnType = () => string
function outerFunc(): () => string {
let count = 0
return (): string => {
count++
return count.toString()
}
}
let invoker = outerFunc()
console.log(invoker()) // 输出1
console.log(invoker()) // 输出2
类的声明和使用
ArkTS支持面向对象编程。可声明对象属性和方法。
class Person {
name: string = "ZhangSan"
age: number = 20
isMale: boolean = true
}
创建类的实例
const person = new Person()
console.log(person.name)
const person: Person = {
name: "ZhangSan",
age: 29,
isMale: true
}
console.log(person.name)
构造器:在创建类实例的时候可以执行一些代码或传参。
class Person {
name: string = "ZhangSan"
age: number = 20
isMale: boolean = true
constructor(
name: string,
age: number,
isMale: boolean
) {
this.name = name
this.age = age
this.isMale = isMale
}
}
const person = new Person("ZhangSan", 20, false)
console.log(person.name)
上例中在Person声明时定义了一个constructor构造器,创建实例的时候传入相应参数,会执行构造器里的语句。
实例方法:实例调用的方法
class Person {
name: string = "ZhangSan"
age: number = 20
isMale: boolean = true
constructor(...){...}
printInfo() {
if (this.isMale) {
console.log(`${this.name} is a boy, he is ${this.age} years old`)
} else {
console.log(`${this.name} is a girl, she is ${this.age} years old`)
}
}
}
const person = new Person("LiSi", 18, false)
person.printInfo()
上例中声明了一个实例方法printInfo()。在实例person中被调用。
可见性及属性
类成员有 public、private、protected可见性。
public修饰符表示这是公开成员,外部可以自由访问。
private修饰符表示私有成员,只能用在当前类的内部,类的实例和子类都不能使用该成员。
protected修饰符表示该成员是保护成员,只能在类的内部使用该成员,实例无法使用该成员,但是子类内部可以使用。
class Person {
public name: string = "ZhangSan"
private _age: number = 20
isMale: boolean = true
constructor(...) {...}
printInfo() {...}
get age(): number {
return this._age
}
set age(age: number) {
this._age = age
}
}
const person: Person = new Person("LiSi", 18, true)
console.log(person._age.toString()) // 无法访问
console.log(person.age.toString())
另外,对于上例中private _age可以声明setter和getter,在调用的时候可以与访问属性的语法一样来访问。
继承
子类继承父类的特征和行为。关键字extends。
class Employee extends Person {
department: string
constructor(
name: string,
age: number,
isMale: boolean,
department: string
) {
super(name, age, isMale)
this.department = department
}
}
const employee: Employee = new Employee("LiSi", 18, false, "XCompany")
employee.printInfo()
上例中的Employee继承了Person。增加了department属性。构造函数重写了父类的构造函数。使用super()方法可以调用父类的方法实现。
多态
子类继承父类,可以重写父类方法
class Employee extends Person {
department: string
constructor(...) {...}
printInfo(): void {
super.printInfo()
console.log(`working in ${this.department}}`)
}
}
const person: Person = new Person("LiSi", 18, false)
person.printInfo()
const employee: Employee = new Employee("LiSi", 18, false, "XCompany")
employee.printInfo()
Employee构造函数重写了父类的构造函数。使用super()方法可以调用父类的方法实现。在Employee实例化的时候调用重写的构造函数。另外printInfo()也重写了父类的对应方法,并用super()调用了父类的方法。在调用Employee的printInfo()方法时会先调用父类的方法,再执行之后的代码。
模块导出与导入
ArkTS中文件内的作用域是独立的。如果想要在B文件中引用A文件定义的变量、函数、类等,需要使用export关键字。
// Person.ets
export class Person {...}
// Index.ets
import { Person } from './Person'
const person = new Person("LiSi", 18, false)
person.printInfo()
声明式UI
声明式UI是当前移动开发的UI语法新趋势。它相较于传统的命令式UI,代码结构更直观。拥有声明式布局描述及状态驱动视图更新的特点。
传统的命令式UI代码,我们拿安卓举例,像这样
TextView title = findViewById(R.id.tv_title);
title.setText("HarmonyOS Developer World") ;
Button button = findViewById(R.id.btn_join);
Button.setText("Join Now");
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(300,300);
child.setLayoutParams(layoutParams);
linear_layout.addView(title, Integer.valueOf(add_index) - 1);
linear_layout.addView(button, Integer.valueOf(add_index) - 1);
而如果使用声明式UI,可以写成这样
Column() {
Text("HarmonyOS Developer World")
Button("Join Now")
}
本章将以一个待办事项界面为例讲解ArkTS声明式UI。
声明式描述
布局描述
下例中描述了一个横向布局,里面依次排列了一张图片和一个文本框,对应的UI组件如图:
属性设置
使用点语法设置组件属性
Text("待办")
.fontSize(28)
.fontWeight(FontWeight.Bold)
渲染控制
支持if、else条件渲染。
Column() {
Text(`count=${this.count}`)
if (this.count > 0) {
Text(`count is positive`)
.fontColor(Color.Green)
}
}
支持ForEach循环
Column() {
ForEach(this.simpleList, (item: string) => {
Text(item)
}, (item: string) => item)
}
支持LazyForEach循环
private data: MyDataSource = new MyDataSource()
…
List({ space: 3 }) {
LazyForEach(this.data, (item: string) => {
ListItem() {
Row() {
Text(item)
.fontSize(50)
}
}
}, (item: string) => item)
}.cachedCount(5)
class MyDataSource extends BasicDataSource {
private dataArray: string[] = [];
public totalCount(): number {
return this.dataArray.length;
}
public getData(index: number): string {
return this.dataArray[index];
}
public addData(index: number, data: string): void {
this.dataArray.splice(index, 0, data);
this.notifyDataAdd(index);
}
public pushData(data: string): void {
this.dataArray.push(data);
this.notifyDataAdd(this.dataArray.length - 1);
}
}
// Basic implementation of IDataSource to handle data listener
class BasicDataSource implements IDataSource {
private listeners: DataChangeListener[] = [];
private originDataArray: string[] = [];
public totalCount(): number {
return 0;
}
public getData(index: number): string {
return this.originDataArray[index];
}
// 该方法为框架侧调用,为LazyForEach组件向其数据源处添加listener监听
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
console.info('add listener');
this.listeners.push(listener);
}
}
// 该方法为框架侧调用,为对应的LazyForEach组件在数据源处去除listener监听
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
console.info('remove listener');
this.listeners.splice(pos, 1);
}
}
// 通知LazyForEach组件需要重载所有子组件
notifyDataReload(): void {
this.listeners.forEach(listener => {
listener.onDataReloaded();
})
}
// 通知LazyForEach组件需要在index对应索引处添加子组件
notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index);
})
}
// 通知LazyForEach组件在index对应索引处数据有变化,需要重建该子组件
notifyDataChange(index: number): void {
this.listeners.forEach(listener => {
listener.onDataChange(index);
})
}
// 通知LazyForEach组件需要在index对应索引处删除该子组件
notifyDataDelete(index: number): void {
this.listeners.forEach(listener => {
listener.onDataDelete(index);
})
}
// 通知LazyForEach组件将from索引和to索引处的子组件进行交换
notifyDataMove(from: number, to: number): void {
this.listeners.forEach(listener => {
listener.onDataMove(from, to);
})
}
}
状态驱动UI更新
使用@State @Prop @Link等驱动UI更新。
下例说明了一个状态属性是如何驱动UI更新的,isComplete选择不同的图片资源。
下图说明了isComplete控制了文字的样式
以下是其他状态装饰器
•@State:@State装饰的变量拥有其所属组件的状态,可以作为其子组件单向和双向同步的数据源。当其数值改变时,会引起相关组件的渲染刷新。
•@Prop:@Prop装饰的变量可以和父组件建立单向同步关系,@Prop装饰的变量是可变的,但修改不会同步回父组件。
•@Link:@Link装饰的变量和父组件构建双向同步关系的状态变量,父组件会接受来自@Link装饰的变量的修改的同步,父组件的更新也会同步给@Link装饰的变量。
•@Provide/@Consume:@Provide/@Consume装饰的变量用于跨组件层级(多层组件)同步状态变量,可以不需要通过参数命名机制传递,通过alias(别名)或者属性名绑定。
•@Observed:@Observed装饰class,需要观察多层嵌套场景的class需要被@Observed装饰。单独使用@Observed没有任何作用,需要和@ObjectLink、@Prop连用。
•@ObjectLink:@ObjectLink装饰的变量接收@Observed装饰的class的实例,应用于观察多层嵌套场景,和父组件的数据源构建双向同步。
自定义组件
当界面中有相同结构的模块时,可以封装成一个独立组件。
比如每一行代办事项都是由一张图片和一个文本组成。
自定义组件用@Component装饰器来声明。ToDoItem表示每一行代办事项。
@Component
export default struct ToDoItem {…}
在首页如图所示来使用。
子组件的数据
复用的子组件,需要传入数据来显示不同内容,本例中通过content属性给ToDoItem传递数据。
@Component
export default struct ToDoItem {
private content?: string
build() {
Row() {
Image($r('app.media.ic_default'))
Text(this.content)
}
}
}
在首页中参考如下代码传递数据
@Entry
@Component
struct ToDoListPage {
build() {
Column({ space: 16 }) {
Text(“待办”)
…
ToDoItem({ content: "学习" })
}
}
}
组件的生命周期
每个组件,包括页面组件和子组件,都有相应的生命周期。生命周期如下图
自定义组件和页面的关系:
•自定义组件:@Component装饰的UI单元,可以组合多个系统组件实现UI的复用,可以调用组件的生命周期。
•页面:即应用的UI页面。可以由一个或者多个自定义组件组成,@Entry装饰的自定义组件为页面的入口组件,即页面的根节点,一个页面有且仅能有一个@Entry。只有被@Entry装饰的组件才可以调用页面的生命周期。
页面生命周期,即被@Entry装饰的组件生命周期,提供以下生命周期接口:
•onPageShow:页面每次显示时触发一次,包括路由过程、应用进入前台等场景。
•onPageHide:页面每次隐藏时触发一次,包括路由过程、应用进入后台等场景。
•onBackPress:当用户点击返回按钮时触发。
组件生命周期,即一般用@Component装饰的自定义组件的生命周期,提供以下生命周期接口:
•aboutToAppear:组件即将出现时回调该接口,具体时机为在创建自定义组件的新实例后,在执行其build()函数之前执行。
•aboutToDisappear:aboutToDisappear函数在自定义组件析构销毁之前执行。不允许在aboutToDisappear函数中改变状态变量,特别是@Link变量的修改可能会导致应用程序行为不稳定。
生命周期流程如下图所示,下图展示的是被@Entry装饰的组件(页面)生命周期。
@Builder装饰器:自定义构建函数
区别与子组件的声明,在当前组件中也可以使用@Builder来装饰一个方法,用于提取一些可复用的UI代码。
在ToDoItem中,对于Image组件的描述可以提取成一个方法,并用@Builder来装饰,然后可以根据条件引用不同图片文件。
@Component
export default struct ToDoItem {
private content?: string
@Builder
labelIcon(icon: Resource) {
Image(icon)
.objectFit(ImageFit.Contain)
.width(28)
.height(28)
.margin(20)
}
build() {
Row() {
this.labelIcon($r('app.media.ic_default'))
Text()
}
}
}
渲染列表数据
子组件ToDoItem的设计已经完成,现在在首页,我们需要用一个属性提供列表数据。下例中totalTasks保存了一个字符串数组。用ForEach来循环渲染。
@Entry
@Component
struct ToDoListPage {
private totalTasks: Array<string> = [
"晨练",
"做早餐",
"读书",
"学习",
"刷剧"
]
build() {
Column({ space: 16 }) {
Text("待办")
…
ForEach(this.totalTasks, (item: string) => {
ToDoItem({ content: item })
}, …)
}
}
}