收集官网faq:文档中心
一. ArkTS&ArkUI
1. 基础理论
1. 鸿蒙相关的生命周期都有哪些?
UIAbility生命周期:onCreate、onWindowStageCreate、onForeground、onBackground、onWindowStageDestroy、onDestroy。
-
onCreate:Create状态为在应用加载过程中,UIAbility实例创建完成时触发,系统会调用onCreate()回调。可以在该回调中进行页面初始化操作,例如变量定义资源加载等,用于后续的UI展示。
-
onWindowStageCreate():UIAbility实例创建完成之后,在进入Foreground之前,系统会创建一个WindowStage。WindowStage创建完成后会进入onWindowStageCreate()回调,可以在该回调中设置UI加载、设置WindowStage的事件订阅。事件订阅代码
-
onForegound():在UIAbility的UI可见之前,如UIAbility切换至前台时触发。可以在onForeground()回调中申请系统需要的资源,或者重新申请在onBackground()中释放的资源。
-
onWindowStageDestory():在UIAbility实例销毁之前,则会先进入onWindowStageDestroy()回调,可以在该回调中释放UI资源。
-
onBackground():在UIAbility的UI完全不可见之后,如UIAbility切换至后台时候触发。可以在onBackground()回调中释放UI不可见时无用的资源,或者在此回调中执行较为耗时的操作,例如状态保存等。
-
onWindowStageDestory():在UIAbility实例销毁之前,则会先进入onWindowStageDestroy()回调,可以在该回调中释放UI资源。
-
onDestroy():Destroy状态在UIAbility实例销毁时触发。可以在onDestroy()回调中进行系统资源的释放、数据的保存等操作。
页面生命周期:onPageShow、onPageHide、onBackPress。
-
页面生命周期,说白了就是@Entry修饰的组件,才称之为页面。
-
onPageShow:页面每次显示时触发一次,包括路由过程、应用进入前台等场景,仅@Entry装饰的自定义组件生效。
-
onPageHide:页面每次隐藏时触发一次,包括路由过程、应用进入后台等场景,仅@Entry装饰的自定义组件生效。
-
onBackPress:当用户点击返回按钮时触发,仅@Entry装饰的自定义组件生效。
组件生命周期:aboutToAppear(发起网络请求)、aboutToDisappear。
-
aboutToAppear:在创建自定义组件的新实例后,在执行其build()函数之前执行。允许在aboutToAppear函数中改变状态变量,更改将在后续执行build()函数中生效。
-
aboutToDisappear:函数在自定义组件销毁之前执行。不允许在aboutToDisappear函数中改变状态变量,特别是@Link变量的修改可能会导致应用程序行为不稳定。
按返回键页面执行生命周期方法:
-
打开第一个页面:
-
Index:aboutToAppear
-
Index:onPageShow
-
-
跳转第二个页面:
-
Index:onPageHide
-
Second:aboutToAppear
-
Second:onPageShow
-
-
点击back: 如果是在第二个页面跳转到第一个页面:
-
Second:onBackPress Second:onPageHide
-
Second:onPageHide Index:aboutToAppear
-
Index:onPageShow Index:onPageShow
-
Second:aboutToDisappear
-
返回页面不走aboutToAppear:
-
aboutToAppear函数在创建自定义组件的新实例后,在执行其build()函数之前执行。
-
返回页面时==不需要走重新创建==,不会执行aboutToAppear,只会执行onPageShow。
aboutToAppear和onAppear的区别?
-
aboutToAppear:是组件的生命周期方法,当组件实例创建后,执行build函数之前执行aboutToAppear
-
onAppear:是组件的属性方法,在该组件显示时触发此回调
Text() .onAppear(()=>{}
2. ArkUI的两大开发范式是什么,区别是什么 ***
-
ArkUI推荐使用声明式开发范式 , 其他的框架有参考类Web开发范式
-
类Web开发范式:采用经典的HML、CSS、JavaScript三段式开发方式,即使用HML标签文件搭建布局、使用CSS文件描述样式、使用JavaScript文件处理逻辑。该范式更符合于Web前端开发者的使用习惯,便于快速将已有的Web应用改造成方舟UI框架应用。
-
声明式开发范式:采用基于TypeScript声明式UI语法扩展而来的ArkTS语言,从组件、动画和状态管理三个维度提供UI绘制能力。
-
==延伸问题==:有听过命令式编程么,命令式编程与声明式编程的区别是什么?
-
其实这里的命令式编程,就相当于是类Web开发范式
-
-
我们来对比一下命令式和声明式
-
声明式开发范式 : 只需要描述/声明 , 你要做什么 (通过封装好的组件以及相关熟悉方法 , 快速实现目的)
-
命令式开发范式 : 不仅需要知道做什么 , 更需要知道如何做 (需要通过最原始的方式 , 一步一步实现)
-
左侧是纯前端实现的按钮点击 , 也就是命令式
-
命令式需要自己一点一点实现 , 如何做必须清楚
-
-
右侧是ArkTs实现的按钮点击 , 也就是声明式
-
声明式只需要知道做什么 , 调用对应api即可 , 不需要知道内部如何实现
-
3. 项目使用的是harmoneyos还是openharmoney,区别是啥
我们公司的项目使用的是HarmonyOS。
HarmonyOS:OpenHarmony+闭源应用和华为移动服务HMS(比如应用市场,视频,音乐等app)
Andoird:aosp(android open source project) + GMS(Google Mobile Service)
3. 关于context相关得内容有哪些 , 他们的区别?
各类Context的继承关系:
使用:
-
page中获取上下文:通过函数getContext获取
import common from '@ohos.app.ability.common'; private context:common.UIAbilityContext = getContext(this) as common.UIAbilityContext
-
Ability中获取上下文: 直接通过this获取context
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { let uiAbilityContext = this.context; ... }
理论:
基类Context提供了获取应用文件路径的能力,ApplicationContext、AbilityStageContext、UIAbilityContext和ExtensionContext均继承该能力。应用文件路径属于应用沙箱路径,具体请参见应用沙箱目录。
上述各类Context获取的应用文件路径有所不同。
通过ApplicationContext==获取应用级别的应用文件路径==,此路径是应用全局信息推荐的存放路径,这些文件会跟随应用的卸载而删除。
import common from '@ohos.app.ability.common'; @Entry @Component struct Page_Context { private context = getContext(this) as common.UIAbilityContext; build() { ... Button() .onClick(() => { let applicationContext = this.context.getApplicationContext(); let cacheDir = applicationContext.cacheDir;//<路径前缀>/<加密等级>/base/cache let tempDir = applicationContext.tempDir; let filesDir = applicationContext.filesDir; let databaseDir = applicationContext.databaseDir; let bundleCodeDir = applicationContext.bundleCodeDir; let distributedFilesDir = applicationContext.distributedFilesDir; let preferencesDir = applicationContext.preferencesDir; // 获取应用文件路径 let filePath = tempDir + 'test.txt'; hilog.info(DOMAIN_NUMBER, TAG, `filePath: ${filePath}`); if (filePath !== null) { promptAction.showToast({ message: filePath }); } }) } }
通过AbilityStageContext、UIAbilityContext、ExtensionContext==获取HAP级别的应用文件路径==。此路径是HAP相关信息推荐的存放路径,这些文件会跟随HAP的卸载而删除,但不会影响应用级别路径的文件,除非该应用的HAP已全部卸载。
import common from '@ohos.app.ability.common'; @Entry @Component struct Page_Context { private context = getContext(this) as common.UIAbilityContext; build() { ... Button() .onClick(() => { let cacheDir = this.context.cacheDir;//<路径前缀>/<加密等级>/base/haps/<module-name>/cache let tempDir = this.context.tempDir; let filesDir = this.context.filesDir; let databaseDir = this.context.databaseDir; let bundleCodeDir = this.context.bundleCodeDir; let distributedFilesDir = this.context.distributedFilesDir; let preferencesDir = this.context.preferencesDir; // 获取应用文件路径 let filePath = tempDir + 'test.txt'; }) } }
4. arkts中哪些类不能被继承, 面试官关注点是组件是否可以继承?(组件是否可以被继承)
组件不能被继承,被@compent修饰的自定义组件不能被继承,只能引用,或者对外暴露方法。
官网解释:
-
struct:自定义组件基于struct实现,struct + 自定义组件名 + {...}的组合构成自定义组件,==不能有继承关系==。对于struct的实例化,可以省略new。
-
@Component:@Component装饰器仅能装饰struct关键字声明的数据结构。==struct被@Component装饰后具备组件化的能力==,需要实现build方法描述UI,一个struct只能被一个@Component装饰。@Component可以接受一个可选的bool类型参数。
@Component struct MyComponent { }
5.介绍Stage模型和FA模型
-
Stage模型 : HarmonyOS 3.1推出 也就是API9 , 是目前==主推==且会长期演进的模型
-
由于提供了AbilityStage、WindowStage等类作为应用组件和Window窗口的“舞台”,因此称这种应用模型为Stage模型
-
stage: 舞台 /steɪdʒ/
-
-
FA模型: FA(Feature Ability)模型:HarmonyOS早期版本开始支持的模型,已经不再主推
-
feature: 特点 /ˈfiːtʃə(r)/
-
-
区别: Stage模型与FA模型最大的区别在于
-
Stage模型中,多个应用组件共享同一个ArkTS引擎实例;
-
而FA模型中,每个应用组件独享一个ArkTS引擎实例。
-
因此在Stage模型中,应用组件之间可以方便的共享对象和状态,同时减少复杂应用运行对内存的占用。
-
Stage模型作为主推的应用模型,开发者通过它能够更加便利地开发出分布式场景下的复杂应用。
-
2. 装饰器
1. 你使用过哪些装饰器,分别阐述一下他们得作用 ***
-
@State装饰器,使得变量变为状态变量,影响UI(数据变化,UI变化)
-
@Prop装饰的变量和父组件建立单向的同步关系:
-
父组件的@State数据变化,会同步到子组件@Prop
-
具体用法
//父组件:Parent @State num:number = 0 build(){ Son({num:this.num}) } //子组件:Son @Prop num:number
-
@Prop修饰的变量,api9不能初始化,api11能初始化
-
-
@Link装饰的变量与其父组件中的数据源共享相同的值。
-
父组件的@State数据变化,会同步到子组件@Link数据
-
子组件@link数据变化,会同步到父组件@State数据
-
具体用法
//父组件:Parent @State num:number = 0 build(){ Son({num:$num})//api9必须使用$,api11开始也可以使用this了 } //子组件:Son @Link num:number
-
@Link修饰的变量,api9不能初始化,api11不能初始化
-
2. 有用过@Styles,@Extend,@Builder装饰器么?
-
@Styles装饰器:定义组件重用样式 (多个组件通用的样式)
@Styles装饰器,用于封装重复的通用样式代码。
如果多个不同类型的组件,有着相同的样式,例如宽高,背景色,字体大小。那么就可以将这下相同的样式代码抽取到一个@Styles装饰器修饰的方法中,供大家复用。
支持全局和局部定义:
// 全局 @Styles function functionName() { ... } //styles方法不能调用另一个styles方法*** // 在组件内 @Component struct FancyUse { @Styles fancy() { .height(100) } }
-
@Extend装饰器:定义扩展组件样式 (某一种组件自己的样式,私有属性)
@Extend,用于扩展原生组件样式。
如果同一类型的组件,有着很多相同的样式,例如按钮的类型,点击事件等。那么就可以将这些重复代码,抽过去到一个@Extend装饰器修饰的方法中,供此组件使用。
==仅支持全局定义==:(因为它相当于是给所有的此类组件使用)
// @Extend(Text)可以支持Text的私有属性fontColor @Extend(Text) function fancy () { .fontColor(Color.Red) } // superFancyText可以调用预定义的fancy @Extend(Text) function superFancyText(size:number) { //Extend方法可以调用另一个Extend方法 .fontSize(size) .fancy() }
-
@Builder装饰器:自定义构建函数
@Builder装饰器,用于封装重复的,复杂UI结构代码,例如List中的ListItem的布局结构,一般比较复杂就可以抽取到@Builder装饰的函数中
@Builder所装饰的函数遵循build()函数语法规则,开发者可以将重复使用的UI元素抽象成一个方法,在build方法里调用。
支持全局定义和局部定义:
//既然调用是通过this调用,那么说明是在组件内部定义 //组件内部定义不需要关键字function @Builder MyBuilderFunction() { ... } //全局定义 MyGlobalBuilderFunction()
-
@BuilderParam装饰器:引用@Builder函数
当开发者创建了自定义组件,并想对该组件添加特定功能时,例如在自定义组件中添加一个点击跳转操作。若直接在组件内嵌入事件方法,将会导致所有引入该自定义组件的地方均增加了该功能。
为解决此问题,ArkUI引入了@BuilderParam装饰器,该装饰器用于声明任意UI描述的一个元素,==类似slot占位符==。
-
使得自定义组件更加灵活
代码:
@Component struct Child { @Builder customBuilder() {} // 使用父组件@Builder装饰的方法初始化子组件@BuilderParam @BuilderParam customBuilderParam: () => void = this.customBuilder; build() { Column() { this.customBuilderParam() } } } @Entry @Component struct Parent { @Builder componentBuilder() { Text(`Parent builder `) } build() { Column() { Child({ customBuilderParam: this.componentBuilder }) } } }
-
3. 还用过其他装饰器么?
1. Provide和Consume
-
@Provide和@Consume,用于祖先与后代组件的双向数据同步,实现跨层级传递
-
==理解==:@Provide装饰器的变量是在祖先组件中,可以理解为被“提供”给后代的状态变量。@Consume装饰的变量是在后代组件中,去“消费”数据
-
-
语法特点:
-
@Provide和@Consume可以通过相同的变量名或者相同的变量别名绑定
// 通过相同的变量名绑定 @Provide a: number = 0; //祖先组件中定义 @Consume a: number; //子孙组件中定义 // 通过相同的变量别名绑定 @Provide('a') b: number = 0;//参数即为别名 @Consume('a') c: number;
-
2. ObjectLink和Observed
-
@ObjectLink和@Observed类装饰器用于在涉及嵌套对象或数组的场景中进行双向数据同步:
-
被@Observed装饰的类,可以被观察到属性的变化;
-
子组件中@ObjectLink装饰器装饰的状态变量用于接收@Observed装饰的类的实例,和父组件中对应的状态变量建立双向数据绑定。这个实例可以是数组中的被@Observed装饰的项,或者是class object中的属性,这个属性同样也需要被@Observed装饰。
-
单独使用@Observed是没有任何作用的,需要搭配@ObjectLink或者@Prop使用。
-
-
详情参考: 3.状态管理.md --> 2.5章节
3. Watch
概述
-
@Watch应用于==对状态变量的监听==。如果开发者需要关注某个状态变量的值是否改变,可以使用@Watch为状态变量设置回调函数。
-
@Watch用于监听状态变量的变化,当状态变量变化时,@Watch的回调方法将被调用
代码:
@Component struct TotalView { @Prop @Watch('onCountUpdated') count: number = 0; @State total: number = 0; // 该函数是自定义组件的成员函数 // @Watch 回调 // propName是被watch的属性名 // 多个状态绑定同一个@Watch回调时,通过propName区分到底是哪个状态改变了 onCountUpdated(propName: string): void { this.total += this.count; } build() { Text(`Total: ${this.total}`) } } @Entry @Component struct CountModifier { @State count: number = 0; build() { Column() { Button('add to basket') .onClick(() => { this.count++ }) TotalView({ count: this.count }) } } }
3. 数据存储
1. LocalStorage和AppStorage的区别,和对应的装饰器以及PersistentStorage ***
LocalStorage
页面级UI状态存储,通常用于UIAbility内、页面间的状态共享。
localStorage是页面级数据存储,在页面中创建实例,组件中使用@LocalStorageLink和@LocalStorageProp装饰器修饰对应的状态变量,绑定对应的组件使用比状态属性更灵活
//应用逻辑使用LocalStorage let para: Record<string,number> = { 'PropA': 47 }; let storage: LocalStorage = new LocalStorage(para); // 创建新实例并使用给定对象初始化 , 创建实例存储数据 let propA: number | undefined = storage.get('PropA') // propA == 47 ,get()获取数据 //link():如果给定的propName在LocalStorage实例中存在,则返回与LocalStorage中propName对应属性的双向绑定数据。 (双向同步) let link1: SubscribedAbstractProperty<number> = storage.link('PropA'); // link1.get() == 47 let link2: SubscribedAbstractProperty<number> = storage.link('PropA'); // link2.get() == 47 //prop():如果给定的propName在LocalStorage中存在,则返回与LocalStorage中propName对应属性的单向绑定数据。 (单向同步) let prop: SubscribedAbstractProperty<number> = storage.prop('PropA'); // prop.get() == 47 link1.set(48); // two-way sync: link1.get() == link2.get() == prop.get() == 48 //双向同步 prop.set(1); // one-way sync: prop.get() == 1; but link1.get() == link2.get() == 48 //单向同步 link1.set(49); // two-way sync: link1.get() == link2.get() == prop.get() == 49
-
new LocalStorage(数据Object)
创建实例并存储数据 -
set方法,设置数据
-
get方法,获取数据
-
link方法,返回一个双向同步的变量
-
与UI逻辑中@LocalStorageLink装饰器类似
-
-
prop方法,返回单向同步的变量
-
与UI逻辑中@LocalStorageProp装饰器类似
-
//UI使用LocalStorage //除了应用程序逻辑使用LocalStorage,还可以借助LocalStorage相关的两个装饰器@LocalStorageProp和@LocalStorageLink,在UI组件内部获取到LocalStorage实例中存储的状态变量。 // 创建新实例并使用给定对象初始化 let para: Record<string, number> = { 'PropA': 47 }; //这个变量一般就定义在某个@Entry组件的上方** let storage: LocalStorage = new LocalStorage(para); @Component struct Child { // @LocalStorageLink变量装饰器与LocalStorage中的'PropA'属性建立双向绑定 @LocalStorageLink('PropA') storageLink2: number = 1; build() { Button(`Child from LocalStorage ${this.storageLink2}`) // 更改将同步至LocalStorage中的'PropA'以及Parent.storageLink1 .onClick(() => { this.storageLink2 += 1 }) } } // 使LocalStorage可从@Component组件访问 @Entry(storage)//storage是上边定义的变量** @Component struct Parent { // @LocalStorageLink变量装饰器与LocalStorage中的'PropA'属性建立双向绑定 @LocalStorageLink('PropA') storageLink1: number = 1; build() { Column({ space: 15 }) { Button(`Parent from LocalStorage ${this.storageLink1}`) // initial value from LocalStorage will be 47, because 'PropA' initialized already .onClick(() => { this.storageLink1 += 1 }) // @Component子组件自动获得对CompA LocalStorage实例的访问权限。 Child() } } } //上述代码:如果将LocalStorageLink改为LocalStorageProp就由双向变为了单向
-
new LocalStorage(数据Object)
创建实例并存储数据 -
父组件:
-
@Entry(LocalStorage实例)
, 将LocalStorage数据注册到页面中,使得页面内部可以使用数据 -
@LocalStorageLink
,将页面变量与数据进行双向绑定,父子组件都可以使用 -
@LocalStorageProp
,将页面变量与数据进行单向绑定,父子组件都可以使用
-
AppStorage
AppStorage是进程级数据存储(==应用级的全局状态共享==),进程启动时自动创建了唯一实例,在各个页面组件中@StorageProp和@StorageLink装饰器修饰对应的状态变量。
//AppStorage是单例,它的所有API都是静态的 AppStorage.setOrCreate('PropA', 47); let propA: number | undefined = AppStorage.get('PropA') // propA in AppStorage == 47 let link1: SubscribedAbstractProperty<number> = AppStorage.link('PropA'); // link1.get() == 47 let link2: SubscribedAbstractProperty<number> = AppStorage.link('PropA'); // link2.get() == 47 let prop: SubscribedAbstractProperty<number> = AppStorage.prop('PropA'); // prop.get() == 47 link1.set(48); // two-way sync: link1.get() == link2.get() == prop.get() == 48 prop.set(1); // one-way sync: prop.get() == 1; but link1.get() == link2.get() == 48 link1.set(49); // two-way sync: link1.get() == link2.get() == prop.get() == 49 AppStorage.get<number>('PropA') // == 49 link1.get() // == 49 link2.get() // == 49 prop.get() // == 49
-
语法基本上与LocalStorage类似,只不过是静态方法
-
link双向,prop单向
AppStorage.setOrCreate('PropA', 47); @Entry(storage) @Component struct CompA { @StorageLink('PropA') storageLink: number = 1; build() { Column({ space: 20 }) { Text(`From AppStorage ${this.storageLink}`) .onClick(() => { this.storageLink += 1 }) } } }
-
将
@StorageLink
换为@StorageProp
,双向就变为单向的了 -
不建议使用@StorageLink , 其实就是因为AppStorage共享范围太多,更新效率低下,也可能造成不必要的更新
补充:
localStorage和appStorage数据存取都是在主线程进行的,且api只提供了同步接口,存取数据时要注意数据的大小。
-
关于存储时数据的大小问题
-
AppStorage没有大小限制,单条数据[k,v)],v也没有限制,但是不建议单条v大于1kb,大于1kb建议使用数据库。多条使用没有限制,会动态分配的。
-
LocalStorage底层实现是一个map,理论上没有大小限制。
-
-
参考链接:文档中心
PersistentStorage
PersistentStorage将选定的AppStorage属性保留在设备磁盘上。应用程序通过API,以决定哪些AppStorage属性应借助PersistentStorage持久化。UI和业务逻辑不直接访问PersistentStorage中的属性,所有属性访问都是对AppStorage的访问,AppStorage中的更改会自动同步到PersistentStorage。
PersistentStorage和AppStorage中的属性建立双向同步。应用开发通常通过AppStorage访问PersistentStorage,另外还有一些接口可以用于管理持久化属性,但是业务逻辑始终是通过AppStorage获取和设置属性的
从AppStorage中访问PersistentStorage初始化的属性
-
初始化PersistentStorage:
PersistentStorage.persistProp('aProp', 47);
-
在AppStorage获取对应属性:
AppStorage.get<number>('aProp'); // returns 47
或在组件内部定义:
@StorageLink('aProp') aProp: number = 48;
2.数据存储怎么存?都用过什么数据存储?用户首选项,键值型数据库,关系数据库
首选项
用户首选项(Preferences):提供了==轻量级配置数据的持久化能力==,并支持订阅数据变化的通知能力。不支持分布式同步,常用于保存==应用配置信息、用户偏好设置==等。
运作机制:
约束限制:
-
Key键为string类型,要求非空且长度不超过==80个字节==
-
如果Value值为string类型,请使用UTF-8编码格式,可以为空,不为空时长度不超过==8192个字节==
-
内存会随着存储数据量的增大而增大,所以存储的数据量应该是轻量级的,建议存储的数据==不超过一万条==,否则会在内存方面产生较大的开销
代码:
import dataPreferences from '@ohos.data.preferences'; //1. 获取preference private preferences: dataPreferences.Preferences = dataPreferences.getPreferencesSync(this.context, { name: 'myStore' }); //2. 保存数据 this.preferences.putSync('key', value); //3. 持久化数据 this.preferences.flush() //4. 获取数据 let result = this.preferences.getSync("key",16) this.changeFontSize = Number(result)
键值型数据库
键值型数据库存储键值对形式的数据,当需要存储的数据没有复杂的关系模型,比如存储商品名称及对应价格、员工工号及今日是否已出勤等,由于数据复杂度低,更容易兼容不同数据库版本和设备类型,因此推荐使用键值型数据库持久化此类数据。
约束:
-
设备协同数据库,针对每条记录,Key的长度≤896 Byte,Value的长度<4 MB。
-
单版本数据库,针对每条记录,Key的长度≤1 KB,Value的长度<4 MB。
-
每个应用程序最多支持同时打开16个键值型分布式数据库。
-
键值型数据库事件回调方法中不允许进行阻塞操作,例如修改UI组件。
剩余内容:文档中心
关系型数据库
关系型数据库基于SQLite组件,适用于存储包含==复杂关系数据==的场景,比如一个班级的学生信息,需要包括姓名、学号、各科成绩等,又或者公司的雇员信息,需要包括姓名、工号、职位等,由于数据之间有较强的对应关系,复杂程度比键值型数据更高,此时需要使用关系型数据库来持久化保存数据。
运作机制:
约束:
-
数据库中有4个读连接和1个写连接,线程获取到空闲读连接时,即可进行读取操作。当没有空闲读连接且有空闲写连接时,会将写连接当做读连接来使用。
-
为保证数据的准确性,数据库同一时间只能支持一个写操作。
-
当应用被卸载完成后,设备上的相关数据库文件及临时文件会被自动清除。
-
为保证插入并读取数据成功,建议一条数据不要超过2M。超出该大小,插入成功,读取失败。
代码:
import relationalStore from '@ohos.data.relationalStore' import { common } from '@kit.AbilityKit' export class DBUtils { // 数据库名称 private tableName: string = 'accountTable' // 建表语句 private sqlCreate: string = 'CREATE TABLE IF NOT EXISTS accountTable(id INTEGER PRIMARY KEY AUTOINCREMENT, accountType INTEGER, ' + 'typeText TEXT, amount INTEGER)' // 表字段 private columns: string[] = ['id', 'accountType', 'typeText', 'amount'] // 数据库核心类 private rdbStore: relationalStore.RdbStore | null = null // 数据库配置 DB_CONFIG: relationalStore.StoreConfig = { name: 'RdbTest.db', // 数据库文件名 securityLevel: relationalStore.SecurityLevel.S1, // 数据库安全级别 }; /** * 获取rdb * @param context:上下文 * @param callback:回调函数,我们第一次获取数据时,需要在获取到rdb之后才能获取,所以有此回调 */ getRdbStore(context: common.UIAbilityContext, callback: Function) { relationalStore.getRdbStore(context, this.DB_CONFIG, (error, store) => { if (this.rdbStore !== null) { //如果已经有rdb,直接建表 store.executeSql(this.sqlCreate) return } //保存rdb,下边会用 this.rdbStore = store //建表 store.executeSql(this.sqlCreate) console.log("test", "successed get dbStore") if (callback) callback() }) } /** * 插入数据 * @param data:数据对象 * @param callback:回调函数,这里的结果是通过回调函数返回的(也可使用返回值) */ insertData(data: AccountData, callback: Function) { //将数据对象,转换为ValuesBucket类型 const valueBucket: relationalStore.ValuesBucket = generateBucket(data); // 调用insert插入数据 this.rdbStore && this.rdbStore.insert(this.tableName, valueBucket, (err, res) => { if (err) { console.log("test,插入失败", err) callback(-1) return } console.log("test,插入成功", res) callback(res) //res为行号 }) } /** * 获取数据 * @param callback:接收结果的回调函数 */ query(callback: Function) { //predicates是用于添加查询条件的 let predicates = new relationalStore.RdbPredicates(this.tableName) // 查询所有,不需要条件 // predicates.equalTo("字段",数据) this.rdbStore && this.rdbStore.query(predicates, this.columns, (error, resultSet: relationalStore.ResultSet) => { if(error){ console.log("test,获取数据失败",JSON.stringify(error)) return } let count: number = resultSet.rowCount console.log("test","数据库中数据数量:"+count) //没数据时返回-1或0 if (count <= 0 || typeof count === 'string') { callback([]) return } let result: AccountData[] = [] //上来必须调用一次goToNextRow,让游标处于第一条数据,while(resultSet.goToNextRow())是最有写法 while(resultSet.goToNextRow()) { let accountData:AccountData = {id:0,accountType:0,typeText:'',amount:0} accountData.id = resultSet.getDouble(resultSet.getColumnIndex('id')); accountData.typeText = resultSet.getString(resultSet.getColumnIndex('typeText')) accountData.accountType = resultSet.getDouble(resultSet.getColumnIndex('accountType')) accountData.amount = resultSet.getDouble(resultSet.getColumnIndex('amount')) result.push(accountData) } callback(result) resultSet.close()//释放数据集内容 }) } } function generateBucket(account: AccountData): relationalStore.ValuesBucket { let obj: relationalStore.ValuesBucket = {}; obj.accountType = account.accountType; obj.typeText = account.typeText; obj.amount = account.amount; return obj; } export class AccountData { id: number = -1; accountType: number = 0; typeText: string = ''; amount: number = 0; }
使用代码:(部分代码,从HelloDataManager中copy的)
// 数据库工具类 private dbUitls: DBUtils = new DBUtils() // 界面打开时,查询数据,展示胡静 aboutToAppear(): void { this.dbUitls.getRdbStore(this.context, () => { this.queryData() }) } // 查询数据方法 queryData(){ this.dbUitls.query((result: AccountData[]) => { this.accountDataArray = result console.log("test,获取数据成功:", JSON.stringify(this.accountDataArray)) }) } // 点击确定回调 onConfirm(insertData: AccountData) { console.log("test", JSON.stringify(insertData)) // 插入数据 this.dbUitls.insertData(insertData, (res: number) => { if (res > 0) { // AlertDialog.show({ message: "添加成功" }) this.queryData() } else { AlertDialog.show({ message: "添加失败" }) } }) }
4. 组件&布局
1.用过flex布局吗?flex存在的问题是什么?为什么会造成二次渲染?如何优化?
-
用过flex
-
弹性布局(Flex)提供更加有效的方式对容器中的子元素进行排列、对齐和分配剩余空间。常用于页面头部导航栏的均匀分布、页面框架的搭建、多行数据的排列等。
容器默认存在主轴与交叉轴,子元素默认沿主轴排列,子元素在主轴方向的尺寸称为主轴尺寸,在交叉轴方向的尺寸称为交叉轴尺寸。
-
-
flex会造成二次渲染
-
flex中flexgrow=1时,子组件宽度和大于flex的宽度时,页面渲染后,会调整子组件宽度使之宽度和等于flex的宽度,造成二次布局渲染 flex中flexshrink=1时,子组件宽度和小于flex的宽度时,页面渲染后,也会调整子组件宽度使之宽度和等于flex的宽度,造成二次布局渲染
-
flexGrow:设置父容器的剩余空间分配给此属性所在组件的比例。用于分配父组件的剩余空间。
flexShrink: 当父容器空间不足时,子元素的压缩比例。
-
-
优化
-
使用Column/Row代替Flex。
-
大小不需要变更的子组件主动设置flexShrink属性值为0。
-
优先使用layoutWeight属性替代flexGrow属性和flexShrink属性。
-
子组件主轴长度分配设置为最常用场景的布局结果,使子组件主轴长度总和等于Flex容器主轴长度。
-
-
参考链接:文档中心
2. flex导致二次布局的原因,以及调研的经历
flex中flexgrow=1时,子组件宽度和大于flex的宽度时,页面渲染后,会调整子组件宽度使之宽度和等于flex的宽度,造成二次布局渲染 flex中flexshrink=1时,子组件宽度和小于flex的宽度时,页面渲染后,也会调整子组件宽度使之宽度和等于flex的宽度,造成二次布局渲染
-
参考链接:文档中心
3.使用了哪些组件,有没有写过自定义组件,自定义组件怎么设计的
-
Button是按钮组件,通常用于响应用户的点击操作,其类型包括胶囊按钮、圆形按钮、普通按钮。Button做为容器使用时可以通过添加子组件实现包含文字、图片等元素的按钮。
Text是文本组件,通常用于展示用户视图,如显示文章的文字
-
写过自定义组件
-
struct:自定义组件基于struct实现,struct + 自定义组件名 + {...}的组合构成自定义组件,不能有继承关系。对于struct的实例化,可以省略new。
-
@Component:@Component装饰器仅能装饰struct关键字声明的数据结构。struct被@Component装饰后具备组件化的能力,需要实现build方法描述UI,一个struct只能被一个@Component装饰。@Component可以接受一个可选的bool类型参数。
-
build()函数:build()函数用于定义自定义组件的声明式UI描述,自定义组件必须定义build()函数。
-
@Entry:@Entry装饰的自定义组件将作为UI页面的入口。在单个UI页面中,最多可以使用@Entry装饰一个自定义组件。@Entry可以接受一个可选的LocalStorage的参数。
-
@Reusable:@Reusable装饰的自定义组件具备可复用能力
-
参考链接:文档中心
-
-
自定义组件怎么设计,有两种情况
-
首先公司内部将常用的一些效果 , 都封装了组件
-
比如: listView下拉刷新动画 , 进度条等等
-
具体自定义组件如何定义 , 参考上述链接
-
-
其次就是一个应用中 , 如果一个布局在多个页面中出现 , 我们就可以将这个布局效果封装为一个组件
-
比如: 页面的头部标题 , 点赞 , 收藏等
-
具体自定义组件如何定义 , 参考上述链接
-
-
4. 自定义组件,实际开发中封装过哪些自定义组件
例如:下拉刷新、自定义弹窗、自定义LoadingProgress、统一标题栏的封装等...
自定义组件具有以下特点:
-
可组合:允许组合使用系统组件、及其属性和方法。
-
可重用:自定义组件可以被其他组件重用,并作为不同的实例在不同的父组件或容器中使用。
-
数据驱动UI更新:通过状态变量的改变,来驱动UI的刷新。
自定义组件基于struct实现,struct + 自定义组件名 + {...}的组合构成自定义组件,不能有继承关系。
struct被@Component装饰后具备组件化的能力,需要实现build方法描述UI,一个struct只能被一个@Component装饰。
5.项目中用到arkTs哪些技术
arkTs的技术?
装饰器
渲染控制:
6.flex-shrink的使用
答: 设置父容器压缩尺寸分配给此属性所在组件的比例, 设置为==0表示不压缩==, 父容器为Row、Column时,默认值:0。 父容器为flex时,默认值:1。 在子组件的总宽度大于父容器的宽度时,子组件中有设置flex-shrink , 并且值大于0时,父容器会把剩余宽度按照比例压缩子组件。
Flex布局-通用属性-组件通用信息-组件参考(基于ArkTS的声明式开发范式)-ArkTS API参考-HarmonyOS应用开发
关于flexShrink如何计算
分析过程:
自己总结公式:
溢出值:所有子元素宽度相加 - 父元素宽度
总权重:子元素1的flexShrink*子元素1宽度 + 子元素2的flexShrink*子元素2宽度 +...
子元素压缩值:溢出值 * 子元素的权重 = 溢出值 * 子元素1的flexShrink*子元素1宽度/总权重
7.webview组件如何使用,ets文件如何与h5通讯 ***
总结:两种方式:
方式一:本题
runJavaScript() arkts-》H5 javaScriptProxy() 和 registerjavaScriptProxy() H5-》arkts
方式二:下一题
createWebMessagePorts postMessage onMessageEvent 数据通道
这里的方案是,相互调用函数来通信,12题是通过消息端口建立数据通道
-
@ohos.web.webview提供web控制能力,web组件提供网页显示的能力
-
webview组件如何使用?
-
添加网络权限: ohos.permission.INTERNET
-
加载网页
// xxx.ets import web_webview from '@ohos.web.webview' @Entry @Component struct WebComponent { controller: web_webview.WebviewController = new web_webview.WebviewController() build() { Column() { //加载在线网页 Web({ src: 'www.example.com', controller: this.controller }) //加载本地网页 // 通过$rawfile加载本地资源文件。 Web({ src: $rawfile("index.html"), controller: this.controller }) // 通过resource协议加载本地资源文件。 Web({ src: "resource://rawfile/index.html", controller: this.controller }) } } }
-
-
ets文件如何与h5通讯?
-
ets调用h5网页方法
-
h5定义方法
<!-- index.html --> <!DOCTYPE html> <html> <meta charset="utf-8"> <body> </body> <script type="text/javascript"> function htmlFn() { console.log('run javascript test') return "This value is from index.html" } </script> </html>
-
ets调用
Button('runJavaScript') .onClick(() => { console.log("run-onclick") //点击按钮,运行js方法 this.controller.runJavaScript('htmlFn()', (err, val) => { if (err) { console.log('run javascript err ' + JSON.stringify(err)) return } if (val) { this.message = val } console.log('run javascript success ' + val); }) }) .width('30%') .height('30')
-
-
h5调用ets方法
-
ets定义方法
testObj = { test:() => { this.message = '调用了当前方法' console.log('javascript run test 调用了当前方法') return 'ArkUI Web Component' }, toString:() => { console.log('Web Component toString') } }
-
注入到h5
Button('Register JavaScript To Window') .onClick(() => { AlertDialog.show({message:'注册方法成功'}) try { //注册方法到H5的控制器 //参数1:传入调用方法的对象 //参数2:H5在使用该对象的名字 //参数3:方法列表(数组) this.controller.registerJavaScriptProxy(this.testObj, "testObjName", ["test", "toString"]); } catch (error) { console.error(`ErrorCode: ${error.code}, Message: ${error.message}`); } }) //registerJavaScriptProxy注册的方法,必须刷新才能使用 Button('refresh') .onClick(() => { AlertDialog.show({message:'刷新'}) try { this.controller.refresh(); } catch (error) { console.error(`ErrorCode: ${error.code}, Message: ${error.message}`); } }) //或者不适用registerJavaScriptProxy方法来注册,直接使用Web的方法javaScriptProxy Web({ src: $rawfile("second.html"), controller: this.controller }) // 将对象注入到web端 .javaScriptProxy({ object: this.testObj, name: "testObjName", methodList: ["test", "toString"], controller: this.controller }) //有什么区别呢?目前官网文档中的例子是,如果ets的方法test返回的是复杂类型的数据,比如数组,那么使用的是registerJavaScriptProxy //但是经过测试,本地模拟器和远程模拟器,都无法传递,可能是模拟器的问题
-
h5调用
<!-- index.html --> <!DOCTYPE html> <html> <meta charset="utf-8"> <body> <button style="width:200px;height:200px" type="button" οnclick="htmlTest()">Click Me!</button> <p id="demo"></p> </body> <script type="text/javascript"> function htmlTest() { let str=testObjName.test(); document.getElementById("demo").innerHTML=str; console.log('testObj.test result:'+ str) } </script> </html>
-
-
代码在InterviewQuestion项目中的WebComponent2
-
8.webview如何拿到外面的数据,如何跟外部通信 ***
可以通过WebView的WebMessagePort来进行通信,具体通信流程图如下:
完整代码,参考官网:文档中心
然后找到postMessage,查看完整示例代码(或找到InterviewQuestion项目中的WebComponent.ets)
==注意==:
-
应用端通过消息端口(WebMessagePort):发送消息使用
postMessageEvent
,接收消息使用onMessageEvent
-
html端通过消息端口:发送消息使用
postMessage
,接收消息监听事件onmessage
(都少了event)
==问题==: 模拟器中web加载网页后,网页的按钮无法点击。所以上述案例中,在模拟器中,是无法测试html端---应用端的数据传递。
-
==本地的模拟器现在不支持webview==
-
可以使用远程模拟器,或远程真机
==问题==: 在将端口0发送到html端时,使用的方法是postMessage
,而真正发送数据是通过postMessageEvent
,区别在哪?
-
postMessage就是用于发送消息端口的
-
postMessageEvent就是用于发送消息的
二. 网络请求&线程相关
1. 数据通信 ***
eventHub : 普通事件发布,订阅
-
eventHub:提供了事件中心,提供订阅,取消订阅,触发事件的能力,同hap内通信,不跨线程
-
eventHub.emit(数据标记,数据) ,触发事件
-
eventHub.on(数据标记, ()=>{}) , 监听事件
emitter : 处理进程内,线程间事件 , 发送事件会放到事件队列 ,多hap通信方案
-
emitter.emit({eventId:xx},数据) ,触发事件
-
emitter.on({eventId:xx},()=>{}) , 监听事件
worker: 处理线程间事件,主要处理耗时事件
-
验证- 同hap
-
跨hap
2、项目中接口请求的数据,需要进行缓存吗?鸿蒙化处理过程
根据自己项目中的业务逻辑描述,MapKit需要将当前屏幕中展示的6个瓦片数据图片缓存到沙盒中, 杀掉进程后,断开网络,打开app会展示缓存的数据瓦片。
2.promise如何解决异步并发(Promise并发控制的实现方式)
异步并发涉及同时执行多个异步操作。可以使用 Promise.all
或 Promise.allSettled
方法来处理多个异步操作的并发执行。
-
使用
Promise.all
处理异步并发:Promise.all 可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被 reject 失败状态的值。
Promse.all 在处理多个异步处理时非常有用,比如说一个页面上需要等两个或多个ajax的数据回来以后才正常显示,在此之前只显示loading图标。
需要特别注意的是,Promise.all 获得的成功结果的数组里面的==数据顺序==和Promise.all接收到的数组顺序是一致的,即p1的结果在前,即便p1的结果获取的比p2要晚。这带来了一个绝大的好处:在前端开发请求数据的过程中,偶尔会遇到发送多个请求并根据请求顺序获取和使用数据的场景,使用Promise.all毫无疑问可以解决这个问题。
-
race
顾名思义,Promse.race 就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。
race 的使用场景就是,多台服务器部署了同样的服务端代码,假如我要获取一个商品列表接口,我可以在 race 中写上所有服务器中的查询商品列表的接口地址,哪个服务器响应快,就从哪个服务器拿数据。
-
使用
Promise.allSettled
处理异步并发:**Promise.allSettled()**
方法返回一个在所有给定的promise都已经fulfilled
或rejected
后的promise,并带有一个对象数组,每个对象表示对应的promise结果。当您有多个彼此不依赖的异步任务成功完成时,或者您总是想知道每个
promise
的结果时,通常使用它。顾名思义:allsettled,所有的都结束,不管结果。
参考链接:Promise异步并发控制Promise 异步并发控制-JavaScript中文网-JavaScript教程资源分享门户
3.异步提交,代码封装 (Promise如何使用,有几种是否用方式,分别是什么)***
-
then().catch()
-
async/await+try{}catch(){}
异步提交(例如异步请求提交数据),可以封装一个函数来处理异步操作,使用 Promise 或者 async/await 提供异步编程的支持。
-
使用
Promise
封装异步提交:function asyncSubmit(data) { return new Promise((resolve, reject) => { // 模拟异步提交 setTimeout(() => { const success = true; // 模拟提交成功 if (success) { resolve('提交成功'); } else { reject('提交失败'); } }, 1000); }); } // 调用异步提交 asyncSubmit({ /* 数据 */ }) .then((result) => { console.log(result); }) .catch((error) => { console.error(error); });
-
使用async/await封装异步提交:
async function asyncSubmit(data) { return new Promise((resolve, reject) => { // 模拟异步提交 setTimeout(() => { const success = true; // 模拟提交成功 if (success) { resolve('提交成功'); } else { reject('提交失败'); } }, 1000); }); } // 调用异步提交 async function submit() { try { const result = await asyncSubmit({ /* 数据 */ }); console.log(result); } catch (error) { console.error(error); } } submit();
参考链接:
-
promise封装异步操作promise封装异步操作_promise封装异步函数-CSDN博客
-
Promise、async、await,封装异步代码,解决回调地狱Promise、async、await,封装异步代码,解决回调地域_async await 包装promise-CSDN博客
15.请介绍一下Async await ,await是否阻塞线程?
async/await
是 JavaScript 中处理异步代码的一种方式,它建立在 Promise 基础之上,提供了更清晰和易读的语法。使用 await
关键字可以暂停异步函数的执行,等待 Promise 对象的解决(resolve)。
await
并不会阻塞整个线程。当遇到 await
时,它会让出线程的执行,使得其他任务可以继续执行。在背后,await
会暂停当前函数的执行,等待 Promise 对象的状态发生变化。
参考链接:async/await详解async、await详解_async await-CSDN博客
1. 鸿蒙系统http在接口请求时,系统本身应该是开启子线程来请求,那么为什么在项目中还要使用taskpool开辟子线程呢
鸿蒙底层的http请求实现是在子线程中进行的,但针对于请求参数的编码与返回值的解析,这部分需要花费时间来处理的业务逻辑,还是建议使用taskpool开辟子线程来处理
参考答案二:
鸿蒙底层的http请求实现是在子线程中进行的。但在http在接口请求时,我们再开辟的子线程主要用于单次任务的执行,并对于请求返回值的解析,因为在返回大量时值解析是比较花费费时间的; 在多任务请求时TaskPool开辟的子线程可以执行密集型任务的开发,更加的灵活方便,
2. http数据请求采用什么方案? 请求参数类型是怎么设计的,如果是object, Object与object有啥区别?
已融入ppt: 进阶day01: 61
在进行网络请求前,您需要在module.json5文件中申明网络访问权限。因为HarmonyOS提供了一种访问控制机制即应用权限,用来保证这些数据或功能不会被不当或恶意使用。
{ "module" : { "requestPermissions":[ { "name": "ohos.permission.INTERNET" } ] } }
-
导入http模块。
import http from '@ohos.net.http';
-
创建httpRequest对象。 使用createHttp()创建一个httpRequest对象,里面包括常用的一些网络请求方法,比如request、destroy、on(‘headerReceive’)等。
let httpRequest = http.createHttp();
-
订阅请求头(可选)。 用于订阅http响应头,此接口会比request请求先返回,可以根据业务需要订阅此消息。
httpRequest.on('headersReceive', (header) => { console.info('header: ' + JSON.stringify(header)); });
-
发起http请求。
http模块支持常用的POST和GET等方法,封装在RequestMethod中。调用request方法发起网络请求,需要传入两个参数。第一个是请求的url地址,第二个是可选参数,类型为HttpRequestOptions,用于定义可选参数的类型和取值范围,包含请求方式、连接超时时间、请求头字段等。
请求参数通常以键值对的形式进行传递,其中参数类型可以是字符串、数字等基本类型。如果涉及到发送复杂的数据结构,比如JSON对象,那么请求参数的类型可能是一个对象(Object)。
Object:通常指的是 ArkTS 中的内置对象类型,用于表示键值对集合。
object:通常指的是 ArkTS 中的对象类型。在 ArkTS 中,对象是一种复合数据类型,它可以包含多个键值对。对象可以是基本类型的包装对象,也可以是用户自定义的对象。
参考链接:
3. 都说服务端响应整个流程慢,到底慢在哪里
服务端响应整个流程慢可能有多个原因,这需要进行系统性的性能分析以确定根本原因。以下是一些可能导致服务端响应慢的常见原因:
-
网络延迟: 如果服务端与客户端之间的网络延迟较高,数据在两者之间传输的时间将会增加。
-
服务器性能不足: 服务器硬件性能不足,或者服务器资源(CPU、内存、磁盘)被过度占用,可能导致响应时间延迟。
-
数据库性能: 如果应用程序需要从数据库中检索大量数据,或者数据库查询性能较差,会导致响应时间延迟。优化数据库查询、使用索引、缓存等可以改善这方面的问题。
-
第三方服务延迟: 如果应用程序依赖于外部的第三方服务,而这些服务的响应速度较慢,也会影响整体的响应时间。
-
未优化的代码: 代码的性能问题可能导致响应时间延迟。循环次数过多、未经优化的算法、内存泄漏等都可能影响代码的性能。
-
未优化的资源: 大量未经优化的静态资源(如图片、CSS、JavaScript)可能导致页面加载慢,从而影响整体的用户体验。
-
缺乏合适的缓存机制: 缺乏适当的缓存机制可能导致相同的请求重复执行相同的计算,增加响应时间。
为了准确定位服务端响应慢的原因,可以使用性能分析工具、日志分析工具以及监控工具进行详细的诊断。系统性的性能测试和监控可以帮助发现瓶颈,从而采取相应的优化策略。
参考链接:
-
应用服务器响应缓慢的原因及分析技巧 (应用服务器慢怎么分析)应用服务器响应缓慢的原因及分析技巧 (应用服务器慢怎么分析)-数据运维技术
-
服务器运行慢的原因有哪些服务器运行慢的原因有哪些(服务器运行慢的原因有哪些呢)_服务器知识_侠客网
4. 服务端响应期间会不会卡页面渲染
HarmonyOS(鸿蒙)是一个分布式操作系统,与传统的 Web 开发有所不同。在 Web 开发中,服务端响应慢可能会导致页面在浏览器中渲染时出现卡顿,因为浏览器通常会在主线程上执行 JavaScript 代码,包括处理响应数据和更新 DOM。
对于 HarmonyOS,服务端响应慢不会直接导致页面渲染的卡顿,因为 HarmonyOS 应用通常是本地应用,而不是在浏览器中运行的 Web 应用。HarmonyOS 应用的渲染通常是在本地设备上进行的,与服务端响应的速度关系不大。
但是,在 HarmonyOS 应用中,如果你的应用在进行网络请求时阻塞了主线程,例如在主线程上进行了同步的网络请求,那么这可能会导致 UI 的卡顿。因此,建议在进行网络请求时使用异步的方式,以避免阻塞主线程。
总体而言,HarmonyOS 应用的响应速度主要与本地设备的性能和网络请求的优化有关,与服务端响应慢是否会卡页面渲染关系较小。在应用开发中,建议合理使用异步操作、优化网络请求和合理设计本地业务逻辑,以提供更好的用户体验。
参考链接:
5. 网络请求怎么使用 (你的项目中的网络请求用的是哪个api)***
-
官方提供的HTTP,我们进一步进行封装
-
三方提供的Axios
首先需要请求相应的权限。
-
HTTP数据请求:通过HTTP发起一个数据请求。
-
WebSocket连接:使用WebSocket建立服务器与客户端的双向连接。
-
Socket连接:通过Socket进行数据传输。
-
网络连接管理:网络连接管理提供管理网络一些基础能力,包括WiFi/蜂窝/Ethernet等多网络连接优先级管理、网络质量评估、订阅默认/指定网络连接状态变化、查询网络连接信息、DNS解析等功能。
-
MDNS管理:MDNS即多播DNS(Multicast DNS),提供局域网内的本地服务添加、移除、发现、解析等能力。
参考链接:文档中心
6. 联系人中涉及到哪些数据请求,畅连caas接口提供方
联系人(contact)提供了联系人管理能力,包括添加联系人、删除联系人、更新联系人等。
HarmonyOS 提供了能力开发框架,你可以通过调用系统能力来获取联系人信息。如果联系人信息存储在设备本地,你可能需要使用数据存储和访问的 API 来获取这些信息。鸿蒙系统提供了分布式数据管理的能力,可以用于访问本地数据。 访问用户的敏感信息通常需要相应的权限。
关于 CaaS(Capability as a Service)接口提供方,鸿蒙系统提供了能力框架,使开发者可以调用系统能力。
参考链接:@ohos.contact(联系人)文档中心
7. axios请求参数如何写类型
在Axios中,如果想指定请求参数的类型,可以通过设置HTTP头部来实现。发送JSON数据,可以设置Content-Type
头部为application/json
。
告诉服务器正在发送JSON格式的数据。data
对象会被自动转换为JSON字符串。
参考链接:axios请求参数类型及传参方式axios请求参数类型及传参方式_axios 请求参数是多个对象类型怎么传递-CSDN博客
8. 项目中接口请求的数据,需要进行缓存吗?鸿蒙化处理过程
根据自己项目中的业务逻辑描述,MapKit需要将当前屏幕中展示的6个瓦片数据图片缓存到沙盒中,杀掉进程后,断开网络,打开app会展示缓存的数据瓦片。
答:根据项目的实际情况决定,如有些数据长期可能不更改并且经常需要获取使用的数据(比如: 用户id, 登录态,token,以及项目中某些特殊页面的数据等)。还有就是有些应用或界面需要的做把文件缓存到本地再做对应的逻辑处理的情况也需要缓存。鸿蒙提供了多种方式的数据缓存,比如首选项、键值型数据库、关系型数据库等。
我们可以根据实际的数据类型进行选择对应的缓存方式。如:
键值型数据库:以键值对的形式存储数据,当需要存储的数据没有复杂的关系模型,比如存储商品名称及对应价格、员工工号及今日是否已出勤等,由于数据复杂度低,更容易兼容不同数据库版本和设备类型,因此推荐使用键值型数据库持久化此类数据。
关系型数据库:基于SQLite组件,适用于存储包含复杂关系数据的场景,比如一个班级的学生信息,需要包括姓名、学号、各科成绩等,又或者公司的雇员信息,需要包括姓名、工号、职位等,由于数据之间有较强的对应关系,复杂程度比键值型数据更高,此时需要使用关系型数据库来持久化保存数据。
9. taskpool开启子线程和promise的区别
taskpool开辟子线程能够充分利用现代手机设备多核配置,提高程序的并发性和运行效率;
Promise是在当前线程进行异步调用,可以避免阻塞主线程,保证程序的响应速度,提高用户体验;
具体使用场景:当需要执行I/O操作时,使用异步操作比使用线程+同步I/O操作更合适。
I/O操作不仅包括了直接的文件、网络的读写,还包括数据库操作、Web Service、
HttpRequest以及.Net Remoting等跨进程的调用。当需要长时间CPU运算的场合,
例如耗时较长的图形处理和算法执行时,建议使用子线程操作。
taskpool开辟子线程能够充分利用现代手机设备多核配置,提高程序的并发性和运行效率;
Promise是在当前线程进行异步调用,可以避免阻塞主线程,保证程序的响应速度,提高用户体验;
具体使用场景:
当需要执行I/O操作时,使用异步操作比使用线程+同步I/O操作更合适。I/O操作不仅包括了直接的文件、网络的读写,还包括数据库操作、Web Service、HttpRequest以及.Net Remoting等跨进程的调用。
当需要长时间CPU运算的场合,例如耗时较长的图形处理和算法执行时,建议使用子线程操作。
参考二:
taskpool:是多线程异步,是通过调用线程池内的线程去异步完成任务;
promise:是同一个线程在各个任务间进行切换,通过暂时挂起任务实现异步 ,异步代码会被挂起并在之后继续执行,并且同一时间只有一段代码执行。
10. 任务池(taskPool),worker,他们之间的区别及使用场景,实际项目怎么使用?
实现 | TaskPool | Worker |
---|---|---|
内存模型 | 线程间隔离,内存不共享。 | 线程间隔离,内存不共享。 |
参数传递机制 | 采用标准的结构化克隆算法(Structured Clone)进行序列化、反序列化,完成参数传递。支持ArrayBuffer转移和SharedArrayBuffer共享。 | 采用标准的结构化克隆算法(Structured Clone)进行序列化、反序列化,完成参数传递。支持ArrayBuffer转移和SharedArrayBuffer共享。 |
参数传递 | 直接传递,无需封装,默认进行transfer。 | 消息对象唯一参数,需要自己封装。 |
方法调用 | 直接将方法传入调用。 | 在Worker线程中进行消息解析并调用对应方法。 |
返回值 | 异步调用后默认返回。 | 主动发送消息,需在onmessage解析赋值。 |
生命周期 | TaskPool自行管理生命周期,无需关心任务负载高低。 | 开发者自行管理Worker的数量及生命周期。 |
任务池个数上限 | 自动管理,无需配置。 | 同个进程下,最多支持同时开启8个Worker线程。 |
任务执行时长上限 | 无限制。 | 无限制。 |
设置任务的优先级 | 不支持。 | 不支持。 |
执行任务的取消 | 支持取消任务队列中等待的任务。 | 不支持。 |
TaskPool中超长任务(大于3分钟)会被系统自动回收,如何避免超长任务?
将超大数据进行分段处理
CPU密集型任务开发指导-使用多线程并发能力进行开发-并发-ArkTS语言基础类库-开发-HarmonyOS应用开发
补充:
Worker:Worker中不能直接更新Page。Worker是否配置文件:在工程的模块级build-profile.json5文件的buildOption属性中添加配置信息。
taskPool与worker参数传递中数据类型支持:
基本类型数据、传递通过自定义class创建出来的object时,不会发生序列化错误
@ohos.worker (启动一个Worker)-语言基础类库-ArkTS接口参考-ArkTS API参考-HarmonyOS应用开发
常见的一些开发场景及适用具体说明如下:
-
有关联的一系列同步任务。例如在一些需要创建、使用句柄的场景中,句柄创建每次都是不同的,该句柄需永久保存,保证使用该句柄进行操作,需要使用Worker。
-
需要频繁取消的任务。例如图库大图浏览场景,为提升体验,会同时缓存当前图片左右侧各2张图片,往一侧滑动跳到下一张图片时,要取消另一侧的一个缓存任务,需要使用TaskPool
-
大量或者调度点较分散的任务。例如大型应用的多个模块包含多个耗时任务,不方便使用8个Worker去做负载管理,推荐采用TaskPool。
11. 异步方法Promise,callback,promise是在哪个线程? 是否会卡主线程?实际开发中会优先使用哪种方式做回调?await是否会卡线程?
答:
Promise是在主线程执行的,不会卡主线程,是一种异步编程方式,用于处理异步操作。
await 不会卡线程,await 关键字会暂停当前异步函数的执行,并将控制权交还给事件循环,直到promise对象解析或拒绝。
通过使用async关键字声明一个函数为异步函数,并使用await关键字等待Promise的解析(完成或拒绝),以同步的方式编写异步操作的代码。
优先使用promise。
12.promise如何使用?会不会阻塞UI线程(可了解下底层实现逻辑)
Promise 是 JavaScript 中用于处理异步操作的一种机制,它提供了更优雅的方式来处理异步代码,避免了回调地狱 。
-
使用new Promise() 创建
Promise
对象。 -
处理成功和失败,在执行器函数中进行异步操作,根据结果调用
resolve
或者reject
。 -
使用
then
和catch
处理结果,使用then
处理成功的情况,使用catch
处理失败的情况。
Promise不会阻塞UI线程,Promise设计的目的之一就是避免阻塞主线程。异步操作的执行不会阻塞 UI 线程,而是通过事件循环机制在后台执行。当异步操作完成后,它会调用相应的 resolve
或 reject
,然后触发注册的 then
或 catch
处理程序。
16. 子线程和主线程的消息传递
Worker开辟的子线程需要配合postMessage和onMessage实现消息传递;
TaskPool开辟的子线程通过回调传递进行传递;对持续订阅事件或单次订阅事件的处理、取消订阅事件、发送事件到事件队列等是通过Emitter发送和处理事件的。
-
子线程传递给主线程一个方法,是否支持?
不支持,自定义class的属性(如Function)无法通过序列化传递。
let func1 = function() { console.log("post message is function"); } // workerPort.postMessage(func1); 传递func1发生序列化错误
@ohos.worker (启动一个Worker)-语言基础类库-ArkTS接口参考-ArkTS API参考-HarmonyOS应用开发
17. 主线程上的数据对象序列化后传到子线程,子线程改变了对象的属性值,主线程跟着一起改变吗
不会,序列化是转换为字节码,已经和之前的对象没有关系了,就算反序列化成对象也是一个新对象。
-
序列化传输的数据量大小限制为16MB。
-
目前支持传输的数据对象可以分为普通对象、可转移对象、可共享对象、Native绑定对象四种。
18. then调用和await调用的区别
已融入ppt: 进阶day02-21
当使用Promise进行异步操作时,可以使用.then和await两种方式来调用异步接口;.then调用不阻塞当前线程,异步回调结束后会直接走到then中,await会等待当前异步方法执行结束才会继续往下执行。
参考答案二:
.then是promise接口的一个属性,可以通过.then方法链式的添加回调函数处理结果。
await是一个语法糖,是将后面的代码暂时挂起,等到任务执行完成后再继续执行。
在语法结构上也有不同:.then是通过方法链式调用处理Promise的结果,而await是异步函数内使用的关键字。
在处理错误的方式不同:.then是通过.catch方法来捕获Promise的拒绝结果,而await是通过try/catch块来捕获异步操作中的错误。
执行的顺序也不同:.then中的回调函数会在Promise完成后按顺序执行,而await会暂停当前函数的执行,直到等待Promise的解决或者拒绝。
19.鸿蒙在网络请求的时候如何校验CA(安全证书)
获取服务器证书->验证证书链->检查证书有效性->主机名验证 在鸿蒙系统中,这些步骤通常由底层网络库或框架来处理,而不需要应用程序开发者自己实现。
20.服务端响应期间调用三方库卡顿会不会卡页面渲染
过多引用三方库可能会造成卡顿
21.联系人中sdk的调用
联系人是通过调用full sdk(完整sdk)调用系统级别的功能,包含了public sdk(公共sdk)内容外还包含了系统底层的开发接口和功能,public sdk是包含了用于鸿蒙应用程序开发的基本组件和接口。适用于对资源要求较低、功能相对简单的应用场景。
22.promise异步并发遇到的场景有什么
比如并行请求多个资源的情况: 当需要同时获取多个资源时,可以使用Promise.all()方法将多个Promise对象包装成一个新的Promise对象,以便等待它们全部完成。
23.async是否允许修饰在生命周期方法前
语法不报错,但是不建议添加,可能会引起阻塞
三. 性能&优化
1. 页面性能内存优化***?
性能和内存是两个指标,有时候追求性能,就需要做缓存处理,吃内存。
页面内存优化,主要在减少缓存的处理。
页面性能优化,主要在少嵌套,减少动态渲染的频率。
-
精简节点数:
-
不要出现冗余节点,
-
比如有些效果,使用一个节点可以实现的,不要用多个节点
-
例如:一断文字,有间隔,完全可以使用一个Text,中间添加一些空格
-
-
扁平化减少中间的嵌套层级,使总的组件节点数减少。
-
使用更高级的布局使得页面变得更加扁平化
-
例如:RelativeContainer通过相对布局实现扁平化,不用Row嵌套Column,多层嵌套
-
-
17.arkTs语言高性能写法有哪些
-
变量声明
-
使用const声明常量
-
对于初期明确不会改变的变量,尽量使用const进行初始化,这里的常量包含基础类型和引用类型。通过const保证地址不会发生变化,能够极大减少由于编码时误操作导致的赋值等行为,造成对原有逻辑的改变,声明为const能够在编辑时及时发现错误。其中当const声明的是引用类型时,引用类型内部的属性变化是允许的,对于这种不存在地址变化的情况下,也建议使用const声明。
-
-
指定number的类型
-
对于number类型,编译器在优化时会区分int和double类型。开发者在初始化number类型的变量时,如果预期是整数类型就初始化为0,小数类型就初始化为0.0,避免将一个number类型初始化为undefined或者null。
-
-
减少使用ESObject
-
ESObject主要用于在ArkTS和TS/JS跨语言调用的场景中作为类型标注,在非跨语言场景中使用ESObject标注类型,会引入不必要的跨语言调用,造成额外的性能开销,建议在非跨语言调用的场景下,避免使用ESObject,引入明确的类型进行注释。
-
-
-
属性访问
-
减少变量的属性查找
-
在要求性能的场景下,建议通过使用将全局变量存储为局部变量的方式来减少全局查找,因为访问局部变量的速度要比访问全局变量的速度更快。重复的访问同一个变量,将造成不必要的消耗,尤其当类似的访问出现在循环过程中,其对于性能的影响更大。
-
-
给类属性添加访问修饰符
-
在ArkTS中,对于类结构的属性提供了private、protected和public可访问修饰符。默认情况下一个属性的可访问修饰符为public。选取适当的可访问修饰符可以提升代码的安全性、可读性。
-
-
-
数值计算
-
数值计算使用TypedArray
-
如果是纯数值计算的场合,推荐使用TypedArray数据结构。TypedArray类型化数组是一种类似数组的对象,其提供了一种用于在内存缓冲中访问原始二进制数据的机制。在一些图像数据处理、加解密的数据计算过程中使用TypedArray可以提高数据处理的效率,因为TypedArray是基于ArrayBuffer实现,在性能方面也能够进行较大提升。
-
-
-
数据结构的使用
-
选取合适的数据结构
-
有些时候会采用Record的方式作为临时容器来处理属性存取的逻辑,例如如下案例中,对于info执行的操作是set存储以及读取的操作,这里更好的方式是采用标准内置Map以及基础类库提供的高性能容器类如HashMap。HashMap是ArkTS提供的高性能容器类,底层使用红黑树实现,提供了高性能的数据读写操作,可以用来实现快速读写键值。
-
-
避免造成稀疏数组
-
分配数组时,需要避免使用如下的方式进行处理。当虚拟机在分配大小超过1024大小的数组时,会变成用哈希表来存储元素,相对数组用偏移来访问元素速度较慢,在开发时,尽量避免数组变成稀疏数组。
-
-
-
函数声明与使用
-
函数内部变量尽量使用参数传递
-
能传递参数的尽量传递参数,不要使用闭包,闭包作为参数会多一次闭包的创建和访问。在普通函数中,修改外部作用域的变量时,建议通过函数的参数传递,因为在直接声明时引用外部作用域的变量,如果没有及时清理,可能有内存泄漏的风险。ArkTS中函数参数是引用类型时,作用于引用类型的修改会进行引用传递,函数内对形参的修改也会作用在实参上。
-
-
-
参考链接:文档中心
6. 页面布局上的性能和内存上的注意事项
1、使用row/column+layoutweight代替flex容器使用 2、scroll嵌套list/grid容器时,要设置容器的宽高,数组数据渲染尽量使用lazyforeach渲染item 3、组件的显隐设置,要使用if语句来判断,避免使用visibility 4、list/grid容器要根据具体场景来使用cachecount,避免卡顿
1.使用row/column代替flex容器使用
-
由于Flex容器组件默认情况下存在shrink导致二次布局,这会在一定程度上造成页面渲染上的性能问题。
-
二次布局到底如何观测? harmony 鸿蒙Flex布局性能提升使用指导
-
代码观测,目前还不清楚,组件和页面的声明周期无法观测
@Entry @Component struct MyComponent { build() { Flex({ direction: FlexDirection.Column }) { Flex().width(300).height(200).backgroundColor(Color.Pink) Flex().width(300).height(200).backgroundColor(Color.Yellow) Flex().width(300).height(200).backgroundColor(Color.Grey) } } }
-
上述代码可将Flex替换为Column、Row,在保证实现的页面布局效果相同的前提下避免Flex二次布局带来的负面影响。
@Entry @Component struct MyComponent { build() { Column() { Row().width(300).height(200).backgroundColor(Color.Pink) Row().width(300).height(200).backgroundColor(Color.Yellow) Row().width(300).height(200).backgroundColor(Color.Grey) } } }
-
效果
2. 使用row/column代替flex容器使用
-
在使用Scroll容器组件嵌套List组件加载长列表时,若不指定List的宽高尺寸,则默认全部加载。
-
List没有设置宽高,会布局List的所有子组件。
-
List设置宽高,会布局List显示区域内的子组件。
-
List使用ForEach加载子组件时,无论是否设置List的宽高,都会加载所有子组件。
-
List使用LazyForEach加载子组件时,没有设置List的宽高,会加载所有子组件,设置了List的宽高,会加载List显示区域内的子组件。
-
-
具体演示如下:
-
使用LazyForEach,但不知道宽高 , 加载了所有 (怎么观察加载了所有,通过Tt审查元素)
-
使用LazyForEach,同时指定宽高 , 发现只加载了显示区域内的子item
-
参考链接: harmony 鸿蒙性能提升的其他方法
-
完整代码: ScrollContainListPage.ets
class BasicDataSource implements IDataSource { private listeners: DataChangeListener[] = [] public totalCount(): number { return 0 } public getData(index: number): number { return index } registerDataChangeListener(listener: DataChangeListener): void { if (this.listeners.indexOf(listener) < 0) { console.info('add listener') this.listeners.push(listener) } } unregisterDataChangeListener(listener: DataChangeListener): void { const pos = this.listeners.indexOf(listener); if (pos >= 0) { console.info('remove listener') this.listeners.splice(pos, 1) } } notifyDataReload(): void { this.listeners.forEach(listener => { listener.onDataReloaded() }) } notifyDataAdd(index: number): void { this.listeners.forEach(listener => { listener.onDataAdd(index) }) } notifyDataChange(index: number): void { this.listeners.forEach(listener => { listener.onDataChange(index) }) } notifyDataDelete(index: number): void { this.listeners.forEach(listener => { listener.onDataDelete(index) }) } notifyDataMove(from: number, to: number): void { this.listeners.forEach(listener => { listener.onDataMove(from, to) }) } } class MyDataSource extends BasicDataSource { private dataArray: Array<string> = new Array(100).fill('test') public totalCount(): number { return this.dataArray.length } public getData(index: number): number { return Number(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) } } @Entry @Component struct MyComponent { private data: MyDataSource = new MyDataSource() build() { Scroll() { List() { LazyForEach(this.data, (item: string, index?: number|undefined) => { ListItem() { if(index){ Text('item value: ' + item + (index + 1)).fontSize(20).margin(10) } }.width('100%') }) }.width('100%').height(500) }.backgroundColor(Color.Pink) } }
3. 组件的显隐设置,要使用if语句来判断,避免使用visibility
-
VisiblePage.ets
4. list/grid容器要根据具体场景来使用cachedcount,避免卡顿 (看官网的list组件有cachedcount的介绍)
7.页面性能优化
TODO:这个等到最佳实践处理完之后,完善
-
指定number的类型
对于number类型,编译器在优化时会区分int和double类型。开发者在初始化number类型的变量时,如果预期是整数类型就初始化为0,小数类型就初始化为0.0,避免将一个number类型初始化为undefined或者null
-
减少变量的属性查找
在要求性能的场景下,建议通过使用将全局变量存储为局部变量的方式来减少全局查找,因为访问局部变量的速度要比访问全局变量的速度更快。重复的访问同一个变量,将造成不必要的消耗,尤其当类似的访问出现在循环过程中,其对于性能的影响更大
-
数值计算使用TypedArray
如果是纯数值计算的场合,推荐使用TypedArray数据结构。TypedArray类型化数组是一种类似数组的对象,其提供了一种用于在内存缓冲中访问原始二进制数据的机制。在一些图像数据处理、加解密的数据计算过程中使用TypedArray可以提高数据处理的效率,因为TypedArray是基于ArrayBuffer实现,在性能方面也能够进行较大提升。
-
选取合适的数据结构
使用推荐的高性能容器
-
避免造成稀疏数组
分配数组时,需要避免使用如下的方式进行处理。当虚拟机在分配大小超过1024大小的数组时,会变成用哈希表来存储元素,相对数组用偏移来访问元素速度较慢,在开发时,尽量避免数组变成稀疏数组。 // 下面情形会变成稀疏数组 // 1. 直接分配100000大小的数组,虚拟机会处理成用hash表来存储元素 let count = 100000; let result: number[] = new Array(count); // 2. 分配数组之后直接,在9999处初始化,会变成稀疏数组 result[9999] = 0;
5.如果简历中有写相关性能优化得东西,描述一下项目中是如何做得
-
布局性能优化:
-
减少嵌套层级: 使用扁平化布局优化嵌套层级
-
开发者在实现布局的时候,常使用线性布局来实现效果,这可能会造成多级嵌套。建议采用相对布局RelativeContainer进行扁平化布局,有效减少容器的嵌套层级,减少组件的创建时间
-
-
参考链接: harmony 鸿蒙优化布局性能
四. 其他:
1. hap,har和hsp有啥区别***,自己平时是怎么打包的?有没有单独编写脚本打包?
har和hsp区别?只有体积大小的区别吗? 解决体积膨胀的问题,har是安装包直接安装在进程里
-
hap,har和hsp有啥区别?
-
首先需要搞清楚鸿蒙项目的模块Module的分类: Module分为“Ability”和“Library”两种类型
-
HAP
-
HAP: Harmony Ability Package , 叫做鸿蒙Ability包
-
“Ability”类型的Module编译后叫做HAP。
-
一个HAP , 它是由代码、资源、第三方库及应用/服务配置文件组成,HAP可以分为Entry和Feature两种类型。
-
Entry是主模块 , Feature是动态特性模块。
-
-
HAR
-
HAR: Harmony Archive /ˈɑːkaɪv/, 叫做鸿蒙静态共享包。
-
“Library”类型的Module编译后叫做HAR , 或者 HSP。
-
一个HAR , 它可以包含代码、C++库、资源和配置文件。
-
HAR不同于HAP,不能独立安装运行在设备上,只能作为应用模块的依赖项被引用。
-
-
HSP
-
HSP: Harmony Shared Package , 叫做鸿蒙动态共享包。
-
一个HSP , 它可以包含代码、C++库、资源和配置文件。
-
HSP依然不能独立运行 , 需要作为HAP的依赖项。
-
HSP不同于HAR , 可以被多个HAP同时引用 , HSP旨在解决多个模块引用相同的HAR,导致APP包大小膨胀的问题。
-
-
-
自己平时是怎么打包的
-
一般应用直接使用DevEco进行打包即可:
Build Hap(s)
-
-
有没有单独编写脚本打包?
-
如果此应用针对不同应用市场 , 有不同的调整 , 比如应用闪屏 , 那么就需要使用脚本打包
-
具体介绍:
-
除了使用DevEco Studio一键式构建应用/服务外,还可以使用命令行工具来调用Hvigor任务进行构建
-
首选需要搭建构建环境 , 安装node.js , jdk , sdkmgr
-
配置sdk环境变量
-
最终编写打包脚本:
./hvigorw clean assembleApp --no-daemon
-
以上所有的步骤 , 都可以通过脚本来处理
#!/bin/bash set -ex NODE_HOME=xxx #指定Node.js的安装目录 JAVA_HOME=xxx #指定JDK的安装目录 COMMANDLINE_TOOL_DIR=xxx #命令行工具的安装目录 HOS_SDK_HOME=xxx #HarmonyOS SDK根路径 #下载并配置Node.js function init_Node() { if [ ! -d "${NODE_HOME}" ]; then mkdir "${NODE_HOME}" fi cd ${NODE_HOME} wget --no-check-certificate -q "${node下载路径}" -O node-linux.tar.xz #下载node,需要替换node下载路径 tar -vxf node-linux.tar.xz NODE_DIR=xxx #node压缩包文件里面的目录 cd ${NODE_DIR} mv -f ./* .[^.]* ../ cd .. rm -rf NODE_DIR node-linux.tar.xz export NODE_HOME=${NODE_HOME} export PATH=$NODE_HOME/bin:$PATH node -v npm config set registry=https://repo.huaweicloud.com/repository/npm/ npm config set @ohos:registry=https://repo.harmonyos.com/npm/ npm config get @ohos:registry npm config set proxy=http://user:password@proxy.server.com:port #配置npm http代理,企业网络受限的情况下需要配置 npm config set https-proxy=http://user:password@proxy.server.com:port #配置npm https代理,企业网络受限的情况下需要配置 npm info express } #下载并配置JDK function init_JDK() { if [ ! -d "${JAVA_HOME}" ]; then mkdir "${JAVA_HOME}" fi cd ${JAVA_HOME} wget --no-check-certificate -q "${jdk下载路径}" -O jdk-linux.tar.xz #下载jdk,需要替换jdk下载路径 tar -vxf jdk-linux.tar.xz JDK_DIR=xxx #jdk压缩包文件里面的目录 cd ${JDK_DIR} mv -f ./* .[^.]* ../ cd .. rm -rf JDK_DIR jdk-linux.tar.xz export JAVA_HOME=${JAVA_HOME} export PATH=$JAVA_HOME/bin:$PATH java -version } #配置SDK(已获取离线SDK并解压完成) function init_SDK() { export HDC_HOME=/opt/HarmonyOS/SDK/openharmony/10/toolchains #设置hdc工具的环境变量,hdc工具在toolchains所在路径下,请以实际路径为准 export PATH=$HDC_HOME:$PATH export OHOS_NATIVE_HOME=/opt/HarmonyOS/SDK/openharmony/10/native #如果工程中涉及C/C++,才需要设置,指向Native SDK所在的目录 export HOS_SDK_HOME=${HOS_SDK_HOME} } # 安装ohpm, 若镜像中已存在ohpm,则无需重新安装 function init_ohpm() { # 初始化ohpm OHPM_HOME=${COMMANDLINE_TOOL_DIR}/command-line-tools/ohpm export PATH=${OHPM_HOME}/bin:$PATH ohpm -v # 配置ohpm仓库地址 ohpm config set registry=https://ohpm.openharmony.cn/ohpm/ } # 初始化相关路径 PROJECT_PATH=xxx # 工程目录 # 进入package目录安装依赖 function ohpm_install { cd $1 ohpm install } # 环境适配 function buildHAP() { # 根据业务情况适配local.properties cd ${PROJECT_PATH} echo "hwsdk.dir=${HOS_SDK_HOME}" > ./local.properties # 根据业务情况安装ohpm三方库依赖 ohpm_install "${PROJECT_PATH}" ohpm_install "${PROJECT_PATH}/entry" ohpm_install "${PROJECT_PATH}/xxx" # 如果构建过程报错 ERR_PNPM_OUTDATED_LOCKFILE,需要增加配置:lockfile=false, 根据node版本选择设置方式: # node.version<18 npm config set lockfile=false # 如果执行此命令报错,建议直接在镜像的.npmrc文件中需要增加一行配置:lockfile=false # node.version>=18 #cat ${HOME}/.npmrc | grep 'lockfile=false' || echo 'lockfile=false' >> ${HOME}/.npmrc # 根据业务情况,采用对应的构建命令,可以参考IDE构建日志中的命令 cd ${PROJECT_PATH} chmod +x hvigorw ./hvigorw clean --no-daemon ./hvigorw assembleHap --mode module -p product=default -p debuggable=false --no-daemon # 流水线构建命令建议末尾加上--no-daemon } function install_hap() { hdc file send "${PROJECT_PATH}/entry/build/default/outputs/default/entry-default-signed.hap" "data/local/tmp/entry-default-signed.hap" hdc shell bm install -p "data/local/tmp/entry-default-signed.hap" hdc shell rm -rf "data/local/tmp/entry-default-signed.hap" hdc shell aa start -a MainAbility -b com.example.myapplication -m entry } # 使用ohpm发布har function upload_har { ohpm publish pkg.har } function main { local startTime=$(date '+%s') init_Node init_JDK init_SDK init_ohpm buildHAP install_hap upload_har local endTime=$(date '+%s') local elapsedTime=$(expr $endTime - $startTime) echo "build success in ${elapsedTime}s..." } main
-
-
参考链接: 文档中心
-
2. 三方应用调用系统应用,对于ability的交互和传值有什么限制?除了数据大小方面
重点介绍自己对ability的理解,描述显式want和隐式want的区别,带入到对应面试项目中场景来 启动应用内的UIAbility 启动应用内的UIAbility并获取返回结果 启动其他应用的UIAbility 启动其他应用的UIAbility并获取返回结果 启动UIAbility的指定页面 显式Want启动:在want参数中需要设置该应用bundleName和abilityName,当需要拉起某个明确的UIAbility时,通常使用显式Want启动方式。 隐式Want启动:不明确指出要启动哪一个UIAbility,在调用startAbility()方法时,其入参want中指定了一系列的entities字段(表示目标UIAbility额外的类别信息,如浏览器、视频播放器)和actions字段(表示要执行的通用操作,如查看、分享、应用详情等)等参数信息,然后由系统去分析want,并帮助找到合适的UIAbility来启动。
==三方应用调用系统应用== , 需要用到want , want分为显示和隐式
-
显式Want:在启动目标应用组件时,调用方传入的want参数中指定了abilityName和bundleName,称为显式Want。
//这个代码可以不用详细介绍,不然就跑题了,这里给大家提供代码是为了方便大家回顾 import Want from '@ohos.app.ability.Want'; let wantInfo: Want = { deviceId: '', // deviceId为空表示本设备 bundleName: 'com.example.myapplication', abilityName: 'FuncAbility', }
-
隐式Want:在启动目标应用组件时,调用方传入的want参数中未指定abilityName,称为隐式Want。
//这个代码可以不用详细介绍,不然就跑题了,这里给大家提供代码是为了方便大家回顾 import Want from '@ohos.app.ability.Want'; let wantInfo: Want = { // uncomment line below if wish to implicitly query only in the specific bundle. // bundleName: 'com.example.myapplication', action: 'ohos.want.action.search', // entities can be omitted entities: [ 'entity.system.browsable' ], uri: 'https://www.test.com:8080/query/student', type: 'text/plain', };
-
在调用startAbility()方法时,其入参want中指定了一系列的entities字段(表示目标UIAbility额外的类别信息,如浏览器、视频播放器)和actions字段(表示要执行的通用操作,如查看、分享、应用详情等)等参数信息,
-
然后由系统去分析want,并帮助找到合适的UIAbility来启动。
-
-
回答问题时,尽量与自己的项目/经历关联起来
==关于传值== , 我之前做过授权的功能 , 如果用户第一次拒绝授权,第二次再次进去此页面,则可以直接引导用户调到应用设置界面
-
跳转到应用设置之后的效果如下
==对数据类型的限制==,目前支持的数据类型有:字符串、数字、布尔、对象、数组和文件描述符等。
3. 项目中是否使用过泛型, 结合项目使用展开
-
首选了解一下, 什么是泛型
-
泛型(Generics)是一种编程语言特性,用于支持类型参数化。
-
具体而言,它允许用户在定义类、接口和方法时使用一个或多个类型参数,这些参数表示一种或多种类型而非具体的类型。
-
在使用泛型时,可以使用任何符合泛型约束的类型作为类型参数,使得代码重用性更高,更安全,更可读。泛型在许多现代编程语言中得到广泛的支持和应用
-
理解: 简单一点就是 , 可以在定义接口时 ,可以让==参数的类型更加灵活== , 使得功能更加强大
-
-
项目中使用泛型
-
在项目中 , 使用
LazyForEach
循环ListItem
的时候 , 这个LazyForEach
的数据源必须实现IDataSource
接口 -
而使用
LazyForEach
循环遍历的数据其实是不同数据实体对象的数组 -
而这些数组 , 都得转换为实现了
IDataSource
的数据源 , 所以这时候我们就可以搞一个公共的数据源CommonDataSource
-
代码如下
export class CommonDataSource<T> implements IDataSource { private dataArray: T[] = []; private listeners: DataChangeListener[] = []; constructor(element: T[]) { this.dataArray = element; } public getData(index: number) { return this.dataArray[index] } public totalCount(): number { return this.dataArray.length; } public addData(index: number, data: T[]): void { this.dataArray = this.dataArray.concat(data); this.notifyDataAdd(index); } public pushData(data: T): void { this.dataArray.push(data); this.notifyDataAdd(this.dataArray.length - 1); } ... }
-
页面使用时
//页面1 LazyForEach(new CommonDataSource<Article>(this.articleList), (item: Article) => { ListItem() { } }) //页面2 LazyForEach(new CommonDataSource<Comment>(this.commentList), (item: Comment) => { ListItem() { } })
-
4. 从介绍中延伸出har和下沉到系统的so的优缺点
har分为静态分享库和动态分享库,静态分享库生成har包集成到应用中,应用运行时直接加载到进程中,动态分享库生成hsp和har包,har集成到应用中,hsp需要安装到手机设备中,应用运行时,har直接加载到进程中,但hsp需要业务调用才会加载; so是c++和arkts的混编,arkts开发完成后,编译生成对应的js文件,使用华为工具将js文件和NAPI混编为so; 目前api10和api11不支持har包下沉到系统称为预置SDK,仅支持so。
待完善
12、了解过鸿蒙的Napi吗?如果我需要编译三方库如何链接? 组件是一套对外接口基于Node.js N-API规范开发的原生模块扩展开发框架在HarmonyOS中, C API中的N-API接口可以实现ArkTS/TS/JS与C/C++之间的交互。N-API提供的接口名与三方Node.js一致,目前支持部分接口
10、鸿蒙如何实现自定义Canvas?底层如何进行Ui的渲染 flutter skia 11、Navigation组件和NavPathStack使用
8.Record 和 map 的区别
-
应用场景对比
-
Record 是属于一个轻量级的 type 类型 , Map 相对 Record 是重量级。
-
Map 不能像 Record 一样直接转换为普通的对象,Record 主要应对只有查询的实际问题,只是为了频繁的查询去 new 一个 Map 是一种不必要的浪费。
-
如果读取数据和显示数据频繁,就应该采用 Record。
-
如果增删改比较多,那还是使用 Map
-
-
语法对比
-
Record就是强类型的Map
-
不用Record的写法
const obj: { [key: string ] : number } = {}
-
用了Record的写法
//比如我需要一个对象,有 ABC 三个属性,属性的值必须是数字,那么就这么写: type keys = 'A' | 'B' | 'C' const result: Record<keys, number> = { A: 1, B: 2, C: 3 }
-
总结:
-
map比record灵活,mas可以任意添加key和value,record就不行,一旦定义了record,就只能使用定义里的字段,因此,可以认为record是强类型的
-
record在idea下有语法提示,maps如果不小心把key写错了,会导致运行的时候匹配或get出错
-
最重要的一点,maps的key可以是任意数据类型,而record的key只能是原子
-
-
9.string array map 和 object 关系
-
一切皆为对象 , 所以无论是内置对象,还是自定义对象,都是继承自Object
-
内置对象中有 string array map等
14.踩坑文档有什么内容,印象最深的问题是啥
有时候官方文档不够详细。比如:
-
组件样式问题
-
web打开网页模拟机不能交互。只有真机可以使用交互。
-
通知,数据存储
-
-
hdc环境问题
-
previewer和模拟器的问题(印象最深的,坑人的环境)
-
有时候previewer的代码不会更新,明明修改过了,preivewer还是报错,重新rebuild project也不行,这时候可以删除工程目录中的.preivew目录即可
-
有时候模拟器出问题,效果出不来(权限验证的弹框),删除模拟器重新创建一个好了
-
-
list的高度默认是占满全屏幕高度 , 如果同级存在固定高度的组件时 , 需要开发人员显性指定List组件占满剩余高度 , 而不是全屏幕高度 , 一般用layoutWeight(1)(待验证 , 这个问题没有复现)
-
list如果不指定高度 , 则无法滚动 , 如果想要占剩余空间使用
layoutWeight(1)
-
15.加密怎么实现的,如何存储
关于数据库加密:键值型数据库和关系型数据库均支持数据库加密操作,都是在创建数据库的时候通过options配置参数来开启加密。加密之后则无法通过其他方式打开数据库文件,只能通过接口访问。
-
键值型数据库,通过options中encrypt参数来设置是否加密,默认为false,表示不加密。encrypt参数为true时表示加密。
-
关系型数据库,通过options中encrypt属性来设置是否加密,默认为false,表示不加密。 encrypt参数为true时表示加密。
-
关于上边的链接中的案例,数据加密使用到的是api3里的cipher,但是此接口已经在api9废弃了
-
具体案例
-
代码待研究,有点复杂
-
CipherModel:rsaEncrypt , rsaDecrypt