UI框架
HarmonyOS提供了一套UI开发框架,即方舟开发框架(ArkUI框架)。方舟开发框架可为开发者提供应用UI开发所必需的能力,比如多种组件、布局计算、动画能力、UI交互、绘制等。
两种开发范式
-
概念
-
声明式开发范式:采用基于TypeScript声明式UI语法扩展而来的ArkTS语言,从组件、动画和状态管理三个维度提供UI绘制能力。
-
类Web开发范式:采用经典的HML、CSS、JavaScript三段式开发方式,即使用HML标签文件搭建布局、使用CSS文件描述样式、使用JavaScript文件处理逻辑。该范式更符合于Web前端开发者的使用习惯,便于快速将已有的Web应用改造成方舟开发框架应用
-
对比
| 开发范式名称 | 语言生态 | UI更新方式 | 适用场景 | 适用人群 |
|---|---|---|---|---|
| 声明式开发范式 | ArkTS语言 | 数据驱动更新 | 复杂度较大、团队合作度较高的程序 | 移动系统应用开发人员、系统应用开发人员 |
| 类Web开发范式 | JS语言 | 数据驱动更新 | 界面较为简单的程序应用和卡片 | Web前端开发人员 |
框架示意图
应用模型
Stage模型是主推模型。应用模型官方文档
HarmonyOS先后提供了两种应用模型:
-
FA(Feature Ability)模型:HarmonyOS早期版本开始支持的模型,已经不再主推。
-
Stage模型:HarmonyOS 3.1 Developer Preview版本开始新增的模型,是目前主推且会长期演进的模型。在该模型中,由于提供了AbilityStage、WindowStage等类作为应用组件和Window窗口的“舞台”,因此称这种应用模型为Stage模型。
像素单位
HarmonyOS 采用vp为基准数据单位。
| 名称 | 描述 |
|---|---|
| px | 屏幕物理像素单位。 |
| vp | 屏幕密度相关像素,根据屏幕像素密度转换为屏幕物理像素,当数值不带单位时,默认单位vp。在实际宽度为1440物理像素的屏幕上,1vp约等于3px。 |
| fp | 字体像素,与vp类似适用屏幕密度变化,随系统字体大小设置变化。 |
| lpx | 视窗逻辑像素单位,lpx单位为实际屏幕宽度与逻辑宽度(通过designWidth配置)的比值,designWidth默认值为720。当designWidth为720时,在实际宽度为1440物理像素的屏幕上,1lpx为2px大小。 |
配置文件介绍
应用的配置文件
bundleName:新建项目的时候指定的,项目后续发布打包部署的时候要用到。命名格式:域名倒置方式
AppScope/app.json5
{
"app": {
"bundleName": "com.example.hm", // 唯一标识
"vendor": "example",
"versionCode": 1000000, // 数字版本
"versionName": "1.0.0", // 版本
"icon": "$media:app_icon", // app在应用管理展示的图标
"label": "$string:app_name" // app在应用管理展示的名称
}
}
手机应用管理显示的App名称和图标
app的icon和名称的配置文件位置:
app名称配置:
app图标配置:
手机的应用管理显示:
模块配置
一个项目下可以有多个模块,每个模块都可以打包成一个HAP文件。这里enrty文件夹相当于一个模块。
项目里的多个模块,只能有一个 entry类型的模块,其他一般为feature类型模块。
每个模块编译后都可独立运行。
src/main/module.json5 (入口模块)
{
"module":
"name": "entry", // 模块名称
"type": "entry", // 模块类型,项目只能有一个entry类型模块,type: entry | feature | library
// 模块描述,读取的src/main/resources/base/element/string.json
"description": "$string:module_desc",
// entryability文件下可配置多个ability文件
// 指定该模块的入口EntryAbility文件,src/main/ets/entryability/EntryAbility.ts
"mainElement": "EntryAbility",
"deviceTypes": [ // 设备类型
"phone",
"tablet"
],
// 项目中,entry模块必须安装,feature模块按需安装
// 该模块是否跟随app必须安装
"deliveryWithInstall": true,
"installationFree": false,
// 该模块包含的所有页面,读取的src/main/resources/base/profile/main_pages.json
"pages": "$profile:main_pages",
// entryability文件下配置的ability文件,因可配置多个,这里是对象数组形式
"abilities": [
{
"name": "EntryAbility", // 该模块是入口ability
"srcEntry": "./ets/entryability/EntryAbility.ts", // ability的源码位置
"description": "$string:EntryAbility_desc", // ability的描述
"icon": "$media:icon", // ability图标,应用的桌面图标,由于入口ability,也就是app的图标
// ability描述,应用的桌面名称,由于入口ability,也就是app的名称
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:icon", // 应用启动那一刻展示的图标
"startWindowBackground": "$color:start_window_background", // 应用启动那一刻展示的背景色
"exported": true,
"skills": [ // ability跳转相关配置
{
"entities": [
"entity.system.home" // 代表当前ability是项目入口
],
"actions": [
"action.system.home"
]
}
]
}
],
"requestPermissions": [ // 自己配置的网络权限
{
"name": 'ohos.permission.INTERNET'
}
]
}
}
路由配置
src/main/resources/base/profile/main_pages.json
pages文件夹新建Page会自动生成对应路由,而新建ArkTs File不会自动生成路由,此时需手动配置。
路由的配置文件夹不局限于pages文件夹,其他文件夹也可以,只是需要手动配置路由。
中英文配置
中英文配置会先在en_US文件夹和zh_CN文件夹下的string.json查找,查找不到会在base默认文件夹下的string.json查找。实际上,zh_CN和en_US有的属性,base中也都有。
在修改中英文配置的时候
点击右上角的Open editor
这里操作,可直接配置三个string.json文件,非常方便。
状态变量 (@State装饰器)
认识
@State装饰的变量,或称为状态变量,一旦变量拥有了状态属性,就和自定义组件的渲染绑定起来。当状态改变时,UI会发生对应的渲染改变。
@State装饰的变量,与声明式范式中的其他被装饰变量一样,是私有的,只能从组件内部访问,在声明时必须指定其类型和本地初始化 (不赋值会报错哦~)。初始化也可选择使用命名参数机制从父组件完成初始化。
总结:声明变量不使用@State装饰器,变量改变,页面不渲染。声明变量使用@State装饰器,变量改变,页面同步渲染。
-
特点
-
@State装饰的变量与子组件中的@Prop、@Link或@ObjectLink装饰变量之间建立单向或双向数据同步。
-
@State装饰的变量生命周期与其所属自定义组件的生命周期相同。
-
使用
@State xxx: number = 1
- eg
struct Index {
// num: number = 1 不同步渲染
// 同步渲染
@State num: number = 1 // 定义状态变量
build() {
Row() {
Column() {
Text('NUM' + this.num) // 使用状态变量
.fontSize("30vp")
Button('点击NUM++')
.onClick(() => {
this.num++ // 使用状态变量
})
}
.width('100%')
}
.height('100%')
}
}
补充
- @State装饰器标记的变量必须初始化,不能为空值
如果没有初始化,会报错提示:由“@State”、“@StorageLink”、“@StorageProp”和“@Provider”修饰的变量必须在本地初始化。
- @State数据类型不可以定义any
如果定义any,预览器日志会提示:[Compile Result] Please define an explicit type, not any.
此时定义的状态变量,预览器也不会显示。
-
@State支持Object、class、string、number、boolean、enum类型,以及这些类型的数组
-
嵌套对象中的属性修改以及数组中的对象属性修改无法触发视图更新
class Person {
name: string
age: number
friend: Person
constructor(name: string, age: number, friend?: Person) {
this.name = name
this.age = age
this.friend = friend
}
}
@Entry
@Component
struct StatePage {
index: number = 1
@State p: Person = new Person('zs', 66, new Person('ls', 55))
@State friends: Person[] = [
new Person('qwe', 11),
new Person('asd', 22)
]
build() {
Row() {
Column() {
Text(`对象的属性:${this.p.name}-${this.p.age}`)
.fontSize(30)
.fontWeight(FontWeight.Bold)
.onClick(() => {
// 对象的属性修改,视图更新
this.p.age++
})
Text(`嵌套对象的属性:${this.p.friend.name}:${this.p.friend.age}`)
.fontSize(30)
.fontWeight(FontWeight.Bold)
.onClick(() => {
// 嵌套对象的属性修改,视图不更新(数据更新了)
this.p.friend.age++
})
Text('对象数组的属性:').fontSize(30).fontWeight(FontWeight.Bold)
ForEach(this.friends, (p, index) => {
Row() {
Text(`${p.name}-${p.age}`)
.fontSize(25).fontWeight(FontWeight.Bold)
.onClick(() => {
// 对象数组的属性修改,视图不更新(数据更新了)
p.age++
// 只有数组添加删除或者赋值的时候,视图更新
// this.friends[index] = new Person(p.name, p.age++)
})
Button('删除').fontSize(25)
.onClick(() => {
this.friends.splice(index, 1)
})
}
})
Button('添加')
.onClick(() => {
this.friends.push(new Person('friend' + this.index++, 33))
})
}
.width('100%')
}
.height('100%')
}
}
事件
事件链式调用方式。
- 箭头函数
Button('按钮')
.onClick(()=>{
this.name='zs';
})
- 匿名函数
匿名函数需使用bind配置this, 确保this指向当前组件。
Button('按钮')
.onClick(function(){
this.name='zs';
}.bind(this))
- 成员函数
成员函数需使用bind配置this, 确保this指向当前组件。
clickHandler(): void{
this.name='zs';
}
Button('按钮')
.onClick(this.clickHandler.bind(this))
自定义组件 (@Component装饰器)
由框架直接提供的称为系统组件,由开发者定义的称为自定义组件。
注意:同项目下各个文件中的自定义组件不可使用重复名称,不然会报错的。
注意:
-
组件必须使用 @Component 装饰。
-
只有 @Entry 装饰的组件,才显示于页面上。
-
@Entry装饰的入口组件,build()中必须有且仅有一个根容器组件。
-
其他的自定义组件,build()中必须有且仅有一个根组件。
根组件、根容器组件的举例:
根容器组件
@Entry
@Component
struct XX{
build(){
Row(){} // 根容器组件
}
}
根组件
@Component
struct XX{
build(){
Divider(){} // 根组件
// Text() //第二个根组件 会报错
}
}
既是根组件又是根容器组件
@Component
struct XX{
build(){
Row(){} // 既是根组件又是根容器组件
}
}
-
特点
-
可组合:允许开发者组合使用系统组件、及其属性和方法。
-
可重用:自定义组件可以被其他组件重用,并作为不同的实例在不同的父组件或容器中使用。
-
数据驱动UI更新:通过状态变量的改变,来驱动UI的刷新
-
使用
@Component
struct xxxComponent {
build() {
}
}
- eg
@Entry
@Component
struct MyCustomComponent {
build() {
Row() {
Column() {
// 使用 自定义组件
itemComponent()
// 使用 自定义组件 传参数
itemComponent({ content: '传的参数' })
}
.width('100%')
}
.height('100%')
}
}
// 定义 自定义组件
// 功能:点击放大字体
@Component
struct itemComponent {
// 自定义组件可以使用变量(变量默认私有化) 传递参数
content: string = '自定义组件的变量' // 其实默认是private
@State isSelected: boolean = false
build() {
Row() { // 必须有一个根组件
Text(this.content)
.fontSize(this.isSelected ? 40 : 20)
}
.onClick(() => {
this.isSelected = !this.isSelected
})
}
}
自定义构建函数 (@Builder装饰器)
ArkUI提供了一种更轻量的UI元素复用机制@Builder,@Builder所装饰的函数遵循build()函数语法规则,可以将重复使用的UI元素抽象成一个方法,在build方法里调用。
注意:普通函数内不能使用组件。加@Builder修饰,可使用组件。
组件内构建函数
只能当前组件内,调用使用。
-
特点
-
允许在自定义组件内定义一个或多个@Builder方法,该方法被认为是该组件的私有、特殊类型的成员函数。
-
自定义构建函数可以在所属组件的build方法和其他自定义构建函数中调用,但不允许在组件外调用。
-
在自定义函数体中,this指代当前所属组件,组件的状态变量可以在自定义构建函数内访问。建议通过this访问自定义组件的状态变量而不是参数传递。(PS:函数内,参数使用无需this)
-
定义
@Builder xxxBuilder(){ ... }
- 使用
this.xxxBuilder(){ ... }
- eg
@Entry
@Component
struct BuilderFun {
@State isSelected: boolean = false
// 局部自定义组件构造函数
@Builder item(content: string) {
Row() {
Text(content) // 参数不能写this.
.fontSize(this.isSelected ? 40 : 20)
}
.onClick(() => {
this.isSelected = !this.isSelected
})
}
build() {
Row() {
Column() {
// 使用 局部自定义组件构造函数
this.item('使用局部自定义组件构造函数')
}
.width('100%')
}
.height('100%')
}
}
全局构造函数
所有组件内,都可以调用使用。
相比组件内自定义: 1.函数加function 2.调用不需要this
-
特点
-
全局的自定义构建函数可以被整个应用获取,不允许使用this和bind方法。
-
如果不涉及组件状态变化,建议使用全局的自定义构建方法。
-
定义
@Builder function xxxBuilder(){ ... }
- 使用
xxxBuilder()
- eg
@Entry
@Component
struct BuilderFun {
@State isSelected: boolean = false
build() {
Row() {
Column() {
// 使用 全局自定义组件构造函数
item('使用全局自定义组件构造函数') // 去掉this
}
.width('100%')
}
.height('100%')
}
}
// 全局自定义组件构造函数
@Builder function item(content: string) { // 添加function
Row() {
Text(content) // 参数不能写this.
.fontSize(this.isSelected ? 40 : 20)
}
.onClick(() => {
this.isSelected = !this.isSelected
})
}
参数传递
-
特点
-
参数的类型必须与参数声明的类型一致,不允许undefined、null和返回undefined、null的表达式。
-
在自定义构建函数内部,不允许改变参数值。如果需要改变参数值,且同步回调用点,建议使用@Link。
-
@Builder内UI语法遵循UI语法规则。
按引用传递
传递的参数可为状态变量,且状态变量的改变会引起@Builder方法内的UI刷新。
ArkUI提供 $$ 作为按引用传递参数的范式。
- 定义
xxxBuilder( $$ : { paramA1: string, paramB1 : string } { ... }
- eg
@Entry
@Component
struct Test {
@State num: number = 0;
build() {
Column() {
showNum({ param: this.num })
Button('点我num+1')
.padding(20)
.fontSize(20)
.onClick(() => {
// num的改变会引起showNum方法内的UI刷新
// 有点类似vue的computed
this.num++;
})
}
}
}
@Builder function showNum($$: { param: number }) {
Row() {
Text(`num: ${$$.param} `)
.fontSize(30)
}
}
按值传递 (默认)
调用@Builder装饰的函数默认按值传递。
当传递的参数为状态变量时,状态变量的改变不会引起@Builder方法内的UI刷新。
所以当使用状态变量的时候,推荐使用按引用传递。
- 定义
xxxBuilder(param: string){ ... }
- eg
@Entry
@Component
struct Parent {
name: string = 'zs';
build() {
Column() {
showName(this.name)
}
}
}
@Builder function showName(name: string) {
Row() {
Text(`name: ${name} `)
.fontSize(30)
}
}
自定义组件重用样式 (@Styles装饰器)
@Styles装饰器可以将多条样式设置提炼成一个方法,直接在组件声明的位置调用。通过@Styles装饰器可以快速定义并复用自定义样式。用于快速定义并复用自定义样式。
注意:同项目下各个文件中的自定义组件重用样式不可使用重复名称,不然会报错的。
-
特点
-
@Styles方法不支持参数
-
@Styles可以定义在组件内或全局,在全局定义时需在方法名前面添加function关键字,组件内定义时则不需要添加function关键字
-
定义在组件内的@Styles可以通过this访问组件的常量和状态变量,并可以在@Styles里通过事件来改变状态变量的值
-
组件内@Styles的优先级高于全局@Styles (PS: 优先找当前组件内的@Styles,如果找不到,则会全局查找)
组件内重用样式
- 定义
@Styles xxxStyle(){ ... }
- 使用
Text().xxxStyle()
- eg
@Entry
@Component
struct StyleFun {
// 定义 组件内重用样式
@Styles commonStyles(){
.width(200)
.height(100)
.padding(30)
.margin(10)
.backgroundColor(Color.Pink)
.border({ radius: 10 })
}
build() {
Row() {
Column() {
// 使用 组件内重用样式
Text('@Styles装饰器').commonStyles()
Button().commonStyles()
}
.width('100%')
}
.height('100%')
}
}
全局重用样式
- 定义
@Styles function xxxStyle() { ... }
- 使用
Text().xxxStyle()
- eg
@Entry
@Component
struct StyleFun {
build() {
Row() {
Column() {
// 使用 全局重用样式
Text('@Styles装饰器').commonStyles()
Button().commonStyles()
}
.width('100%')
}
.height('100%')
}
}
// 定义 全局重用样式
// 注意:全局 加function
// 优先级:组件内重用样式 > 全局重用样式
@Styles function commonStyles() {
.width(200)
.height(100)
.padding(30)
.margin(10)
.backgroundColor(Color.Pink)
.border({ radius: 10 })
}
扩展组件样式 (@Extend装饰器)
在@Styles的基础上,提供的@Extend用于扩展原生组件样式。
-
特点
-
和@Styles不同,@Extend仅支持定义在全局,不支持在组件内部定义
-
和@Styles不同,@Extend支持封装指定的组件的私有属性和私有事件和预定义相同组件的@Extend的方法
-
和@Styles不同,@Extend装饰的方法支持参数,开发者可以在调用时传递参数,调用遵循TS方法传值调用
-
@Extend装饰的方法的参数可以为function,作为Event事件的句柄
-
@Extend的参数可以为状态变量,当状态变量改变时,UI可以正常的被刷新渲染
-
定义
@Extend(UIComponentName) function xxxStyle (params?:xxx) { ... }
- eg
@Entry
@Component
struct ExtendFun {
@State count: number = 0
build() {
Row() {
Column() {
// 使用 扩展组件样式
Text('@Extend')
.textStyle(30, Color.Pink)
Text('@Extend')
.textStyle(40, '#ccc')
// 使用 参数为方法的扩展组件样式
Button(this.count.toString()).btnStyle(() => {
this.count++
})
}
.width('100%')
}
.height('100%')
}
}
// 定义 扩展组件样式1
@Extend(Text) function sizeStyle(fontSize: number) {
.fontSize(fontSize)
}
// 定义 扩展组件样式2
@Extend(Text) function textStyle(fontSize: number, fontColor: Color | string) {
// 调用 扩展组件样式1
.sizeStyle(fontSize)
.fontColor(fontColor)
.lineHeight(60)
}
// 定义 参数为方法的扩展组件样式
@Extend(Button) function btnStyle(click: () => void) {
.fontSize(30)
.width(200)
.height(50)
.onClick(() => {
click()
})
}
父子单向同步 (@Prop装饰器)
@Prop装饰的变量可以和父组件建立单向的同步关系。@Prop装饰的变量是可变的,但是变化不会同步回其父组件。
-
特点
-
@Prop变量允许在本地修改,但修改后的变化不会同步回父组件。
-
当父组件中的数据源更改时,与之相关的@Prop装饰的变量都会自动更新。如果子组件已经在本地修改了@Prop装饰的相关变量值,而在父组件中对应的@State装饰的变量被修改后,子组件本地修改的@Prop装饰的相关变量值将被覆盖。
-
@Prop装饰器不能在@Entry装饰的自定义组件中使用。
-
定义
@Prop xxx: number = 1
- eg
@Entry
@Component
struct PropFun {
// @State 必须初始化
@State name: string = 'zs'
build() {
Row() {
Column() {
Text(this.name)
Button('按钮')
.onClick(() => {
this.name = this.name === 'zs' ? 'ls' : 'zs'
})
Divider()
PropFun_prop({ content_prop: this.name }) //使用@prop的子组件
}
.width('100%')
}
.height('100%')
}
}
// @Prop 装饰的状态数据 便于父子组件传值和同步
// @Prop 装饰的变量是可变的,但修改不会同步回父组件。
// State ---> Prop 单向
@Component
struct PropFun_prop {
@Prop content_prop: string
build() {
Column() {
Text('Prop:' + this.content_prop)
Button('修改Prop数据')
.onClick(() => {
this.content_prop = '改变Prop数据' //子组件数据变了,父组件数据不变
})
}
}
}
父子双向同步 (@Link装饰器)
@Link装饰的变量与其父组件中的数据源共享相同的值。
-
特点
-
@Link装饰的变量和其所属的自定义组件共享生命周期
-
@Link装饰器不能在@Entry装饰的自定义组件中使用
-
定义
@Link xxx: number = 1
- eg
@Entry
@Component
struct LinkFun {
// @State 必须初始化
@State name: string = 'zs'
build() {
Row() {
Column() {
Text(this.name)
Button('按钮')
.onClick(() => {
this.name = this.name === 'zs' ? 'ls' : 'zs'
})
Divider()
// LinkFun_link({ content_link: this.name }) //Preview正常,但本行报红
// 如果是 State <---> Link 参数传递使用$变量名,而不是this.变量
LinkFun_link({ content_link: $name })
}
.width('100%')
}
.height('100%')
}
}
// @Link 装饰的状态数据 同步父组件
// State <---> Prop
@Component
struct LinkFun_link {
@Link content_link: string
build() {
Column() {
Text('Link:' + this.content_link)
Button('修改Link数据').onClick(() => {
this.content_link = '改变Link数据'
})
}
}
}
- @Prop和@Link对比
demo:
// 任务类
class Task {
// 静态变量,类内部所有对象共享的变量
static id: number = 1
// 任务名称
name: string = `任务${Task.id++}`
// 任务状态
finished: boolean = false
}
// 统一的卡片样式
@Styles function PropPage_Card() {
.width('95%')
.padding(20)
.backgroundColor(Color.White)
.borderRadius(15)
.shadow({ radius: 6, color: '#1F000000', offsetX: 2, offsetY: 4 })
}
//任务完成样式
@Extend(Text) function PropPage_FinishedStyle() {
.decoration({ type: TextDecorationType.LineThrough })
.fontColor('#B1B2B1')
}
@Entry
@Component
struct PropPage {
// 总任务数量
@State totalTask: number = 0
// 已完成任务数量
@State finishTask: number = 0
build() {
Column({ space: 10 }) {
// 1.任务进度卡片
PropPage_TaskStatistics({ finishTask: this.finishTask, totalTask: this.totalTask })
// 2.任务列表
PropPage_TaskList({ finishTask: $finishTask, totalTask: $totalTask })
}
.width('100%')
.height('100%')
.backgroundColor('#F1F2F3')
}
}
@Component
struct PropPage_TaskStatistics {
@Prop finishTask: number
@Prop totalTask: number
build() {
Row() {
Text('任务进度:')
.fontSize(30)
.fontWeight(FontWeight.Bold)
Stack() {
Progress({
value: this.finishTask,
total: this.totalTask,
type: ProgressType.Ring
})
.width(100)
Row() {
Text(this.finishTask.toString())
.fontSize(24)
.fontColor('#36D')
Text('/' + this.totalTask.toString())
.fontSize(24)
}
}
}
.PropPage_Card()
.margin({ top: 20, bottom: 10 })
.justifyContent(FlexAlign.SpaceEvenly)
}
}
@Component
struct PropPage_TaskList {
@Link finishTask: number
@Link totalTask: number
// 任务数组
@State tasks: Task[] = []
handleTaskChange() {
// 1.更新任务总数量
this.totalTask = this.tasks.length
// 2.更新已完成任务数量
this.finishTask = this.tasks.filter(item => item.finished).length
}
build() {
Column() {
// 2.新增任务按钮
Button('新增任务')
.width(200)
.margin({bottom:20})
.onClick(() => {
// 1.新增任务数据
this.tasks.push(new Task())
// 2.更新任务总数量
this.handleTaskChange()
})
// 3.任务列表
List({ space: 10 }) {
ForEach(this.tasks, (item: Task, index) => {
ListItem() {
Row() {
Text(item.name)
.fontSize(20)
Checkbox()
.select(item.finished)
.onChange(val => {
// 1.更新当前任务状态
item.finished = val
// 2.更新已完成任务数量
this.handleTaskChange()
})
}
.PropPage_Card()
.justifyContent(FlexAlign.SpaceBetween)
}
.swipeAction({ end: this.PropPage_DeleteButton(index) })
})
}
.width('100%')
.layoutWeight(1)
.alignListItem(ListItemAlign.Center)
}
}
@Builder PropPage_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()
})
}
}
与后代组件双向同步 (@Provide装饰器和@Consume装饰器)
@Provide和@Consume,应用于与后代组件的双向数据同步,应用于状态数据在多个层级之间传递的场景。不同于上文提到的父子组件之间通过命名参数机制传递,@Provide和@Consume摆脱参数传递机制的束缚,实现跨层级传递。
其中@Provide装饰的变量是在祖先节点中,可以理解为被“提供”给后代的状态变量。@Consume装饰的变量是在后代组件中,去“消费(绑定)”祖先节点提供的变量。
-
特点
-
@Provide装饰的状态变量自动对其所有后代组件可用,即该变量被“provide”给他的后代组件。
-
后代通过使用@Consume去获取@Provide提供的变量,建立在@Provide和@Consume之间的双向数据同步,与@State/@Link不同的是,前者可以在多层级的父子组件之间传递。
-
@Provide和@Consume可以通过相同的变量名或者相同的变量别名绑定,变量类型必须相同。
-
不允许在同一个自定义组件内,包括其子组件中声明多个同名或者同别名的@Provide装饰的变量。
-
定义
@Provide('xxx') xxxx: string = 'zs'
@Consume xxxx: string
- eg
@Entry
@Component
struct ProvideConsume {
// @Provide('别名') 变量名
@Provide('alias') message: string = 'zs'
build() {
Row() {
Column({ space: 20 }) {
Text('父组件内容:' + this.message).ProvideConsume_textStyle()
.onClick(() => {
this.message = '父组件修改了'
})
Divider()
// 父调用子
ProvideConsume_son()
}
.width('100%')
}
.height('100%')
}
}
// 子组件
@Component
struct ProvideConsume_son {
@Consume alias: string // 使用Provide的别名
build() {
Column({ space: 20 }) {
Text('子组件内容:' + this.alias).ProvideConsume_textStyle()
.onClick(() => {
this.alias = '子组件修改了'
})
Divider()
// 子调用孙
ProvideConsume_grandson()
}
}
}
// 孙组件
@Component
struct ProvideConsume_grandson {
@Consume message: string // 使用Provide的变量名
build() {
Column() {
Text('孙组件内容:' + this.message).ProvideConsume_textStyle()
.onClick(() => {
this.message = '孙组件修改了'
})
}
}
}
@Extend(Text) function ProvideConsume_textStyle() {
.fontSize(30)
.fontWeight(FontWeight.Bold)
.fontColor(Color.Pink)
}
嵌套类对象双向同步 (@Observed装饰器和@ObjectLink装饰器)
@Observed和@ObjectLink装饰器用于在涉及嵌套对象或数组元素为对象的场景中进行双向数据同步。
-
特点
-
使用@Observed装饰class会改变class原始的原型链,@Observed和其他类装饰器装饰同一个class可能会带来问题。
-
@ObjectLink装饰器不能在 @Entry装饰的自定义组件中使用。
-
@ObjectLink只能接收被@Observed装饰class的实例。
-
@ObjectLink装饰的变量不能被赋值,如果要使用赋值操作,请使用@Prop。
// 允许@ObjectLink装饰的数据属性赋值 this.objLink.a= ... // 不允许@ObjectLink装饰的数据自身赋值 this.objLink= ...-
@Prop装饰的变量和数据源的关系是是单向同步,@Prop装饰的变量在本地拷贝了数据源,所以它允许本地更改,如果父组件中的数据源有更新,@Prop装饰的变量本地的修改将被覆盖;
-
@ObjectLink装饰的变量和数据源的关系是双向同步,@ObjectLink装饰的变量相当于指向数据源的指针。如果一旦发生@ObjectLink装饰的变量的赋值,则同步链将被打断。
-
-
使用
@Observed
class XxxClass {
}
@Entry
@Component
struct Xxx {
build() {
Column() {
XxxComponent({ xxx: xxx })
}
}
}
@Component
struct XxxComponent {
// 自定义组件形式使用@ObjectLink
@ObjectLink xxx: XxxClass
build() {
Row() {
Text(this.xxx.xxx)
}
}
}
- eg
// 任务类
@Observed
class Task {
// 静态变量,类内部所有对象共享的变量
static id: number = 1
// 任务名称
name: string = `任务${Task.id++}`
// 任务状态
finished: boolean = false
}
// 统一的卡片样式
@Styles function PropPage_Card() {
.width('95%')
.padding(20)
.backgroundColor(Color.White)
.borderRadius(15)
.shadow({ radius: 6, color: '#1F000000', offsetX: 2, offsetY: 4 })
}
//任务完成样式
@Extend(Text) function PropPage_FinishedStyle() {
.decoration({ type: TextDecorationType.LineThrough })
.fontColor('#B1B2B1')
}
@Entry
@Component
struct PropPage {
// 总任务数量
@State totalTask: number = 0
// 已完成任务数量
@State finishTask: number = 0
build() {
Column({ space: 10 }) {
// 1.任务进度卡片
PropPage_TaskStatistics({ finishTask: this.finishTask, totalTask: this.totalTask })
// 2.任务列表
PropPage_TaskList({ finishTask: $finishTask, totalTask: $totalTask })
}
.width('100%')
.height('100%')
.backgroundColor('#F1F2F3')
}
}
@Component
struct PropPage_TaskStatistics {
@Prop finishTask: number
@Prop totalTask: number
build() {
Row() {
Text('任务进度:')
.fontSize(30)
.fontWeight(FontWeight.Bold)
Stack() {
Progress({
value: this.finishTask,
total: this.totalTask,
type: ProgressType.Ring
})
.width(100)
Row() {
Text(this.finishTask.toString())
.fontSize(24)
.fontColor('#36D')
Text('/' + this.totalTask.toString())
.fontSize(24)
}
}
}
.PropPage_Card()
.margin({ top: 20, bottom: 10 })
.justifyContent(FlexAlign.SpaceEvenly)
}
}
@Component
struct PropPage_TaskList {
@Link finishTask: number
@Link totalTask: number
// 任务数组
@State tasks: Task[] = []
handleTaskChange() {
// 1.更新任务总数量
this.totalTask = this.tasks.length
// 2.更新已完成任务数量
this.finishTask = this.tasks.filter(item => item.finished).length
}
build() {
Column() {
// 2.新增任务按钮
Button('新增任务')
.width(200)
.margin({ bottom: 20 })
.onClick(() => {
// 1.新增任务数据
this.tasks.push(new Task())
// 2.更新任务总数量
this.handleTaskChange()
})
// 3.任务列表
List({ space: 10 }) {
ForEach(this.tasks, (item: Task, index) => {
ListItem() {
PropPage_TaskItem({ item: item, onTaskChange: this.handleTaskChange.bind(this) })
}
.swipeAction({ end: this.PropPage_DeleteButton(index) })
})
}
.width('100%')
.layoutWeight(1)
.alignListItem(ListItemAlign.Center)
}
}
@Builder PropPage_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 PropPage_TaskItem {
@ObjectLink item: Task
onTaskChange: () => void
build() {
Row() {
if (this.item.finished) {
Text(this.item.name)
.PropPage_FinishedStyle()
} else {
Text(this.item.name)
.fontSize(20)
}
Checkbox()
.select(this.item.finished)
.onChange(val => {
// 1.更新当前任务状态
this.item.finished = val
// 2.更新已完成任务数量
this.onTaskChange()
})
}
.PropPage_Card()
.justifyContent(FlexAlign.SpaceBetween)
}
}
状态变量更改通知 (@Watch装饰器)
@Watch用于监听状态变量的变化,当状态变量变化时,@Watch的回调方法将被调用。@Watch在ArkUI框架内部判断数值有无更新使用的是严格相等(===),遵循严格相等规范。当在严格相等为false的情况下,就会触发@Watch的回调。
-
特点
-
当观察到状态变量的变化(包括双向绑定的AppStorage和LocalStorage中对应的key发生的变化)的时候,对应的@Watch的回调方法将被触发
-
@Watch方法在自定义组件的属性变更之后同步执行
-
如果在@Watch的方法里改变了其他的状态变量,也会引起状态变更和@Watch的执行
-
在第一次初始化的时候,@Watch装饰的方法不会被调用,即认为初始化不是状态变量的改变。只有在后续状态改变时,才会调用@Watch回调方法
-
为了避免循环的产生,建议不要在@Watch的回调方法里修改当前装饰的状态变量
-
回调函数应仅执行快速运算
-
不建议在@Watch函数中调用async await,因为@Watch设计的用途是为了快速的计算,异步行为可能会导致重新渲染速度的性能问题
-
使用
@State @Watch('zzz') xxx: number = 1
@State yyy: number = 1
zzz() {
this.yyy++
}
- eg
/*
* @Watch 修饰 状态数据
* watch函数中不要改变被监视的状态变量 会无限循环 报错
* */
@Entry
@Component
struct WatchDct {
@State @Watch('change') count: number = 1
@State @Watch('change') pow: number = 2
@State result: number = 1
change() {
// this.count += 2 无线循环 会报错
this.result = Math.pow(this.count, this.pow)
}
build() {
Row() {
Column() {
Text('基数:' + this.count)
.fontSize(50)
.onClick(() => {
this.count++
})
Divider()
Text('次幂:' + this.pow)
.fontSize(50)
.onClick(() => {
this.pow++
})
Divider()
Text('结果:' + this.result)
.fontSize(50)
}
.width('100%')
}
.height('100%')
}
}
多态样式 (stateStyles)
@Styles和@Extend仅仅应用于静态页面的样式复用,stateStyles可以依据组件的内部状态的不同,快速设置不同样式。
stateStyles是属性方法,可以根据UI内部状态来设置样式,类似于css伪类,但语法不同。
ArkUI提供四种状态:
-
focused:获焦态。
-
normal:正常态。
-
pressed:按压态。
-
disabled:不可用态。
-
使用
Button('Click')
.stateStyles({
normal: {
// xxx
},
focused: {
// xxx
},
pressed: {
// xxx
},
disabled: {
// xxx
}
})
- eg
@Entry
@Component
struct StateStyleFun {
build() {
Row() {
Column() {
// Button('可以获取焦点')
Text('不可以获取焦点')
Button('stateStyle') // 获焦:粉色 ;不获焦:蓝色
.fontSize(40)
.fontWeight(FontWeight.Bold)
.stateStyles({
normal: {
.backgroundColor(Color.Blue)
},
focused: {
.backgroundColor(Color.Pink)
},
pressed: {
.backgroundColor(Color.Green)
},
disabled: {
.backgroundColor(Color.Gray)
}
})
}
.width('100%')
}
.height('100%')
}
}
循环渲染 (ForEach)
ForEach接口基于数组类型数据来进行循环渲染,需要与容器组件配合使用,且接口返回的组件应当是允许包含在ForEach父容器组件中的子组件。例如,ListItem组件要求ForEach的父容器组件必须为List组件。
- ForEach键值生成规则
- 使用
ForEach(
xx: Array, //哪个变量循环
itemGenerator: (item: any, index?: number) => void,
keyGenerator?: (item: any, index?: number): string => string //类似vue中的key
)
- eg
@Entry
@Component
struct Loop {
@State arr: string[] = ['zs', 'ls', 'zs', 'ww']
build() {
Row() {
Column() {
Text('ForEach')
.fontSize(50)
.fontWeight(FontWeight.Bold)
Divider()
//不写index,第三个参数返回不唯一,此时,数组的重复元素不会渲染
//不写index,不写第三个参数,此时,数组的重复元素会渲染
//写index,第三个参数返回不唯一,此时,数组的重复元素会渲染
ForEach(this.arr, (item) => {
Text(item).fontSize(30)
}, (item) => {
return item
})
}
.width('100%')
}
.height('100%')
}
}
UIAbility的生命周期
UIAbility组件是一种包含UI界面的应用组件,主要用于和用户交互。
UIAbility组件是系统调度的基本单元,为应用提供绘制界面的窗口;一个UIAbility组件中可以通过多个页面来实现一个功能模块。每一个UIAbility组件实例,都对应于一个最近任务列表中的任务。
UIAbility和WindowSatge的生命周期:
src/main/ets/entryability/EntryAbility.ts
日志eg:
UIAbility的启动模式
注:官网写了三种,实际有四种。
src/main/module.json5
四种启动模式
-
singleton 启动模式 (默认启动模式)
每一个UIAbility只存在唯一实例。
任务列表中只存在一个相同的UIAbility。
启动应用后,退到桌面,再次启动应用,发现并没有创建,只是后台切前台。
-
standard 启动模式 (官网没写)
创建新实例,旧实例也会存在,多实例并存。
每次启动UIAbility都会创建一个新的实例。
任务列表中可能存在一个或多个相同的UIAbility。
启动应用后,退到桌面,再次启动应用,发现创建了新实例,旧实例还在。
-
multiton 启动模式
创建新实例,旧实例不会存在,其他类似standard启动模式。
启动应用后,退到桌面,再次启动应用,发现创建了新实例,旧实例没了。
-
specified 启动模式
每个UIAbility实例可以设置key标识。
启动UIAbility时,需要指定key,存在key相同实例直接被拉起,不存在会创建新实例。
关键代码
页面和自定义组件的生命周期
-
自定义组件和页面的关系
-
自定义组件:@Component装饰的UI单元,可以组合多个系统组件实现UI的复用,可以调用组件的生命周期。
-
页面:即应用的UI页面。可以由一个或者多个自定义组件组成,@Entry装饰的自定义组件为页面的入口组件,即页面的根节点,一个页面有且仅能有一个@Entry。只有被 @Entry装饰 的组件才可以调用页面的生命周期。
-
页面生命周期和组件生命周期
页面生命周期,即被 @Entry装饰的组件生命周期:
-
onPageShow:页面每次显示时触发一次,包括路由过程、应用进入前台等场景,仅@Entry装饰的自定义组件生效。
-
onPageHide:页面每次隐藏时触发一次,包括路由过程、应用进入前后台等场景,仅@Entry装饰的自定义组件生效。
-
onBackPress:当用户点击返回按钮时触发,仅@Entry装饰的自定义组件生效。(指的是手机系统底部带的导航栏返回)
组件生命周期 ,即被 @Component装饰的组件生命周期:
-
aboutToAppear:组件即将出现时回调该接口,具体时机为在创建自定义组件的新实例后,在执行其build()函数之前执行。
-
aboutToDisappear:在自定义组件析构销毁之前执行。不允许在aboutToDisappear函数中改变状态变量,特别是@Link变量的修改可能会导致应用程序行为不稳定。
-
生命周期流程图
- 使用
@Entry
@Component
struct LifeCycleOne {
build() { ... }
// 页面显示----只有被@Entry装饰的组件才可以调用页面的生命周期
onPageShow() { ... }
// 页面隐藏----只有被@Entry装饰的组件才可以调用页面的生命周期
onPageHide() { ... }
// 页面返回----只有被@Entry装饰的组件才可以调用页面的生命周期
onBackPress() { ... }
// 组件即将出现----组件生命周期
aboutToAppear() { ... }
// 组件即将销毁----组件生命周期
aboutToDisappear() { ... }
}
- eg
LifeCycleOne.ets
import router from '@ohos.router'
@Entry
@Component
struct LifeCycleOne {
@State message: string = 'page one'
@State isShow: boolean = true
build() {
Row() {
Column({ space: 10 }) {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
Divider()
Button('显示/隐藏')
.onClick(() => {
this.isShow = !this.isShow
})
if (this.isShow) {
LifeCycleOne_son()
}
}
.onClick(() => {
router.pushUrl({ url: 'pages/LifeCycleTwo' })
})
.width('100%')
}
.height('100%')
}
// 页面显示
onPageShow() {
console.log('page one : onPageShow')
}
// 页面隐藏
onPageHide() {
console.log('page one : onPageHide')
}
// 页面返回
onBackPress() {
console.log('page one : onBackPress')
}
// 组件即将出现
aboutToAppear() {
console.log('LifeCycleOne_____aboutToAppear')
}
// 组件即将销毁
aboutToDisappear() {
console.log('LifeCycleOne_____aboutToDisappear')
}
}
@Component
struct LifeCycleOne_son {
build() {
Column() {
Text('这是子组件').fontSize(25)
}
}
// 组件即将出现
aboutToAppear() {
console.log('LifeCycleOne_son_____aboutToAppear')
}
// 组件即将销毁
aboutToDisappear() {
console.log('LifeCycleOne_son_____aboutToDisappear')
}
}
LifeCycleTwo.ets
import router from '@ohos.router'
@Entry
@Component
struct LifeCycleTwo {
@State message: string = 'page two'
build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.onClick(() => {
router.back()
})
.width('100%')
}
.height('100%')
}
// 页面显示
onPageShow() {
console.log('page two : onPageShow')
}
// 页面隐藏
onPageHide() {
console.log('page two : onPageHide')
}
// 页面显示
onBackPress() {
console.log('page two : onBackPress')
}
}
状态管理
在声明式UI编程框架中,UI是程序状态的运行结果,用户构建了一个UI模型,其中应用的运行时的状态是参数。当参数改变时,UI作为返回结果,也将进行对应的改变。这些运行时的状态变化所带来的UI的重新渲染,在ArkUI中统称为状态管理机制。
装饰器
ArkUI提供了多种装饰器,通过使用这些装饰器,状态变量不仅可以观察在组件内的改变,还可以在不同组件层级间传递,比如父子组件、跨组件层级,也可以观察全局范围内的变化。
-
根据状态变量的影响范围划分:
-
管理组件拥有状态的装饰器:组件级别的状态管理,可以观察组件内变化,和不同组件层级的变化,但需要唯一观察同一个组件树上,即同一个页面内。
-
管理应用拥有状态的装饰器:应用级别的状态管理,可以观察不同页面,甚至不同UIAbility的状态变化,是应用内全局的状态管理。
-
根据数据的传递形式和同步类型层面划分:
-
只读的单向传递
-
可变更的双向传递
状态管理图
Components部分的装饰器为组件级别的状态管理,Application部分为应用的状态管理。
通过@StorageLink/@LocalStorageLink实现应用、组件状态的双向同步。
通过@StorageProp/@LocalStorageProp实现应用和组件状态的单向同步。
- 管理组件拥有的状态,即图中Components级别的状态管理:
组件级别的状态管理,装饰器仅能在页面内,即一个组件树上共享状态变量。
-
@State装饰器:组件内状态
-
@Prop装饰器:父子单向同步
-
@Link装饰器:父子双向同步
-
@Provide装饰器和@Consume装饰器:与后代组件双向同步
-
@Observed装饰器和@ObjectLink装饰器:嵌套类对象属性变化
-
管理应用拥有的状态,即图中Application级别的状态管理:
如果要实现应用级的,或者多个页面的状态数据共享,需要用到应用级别的状态管理。
-
LocalStorage:页面级UI状态存储
-
AppStorage:应用全局的UI状态存储
-
PersistentStorage:持久化存储UI状态
-
Environment:设备环境查询
-
其他状态管理功能
-
@Watch装饰器:状态变量更改通知
-
$$语法:内置组件双向同步
补充
在声明式UI中,是以状态驱动视图更新:
-
状态(State):驱动视图更新的数据(被装饰器标记的变量)
-
视图(View):基于UI描述渲染得到用户界面
通用属性-位置设置Demo
- 效果
- eg
@Entry
@Component
struct PositionFun {
build() {
Column({ space: 10 }) {
Column({ space: 10 }) {
//堆叠容器,子组件按照顺序依次入栈,后一个子组件覆盖前一个子组件
Text('align').fontSize(26).fontColor(0x3E3E3E).width('90%')
Stack() {
Text('first').fontSize(26).fontColor(0x3E3E3E).height(70).backgroundColor(Color.Pink)
Text('second').fontSize(26).fontColor(0x3E3E3E).height(30).backgroundColor(Color.Brown)
}
.width('90%')
.height(100)
.backgroundColor(Color.Grey)
.align(Alignment.Bottom)
}
Column({ space: 10 }) {
//设置元素水平方向的布局
Text('direction').fontSize(26).fontColor(0x3E3E3E).width('90%')
Row() {
Text('1').fontSize(26).fontColor(0x3E3E3E).width('25%').backgroundColor(Color.Blue)
Text('2').fontSize(26).fontColor(0x3E3E3E).width('25%').backgroundColor(Color.Pink)
Text('3').fontSize(26).fontColor(0x3E3E3E).width('25%').backgroundColor(Color.Green)
Text('4').fontSize(26).fontColor(0x3E3E3E).width('25%').backgroundColor(Color.Brown)
}
.width('90%')
.height(100)
.backgroundColor(Color.Grey)
.direction(Direction.Rtl)
}
Column({ space: 10 }) {
//绝对定位,设置元素左上角相对于父容器左上角偏移位置。在布局容器中,
//设置该属性不影响父容器 布局,仅在绘制时进行位置调整。
Text('position').fontSize(26).fontColor(0x3E3E3E).width('90%')
Row() {
Text('1').fontSize(26).fontColor(0x3E3E3E).width('25%').backgroundColor(Color.Blue)
Text('2').fontSize(26).fontColor(0x3E3E3E).width('25%').backgroundColor(Color.Pink)
.position({x:30,y:20})
Text('3').fontSize(26).fontColor(0x3E3E3E).width('25%').backgroundColor(Color.Green)
Text('4').fontSize(26).fontColor(0x3E3E3E).width('25%').backgroundColor(Color.Brown)
.position({x:'70%',y:'70%'})
}
.width('90%')
.height(100)
.border({ width: 1 })
}
Column({ space: 10 }) {
//设置元素在位置定位时的锚点,以元素左上角作为基准点进行偏移
Text('mark').fontSize(26).fontColor(0x3E3E3E).width('90%')
Stack({alignContent:Alignment.TopStart}) {
Row(){
}
.width(100)
.height(100)
.backgroundColor(Color.Orange)
Text('1').fontSize(26).fontColor(0x3E3E3E).width(25).backgroundColor(Color.Blue)
.markAnchor({x:25,y:10})
Text('2').fontSize(26).fontColor(0x3E3E3E).width(25).backgroundColor(Color.Pink)
.markAnchor({x:-100,y:-50})
Text('3').fontSize(26).fontColor(0x3E3E3E).width(25).backgroundColor(Color.Green)
.markAnchor({x:25,y:-60})
}
}
Column({ space: 10 }) {
//相对定位,设置元素相对于自身的偏移量。
Text('offset').fontSize(26).fontColor(0x3E3E3E).width('90%')
Row() {
Text('1').fontSize(26).fontColor(0x3E3E3E).width('25%').backgroundColor(Color.Blue)
Text('2').fontSize(26).fontColor(0x3E3E3E).width('25%').backgroundColor(Color.Pink)
.offset({x:30,y:20})
Text('3').fontSize(26).fontColor(0x3E3E3E).width('25%').backgroundColor(Color.Green)
Text('4').fontSize(26).fontColor(0x3E3E3E).width('25%').backgroundColor(Color.Brown)
.offset({x:'-40%',y:-20})
}
.width('90%')
.height(100)
.border({ width: 1 })
}
}
}
}
加载图片
Image加载网络图片 (暂时未显示出,官方说是支持RFC 9133标准的https网络图片才行)
官方提示需申请权限
- src/main/module.json5 添加配置
"requestPermissions":[
{
"name": "ohos.permission.INTERNET",
},
]
- xx.ets
Column() {
Image("https://encrypted-tbn0.gstatic.com/images? q=tbn:ANd9GcTzuRxGGU1FXQ623L953wBBVD9FIlrgpSIAgQ&usqp=CAU")
}
Image加载media图片
media图片无后缀名
- 目录
- 关键代码
Image($r('app.media.fly'))
.width('200vp')
Image加载rawfile图片
rawfile图片有后缀名
- 目录
- 关键代码
Image($rawfile('fly2.png'))
.width('200vp')
Web组件
Web组件用来加载网页: 前提必须申请网络权限, 不支持预览器查看!!
- src/main/ets/entryability/EntryAbility.ts 的onWindowStageCreate方法中修改路由,模拟器打开即可显示
- src/main/module.json5 中添加网络权限
"requestPermissions": [
{
"name": 'ohos.permission.INTERNET'
}
]
- eg
/*
* Web组件用来加载网页: 前提必须申请网络权限,不支持预览器查看!!
* src:''
* 控制器
* */
import webview from '@ohos.web.webview'
@Entry
@Component
struct WebComp {
// 准备web组件的控制器
myController: WebviewController = new webview.WebviewController()
build() {
Row() {
Column() {
Web({
src: 'https://www.csdn.net/',
controller: this.myController
})
}
.width('100%')
}
.height('100%')
}
}
HTTP数据请求
应用通过HTTP发起一个数据请求,支持常见的GET、POST、OPTIONS、HEAD、PUT、DELETE、TRACE、CONNECT方法。(前提必须申请网络权限)
注意:每一个httpRequest对应一个http请求任务,不可复用
使用
导入http模块
import http from '@ohos.net.http'
使用http模块发送请求,处理响应
// 1.创建http的请求对象,不可复用
let httpRequest = http.createHttp()
// 2.发送请求
httpRequest.request(
`${this.baseURL}/shop?pageNo=${this.pageNo}&pageSize=3`, // 请求URL路径
{ // 请求选项 HttpRequestOptions
method: http.RequestMethod.GET,
extraData:{'param1':'value1'} // k1=v1&k2=v2
}
) // Promise:存放未来会完成的结果
// 处理响应结果
.then((resp:http.HttpResponse) => {
if (resp.responseCode === 200) {
// 请求成功
}
})
.catch((error:Error) => {
// 请求失败
})
附:
HttpRequestOptions
| 名称 | 类型 | 描述 |
|---|---|---|
| method | RequestMethod | 请求方式,GET、POST、PUT、DELETE等 |
| extraData | string | Object | 请求参数 |
| header | Object | 请求头字段 |
| connectTimeout | number | 连接超时时间,单位毫秒,默认60000ms |
| readTimeout | number | 读取超时时间,同上 |
HttpResponse
| 名称 | 类型 | 描述 |
|---|---|---|
| responseCode | ResponseCode | 响应状态码 |
| header | Object | 响应头 |
| cookies | string | 响应返回的cookies |
| result | string | Object | 响应体,默认是JSON字符串 |
| resultType | HttpDataType | 返回值类型 |
promise避免回调地狱
// 1、 导入http模块
import http from '@ohos.net.http'
@Entry
@Component
struct HttpReq {
@State message: string = 'Hello World'
aboutToAppear() {
// 2、 创建http请求对象
let httpReq = http.createHttp()
// 3、 发起请求
let promise = httpReq.request('https://api.apiopen.top/api/sentences')
// 根据接口数据再发请求 加then方法使用
promise.then(data => {
this.message = JSON.parse(`${data.result}`).result.name
})
}
build() {
Row() {
Column() {
Text(this.message)
.fontSize(20)
.fontWeight(FontWeight.Bold)
}
.width('100%')
}
.height('100%')
}
}
eg
src/main/ets/model/ShopModel.ts
import ShopInfo from '../viewmodel/ShopInfo'
import http from '@ohos.net.http'
class ShopModel {
baseURL: string = 'http://localhost:3000'
pageNo: number = 1
getShopList(): Promise<ShopInfo[]> {
return new Promise((resolve, reject) => {
// 1.创建http的请求对象
let httpRequest = http.createHttp()
// 2.发送请求
httpRequest.request(
`${this.baseURL}/shop?pageNo=${this.pageNo}&pageSize=3`,
{
method: http.RequestMethod.GET
}
)
.then(resp => {
if (resp.responseCode === 200) {
// 查询成功
console.log('查询成功', resp.result)
resolve(JSON.parse(resp.result.toString()))
} else {
console.log('查询失败', JSON.stringify(resp))
reject('查询失败')
}
})
.catch(error => {
console.log('查询失败', JSON.stringify(error))
reject('查询失败')
})
})
}
}
src/main/ets/pages/ShopPage.ets
import ShopModel from '../model/ShopModel'
@Entry
@Component
struct ShopPage {
@State shops: object = []
build() {
Row() {
Column() {
Text('获取数据')
.onClick(() => {
// 加载数据
ShopModel.getShopList()
.then(shops => {
this.shops = shops
})
})
}
.width('100%')
}
.height('100%')
}
}
第三方库axios
1.下载安装ohpm
OHPM CLI 作为鸿蒙生态三方库的包管理工具,支持OpenHarmony共享包的发布、安装和依赖管理。
-
解压文件,执行初始化 (window为例)
压缩包解压到文件夹中
在command-line-tools\ohpm\bin目录下执行cmd打开命令窗口
执行init.bat 命令进行初始化
然后执行ohpm -v 检测该目录下是否安装成功
-
ohpm配置到环境变量中
为了任何目录下都能访问到ohpm,需要配置到环境变量中
此电脑 > 属性 > 高级系统设置 > 高级 > 环境变量
新建用户变量
点击用户变量的Path 编辑新建
-
安装成功,检测环境
任意文件夹cmd打开命令窗口,执行ohpm -v 显示版本号即安装成功
2.下载安装axios
-
下载axios 打开项目终端,执行ohpm install @ohos/axios
(PS:如果提示安装失败,是因为未识别ohpm,重启项目就好啦)
oh-package.json5文件中可以看到安装依赖
打开显示排除文件,可以看到刚刚安装的axios
-
开放网络权限
打开src/main/module.json5文件,添加网络权限
3.使用axios
- 使用
导入axios
import axios from '@ohos/axios'
发送请求并处理响应
axios.get( // 请求方式,不同方式使用不同方法
`${this.baseURL}/shops`, // 请求路径
{
params: { pageNo: this.pageNo, pageSize: 3 } //请求选项
data: {'params1':'value1'}
}
)
.then(resp => { // 响应结果,AxiosResponse
if (resp.status === 200) {
// 查询成功
}else{
// 查询失败
}
})
.catch(error => {
// 查询失败
})
附:
| 名称 | 类型 | 描述 |
|---|---|---|
| status | number | 响应状态码 |
| headers | Object | 响应头 |
| data | any | 服务端返回的响应体 |
- eg
src/main/ets/model/ShopModelAxios.ts
import ShopInfo from '../viewmodel/ShopInfo'
import axios from '@ohos/axios'
class ShopModel {
baseURL: string = 'http://localhost:3000'
pageNo: number = 1
getShopList(): Promise<ShopInfo[]> {
return new Promise((resolve, reject) => {
axios.get(
`${this.baseURL}/shops`,
{
params: { pageNo: this.pageNo, pageSize: 3 }
}
)
.then(resp => {
if (resp.status === 200) {
// 查询成功
console.log('查询成功', JSON.stringify(resp.data))
resolve(resp.data)
} else {
console.log('查询失败', JSON.stringify(resp))
reject('查询失败')
}
})
.catch(error => {
console.log('查询失败', JSON.stringify(error))
reject('查询失败')
})
})
}
}
应用的数据共享
此方法,预览器显示undefined,真机可正常显示。推测,预览器只运行当前的ets一个文件,并没有从App创建过来,所有没数据。
- EntryAbility.ts
import UIAbility from '@ohos.app.ability.UIAbility';
import hilog from '@ohos.hilog';
import window from '@ohos.window';
// 应用程序之间数据共享
AppStorage.SetOrCreate('appName', '金木')
export default class EntryAbility extends UIAbility { ... }
- xxx.ets
// 从应用对象身上取出数据
const appName: string = AppStorage.Get('appName')
@Entry
@Component
struct test {
build() {
Row() {
Column({ space: 15 }) {
Text(appName + 'qwe').fontSize(30) //预览器undefined 真机正常显示
}
.width('100%')
}
.height('100%')
}
}
应用数据持久化
应用数据持久化,是指应用将内存中的数据通过文件或数据库的形式保存到设备上。内存中的数据形态通常是任意的数据结构或数据对象,存储介质上的数据形态可能是文本、数据库、二进制文件等。
用户首选项
用户首选项(Preference)为应用提供key-Value键值型的数据处理能力,支持应用持久化轻量级数据。
preferences必须使用模拟器,预览器不可以!!
使用
- 导入首选项模块
import preferences from '@ohos.data.preferences';
- 获取首选项实例,读取指定文件
//preferences.getPreferences(UIAbilityContent Preferences实例名称)
preferences.getPreferences(this.context,'MyAppPreferences')
.then(preferences=>{
// 获取成功
})
.catch(reason=>{
// 获取失败
})
- 数据操作
// 写入数据,如果已经存在则会覆盖,可利用.has()判断是否存在
preferences.put('key',val)
.then(()=>preferences.flush()) // 刷到磁盘
.catch(reson=>{}) // 处理异常
// 查询数据
preferences.get('key','defaultValue')
.then(()=>console.log('查询成功')))
.catch(reason=>console.log('查询失败')))
// 删除数据
preferences.delete('key')
.then(()=>{})
.catch(reason=>{})
限制
-
Key为string类型,要求非空且长度不超过80字节
-
Value可以使string、number、boolean及以上类型数组,大小不超过8192字节
-
数据量建议不超过一万条
eg
- 封装preferences
src/main/ets/common/util/PreferencesUtil.ts
import preferences from '@ohos.data.preferences';
class PreferencesUtil {
prefMap: Map<string, preferences.Preferences> = new Map()
// 加载 最佳的加载是在项目启动的时候加载 也就是ability的onCreate
async loadPreference(context, name: string) {
try { // 加载preferences
let pref = await preferences.getPreferences(context, name)
this.prefMap.set(name, pref)
console.log('testTag', `加载Preferences[${name}]成功`)
} catch (e) {
console.log('testTag', `加载Preferences[${name}]失败`, JSON.stringify(e))
}
}
// 新增
async putPreferenceValue(name: string, key: string, value: preferences.ValueType) {
if (!this.prefMap.has(name)) {
console.log('testTag', `Preferences[${name}]尚未初始化`)
return
}
try {
let pref = this.prefMap.get(name)
// 写入数据
await pref.put(key, value)
// 刷盘
await pref.flush()
console.log('testTag', `保存Preferences[${name}.${key}=${value}]成功`)
} catch (e) {
console.log('testTag', `保存Preferences[${name}.${key}=${value}]失败`, JSON.stringify(e))
}
}
// 读取
async getPreferenceValue(name: string, key: string, defaultValue: preferences.ValueType) {
if (!this.prefMap.has(name)) {
console.log('testTag', `Preferences[${name}]尚未初始化`)
return
}
try {
let pref = this.prefMap.get(name)
// 读取数据
let value = await pref.get(key, defaultValue)
console.log('testTag', `读取Preferences[${name}.${key}=${value}]成功`)
return value
} catch (e) {
console.log('testTag', `读取Preferences[${name}.${key}]失败`, JSON.stringify(e))
}
}
}
const preferencesUtil = new PreferencesUtil()
export default preferencesUtil as PreferencesUtil
- 加载preferences
src/main/ets/entryability/EntryAbility.ts
import PreferencesUtil from '../common/util/PreferencesUtil'
export default class EntryAbility extends UIAbility {
async onCreate(want, launchParam) {
// 加载Preferences
await PreferencesUtil.loadPreference(this.context, 'MyPreferences')
// 可加载多个 eg
// await PreferencesUtil.loadPreference(this.context, 'xxx')
}
}
- 使用preferences
src/main/ets/pages/Index.ets
import PreferencesUtils from '../common/util/PreferencesUtil'
@Entry
@Component
struct Index {
@Provide fontSize: number = 16
async aboutToAppear() {
this.fontSize = await PreferencesUtils.getPreferenceValue('MyPreferences', 'IndexFontSize', 16) as unknown as number
}
build() {
Row() {
Column() {
Text('用户首选项数据持久化')
.fontSize(this.fontSize)
.onClick(() => {
this.fontSize++
// 写入Preferences
// 模拟器打开,点击字体变大,退桌面,销毁实例
// 重新打开应用,新建实例,字体还是刚刚设置的大小
PreferencesUtils.putPreferenceValue('MyPreferences', 'IndexFontSize', this.fontSize)
})
}
.width('100%')
}
.height('100%')
}
}
关系型数据库
关系型数据库(RDB)是基于SQLite组件提供的本地数据库,用于管理应用中的结构化管理。例如:记账本、备忘录。
必须使用模拟器,预览器不可以!!
使用
当应用完成查询数据操作,不再使用结果集(ResultSet)时,请及时调用close方法关闭结果集,释放系统为其分配的内存。
- 初始化数据库
- 导入关系型数据库模块
import relationalStore from '@ohos.data.relationalStore';
- 初始化数据库表
// 1.rdb配置
const config = {
name: 'MyApplication.db', // 数据库文件名
securityLevel: relationalStore.SecurityLevel.S1 // 数据库安全级别
}
// 2.初始化SQL语句 (项目启动创建表)
const sql = `CREATE TABLE IF NOT EXISTS TASK (
ID INTEGER PRIMARY KEY AUTOINCREMENT,
NAME TEXT NOT NULL,
FINISHED bit
)`
// 3.获取rdb
relationalStore.getRdbStore(this.context, config, (err, rdbStore) => {
// 执行Sql 后续的增删改查都是使用的rdbStore对象
rdbStore.executeSql(sql)
})
- 增、删、改数据
- 新增数据
// 1.准备数据
let task = {id: 1, name: '任务1', finished: false}
// 1.新增
this.rdbStore.insert(this.tableName, task)
- 修改数据
// 1.要更新的数据
let data = { 'finished': true }
// 2.更新的条件
let predicates = new relationalStore.RdbPredicates(this.tableName)
predicates.equalTo('ID', id)
// 3.更新操作
this.rdbStore.update(data, predicates)
- 删除数据
// 1.删除的条件
let predicates = new relationalStore.RdbPredicates(this.tableName)
predicates.equalTo('ID', id)
// 3.更新操作
this.rdbStore.delete(predicates)
- 查询数据
- 查询数据
// 1.构建查询条件
let predicates = new relationalStore.RdbPredicates(this.tableName)
// 2.查询
let result = await this.rdbStore.query(predicates, ['ID', 'NAME', 'FINISHED'])
- 解析结果
// 1. 准备数组保存结果
let tasks: TaskInfo[] = []
// 2. 循环遍历结果集,判断是否遍历到最后一行
while (!result.isAtLastRow) {
// 指定移动到下一行 (指针,默认指向-1,数据从0开始有)
result.goToNextRow()
// 根据字段名获取字段index,从而获取字段值
let id = result.getLong(result.getColumnIndex('ID'))
let name = result.getString(result.getColumnIndex('NAME'))
tasks.push({ id, name })
}
限制
-
系统默认日志方式是WAL(Write Ahead Log)模式,系统默认落盘方式是FULL模式。
-
数据库中连接池的最大数量是4个,用以管理用户的读操作。
-
为保证数据的准确性,数据库同一时间只能支持一个写操作。
-
当应用被卸载完成后,设备上的相关数据库文件及临时文件会被自动清除。
eg
- 封装关系型数据库
src/main/ets/model/TaskModel.ets
/*关系型数据库*/
import relationalStore from '@ohos.data.relationalStore';
// ts文件不能导入ets,所以TaskModel需定义为ets文件
import TaskInfo from '../viewmodel/TaskInfo'
class TaskModel {
private rdbStore: relationalStore.RdbStore
private tableName: string = 'TASK'
/*
* 初始化任务表
* */
initTaskDB(context) {
// 1.rdb配置
const config = {
name: 'MyApplication.db', // 数据库文件名
securityLevel: relationalStore.SecurityLevel.S1 // 数据库安全级别
}
// 2.初始化SQL语句 (项目启动创建表)
const sql = `CREATE TABLE IF NOT EXISTS TASK (
ID INTEGER PRIMARY KEY AUTOINCREMENT,
NAME TEXT NOT NULL,
FINISHED bit
)`
// 3.获取rdb
relationalStore.getRdbStore(context, config, (err, rdbStore) => {
if (err) {
console.log('testTag', '获取rdbStore失败')
return
}
// 执行Sql
rdbStore.executeSql(sql) // 创建数据表
console.log('testTag', '创建task表成功')
// 保存rdbStore
this.rdbStore = rdbStore
})
}
/*
* 查询任务列表
* */
async getTaskList() {
// 1.构建查询条件
let predicates = new relationalStore.RdbPredicates(this.tableName)
// 2.查询
let result = await this.rdbStore.query(predicates, ['ID', 'NAME', 'FINISHED'])
// 3.解析查询结果
// 3.1 定义数组,组装最终的查询结果
let tasks: TaskInfo[] = []
// 3.2 遍历封装
while (!result.isAtLastRow) {
// 3.3 指定移动到下一行 (指针,默认指向-1,数据从0开始有)
result.goToNextRow()
// 3.4 获取数据
let id = result.getLong(result.getColumnIndex('ID'))
let name = result.getString(result.getColumnIndex('NAME'))
let finished = result.getLong(result.getColumnIndex('FINISHED'))
// 3.5 封装到数组
tasks.push({ id, name, finished: !!finished })
}
console.log('testTag', '查询到数据:', JSON.stringify(tasks))
return tasks
}
/*
* 添加新任务
* @param name 任务名称
* @return 任务id
* */
addTask(name: string): Promise<number> {
return this.rdbStore.insert(this.tableName, { name, finished: false }) // 返回Id
}
/*
* 根据id更新任务状态
* @params id 任务id
* @param finished 任务是否完成
* */
updateTaskStatus(id: number, finished: boolean) {
// 1.要更新的数据
let data = { finished }
// 2.更新的条件
let predicates = new relationalStore.RdbPredicates(this.tableName)
predicates.equalTo('ID', id)
// 3.更新操作
return this.rdbStore.update(data, predicates)
}
/*
* 根据id删除任务
* @param id 任务id
* */
deleteTaskById(id: number) {
// 1.删除的条件
let predicates = new relationalStore.RdbPredicates(this.tableName)
predicates.equalTo('ID', id)
// 3.更新操作
return this.rdbStore.delete(predicates)
}
}
let taskModel = new TaskModel()
export default taskModel as TaskModel
- 加载关系型数据库
src/main/ets/entryability/EntryAbility.ets
// 由于ts文件不能引入ets,这里EntryAbility.ts需改成ets文件
import TaskModel from '../model/TaskModel'
export default class EntryAbility extends UIAbility {
async onCreate(want, launchParam) {
// 初始化任务表
TaskModel.initTaskDB(this.context)
}
}
- 使用关系型数据库
src/main/ets/pages/TaskListRDBDemo.ets
import TaskModel from '../model/TaskModel'
import TaskInfo from '../viewmodel/TaskInfo'
@Entry
@Component
struct TaskListRDBDemo {
// 任务数组
@State tasks: TaskInfo[] = []
aboutToAppear() {
// 查询任务列表
TaskModel.getTaskList()
.then(tasks => {
this.tasks = tasks
})
}
handleAddTask(name: string) {
// 新增任务
TaskModel.addTask(name)
.then(id => {
console.log('testTag', '处理新增任务:', name)
// 回显到数组页面
this.tasks.push(new TaskInfo(id, name))
})
.catch(error => {
console.log('testTag', '新增任务失败:', name, JSON.stringify(error))
})
}
@Builder DeleteButton(index: number, id: number) {
Button('删除')
.onClick(() => {
// 删除任务
TaskModel.deleteTaskById(id)
.then(() => {
this.tasks.splice(index, 1)
console.log('testTag', `删除任务,index:${index}`)
})
.catch(error => {
console.log('testTag', '删除任务失败,id:', id)
})
})
}
build() {
}
}
// 任务类
@Observed
class Task {
// 静态变量,类内部所有对象共享的变量
static id: number = 1
// 任务名称
name: string = `任务${Task.id++}`
// 任务状态
finished: boolean = false
}
@Component
struct TaskItem {
@ObjectLink item: Task
build() {
Column() {
Checkbox()
.onChange(async val => {
// 更新任务
TaskModel.updateTaskStatus(this.item.id, val)
.then(() => {
console.log('testTag', '更新任务成功')
})
.catch(error => {
console.log('testTag', '更新任务失败,id:', this.item.id, JSON.stringify(error))
})
})
}
}
}
通知
基础通知
应用可以使用通知接口发送通知消息,提醒用户关注应用中的变化,用户可以在通知栏查看和操作通知内容。
使用
- 导入nitification模块
import notificationManager from '@ohos.notificationManager'
- 发布通知
// 1.构建通知请求
let request: notificationManager.NotificationRequest = {
id: 10,
content: { // 通知内容}
}
// 2.发布通知
notificationManager.publish(request)
.then(()=>console.log('发送通知成功'))
- 取消通知
进度条通知
通知行为意图
页面路由
页面路由指在应用程序中实现不同页面之间的跳转和数据传递。
页面栈的最大容量为32个页面。如果超过这个限制,可以调用router.clear()方法清空历史页面栈,释放内存空间。
-
Router模块提供了两种跳转模式:
-
router.pushUrl():目标页不会替换当前页,而是压入页面栈。这样可以保留当前页的状态,并且可以通过返回键或者调用router.back()方法返回到当前页。
-
router.replaceUrl():目标页会替换当前页,并销毁当前页。这样可以释放当前页的资源,并且无法返回到当前页。
-
Router模块提供了两种实例模式:
-
Standard:标准实例模式,也是默认情况下的实例模式。每次调用该方法都会新建一个目标页,并压入栈顶。
-
Single:单实例模式。即如果目标页的url在页面栈中已经存在同url页面,则离栈顶最近的同url页面会被移动到栈顶,并重新加载;如果目标页的url在页面栈中不存在同url页面,则按照标准模式跳转。
PS:
需导入router (使用时输入router 会自动导入哦)
import router from '@ohos.router'
页面跳转
声明式开发范式(ArkTS)
eg: Model :Strage ,Langulage: ArkTS 项目, One页面跳转Two页面
- 路由
src/main/resources/base/profile/main_pages.json 项目中的所有路由,直接粘贴使用。
- 目录结构
- 关键代码
- One.ets
Button('点我跳转到Two')
.onClick(()=>{
// 跳转
router.pushUrl({
url:'pages/Two'
})
})
- Two.ets
Text('返回')
.onClick(()=>{
router.back()
})
- 补充
| API | 描述 |
|---|---|
| router.pushUrl | 跳转到应用内的指定页面。 |
| router.replaceUrl | 用应用内的某个页面替换当前页面,并销毁被替换的页面。 |
| router.back | 返回上一页面或指定的页面 |
| router.clear | 清空页面栈中的所有历史页面,仅保留当前页面作为栈顶页面。 |
| router.getLength | 获取当前在页面栈内的页面数量。 |
| router.getState | 获取当前页面的状态信息。 |
| router.showAlertBeforeBackPage | 开启页面返回询问对话框。 |
| router.hideAlertBeforeBackPage | 禁用页面返回询问对话框。 |
| router.getParams | 获取发起跳转的页面往当前页传入的参数。 |
类Web开发范式(JS)
PS : Model 为 Stage,仅支持 ArkTS。Model 为 FA,支持 ArkTS 和 JS。
eg: Model :FA ,Langulage: JS 项目, one页面跳转two页面
- 路由 src/main/config.json 项目中的所有路由,直接粘贴使用。
- 目录结构
- 关键代码
- one.html
<button @click="goTwo">点我跳转到Two</button>
- one.js
// 跳转函数
goTwo(){
// 官方推荐的router.pushUrl跳转不了,点击无反应
// router.pushUrl({
// url:'pages/two/two'
// })
router.push({
url:'pages/two/two'
})
}
- two.html
<text @click="back">返回</text>
- two.js
back(){
router.back()
}
低代码式
打开低代码设置
eg: One页面跳转Two页面
- 布局介绍
- 新建页面
- 路由
src/main/resources/base/profile/main_pages.json 项目中的所有路由,直接粘贴使用。
4. 目录结构
- 关键代码
build(){} 是做布局的,低代码拖拽已实现,故不用写build。
- one.ets
goTwo(){
router.pushUrl({ url: 'pages/Two' })
}
- one.visual
- two.ets
back(){
router.back()
}
- two.visual
路由传参
这种方式不仅可以返回到指定页面,还可以在返回的同时传递自定义参数信息。这些参数信息可以在目标页中通过调用 router.getParams() 方法进行获取和解析。
使用
- 跳转指定路径,并传递参数
router.pushUrl(
{
url: 'pages/Detail', // 目标url
params:{id:1} //传递的参数(可选)
},
router.RouterMode.Single, //页面模式(标准Standard的话,可省略不写)
err => {
if (err) {
console.error(`路由失败,code: ${err.code} message:${err.message}`);
/*
* 异常响应 错误码:
* 100001:内部错误,可能是渲染失败
* 100002:路由地址错误
* 100003:路由栈中页面超过32
* */
}
})
- 获取传递的参数
const params = router.getParams(); // 获取传递过来的参数对象
const id = params['id']; // 获取id属性的值
const age = params['info'].age; // 获取age属性的值
- 返回上一页
router.back()
- 返回指定页,并携带参数
router.back(
{
url: 'pages/Index',
params: {id: 10}
}
)
eg
eg: One->Two
- One.ets
router.pushUrl({ url: 'pages/Two', params: { name: 'zs', age: '18' } })
- Two.etx
import router from '@ohos.router'
const name = router.getParams()['name'] as string
@Entry
@Component
struct Two {
build() {
Row() {
Column() {
Text(name).fontSize(30).width('100%')
}
.height('100%')
}
}
}