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组件,适用于存储包含==复杂关系数据==的场景,比如一个班级的学生信息,需要包括姓名、学号、各科成绩等,又或者公司的雇员信息,需要包括姓名、工号、职位等,由于数据之间有较强的对应关系,复杂程度比键值型数据更高,此时需要使用关系型数据库来持久化保存数据。
运作机制:
<img src="0.Harmony面试题.assets/image-20240521094339897.png" alt="image-20240521094339897" style="zoom: 33%;" />
约束:
- 数据库中有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: 当父容器空间不足时,子元素的压缩比例。
参考链接:https://www.seaxiang.com/blog/8bf810f30c9f4fe7a24fb55c4778ed6c
-
优化
- 使用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装饰的自定义组件具备可复用能力
- 参考链接:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-create-custom-components-0000001820999549#ZH-CN_TOPIC_0000001820999549__%E8%87%AA%E5%AE%9A%E4%B9%89%E7%BB%84%E4%BB%B6%E7%9A%84%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95
-
自定义组件怎么设计,有两种情况
- 首先公司内部将常用的一些效果 , 都封装了组件
- 比如: 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如何计算,参考:https://www.jianshu.com/p/f64eb6613d35
自己总结公式:
溢出值:所有子元素宽度相加 - 父元素宽度
总权重:子元素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定义方法
-
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" onclick="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>
-
参考链接: https://blog.csdn.net/Lu_Ca/article/details/135285413或官网https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/web-in-page-app-function-invoking-0000001774279950
代码在InterviewQuestion项目中的WebComponent2
-
注意:
- 应用端通过消息端口(WebMessagePort):发送消息使用
postMessageEvent
,接收消息使用onMessageEvent
- html端通过消息端口:发送消息使用
postMessage
,接收消息监听事件onmessage
(都少了event)
问题1: 模拟器中web加载网页后,网页的按钮无法点击。
在模拟器中,是无法测试html端---应用端的数据传递。
- 本地的模拟器现在不支持webview
- 可以使用远程模拟器,或远程真机
问题2: 在将端口0发送到html端时,使用的方法是postMessage
,而真正发送数据是通过postMessageEvent
,区别在哪?
- postMessage就是用于发送消息端口的
- postMessageEvent就是用于发送消息的