DEV Community

Freerain
Freerain

Posted on

鸿蒙Next下页面级存储LocalStorage用法全解析

一、概述

LocalStorage是ArkTS为构建页面级别状态变量提供存储的内存内的“数据库”。应用程序可以创建多个LocalStorage实例,其支持UIAbility实例内多个页面间状态共享,可在页面内共享,也能通过GetShared接口跨页面共享。组件树的根节点(被@Entry装饰的@Component)可被分配LocalStorage实例,其所有子组件实例将自动获得访问权限。LocalStorage中的属性都是可变的,其生命周期由应用程序决定。

二、限制条件

  1. 参数类型要求:@LocalStorageProp和@LocalStorageLink的参数必须为string类型,否则编译期会报错。例如:
   let storage = new LocalStorage();
   storage.setOrCreate('PropA', 48);
   // 错误写法,编译报错
   @LocalStorageProp() localStorageProp: number = 1;
   @LocalStorageLink() localStorageLink: number = 2;
   // 正确写法
   @LocalStorageProp('PropA') localStorageProp: number = 1;
   @LocalStorageLink('PropA') localStorageLink: number = 2;
Enter fullscreen mode Exit fullscreen mode
  1. 不支持Function类型变量:@StorageProp与@StorageLink不支持装饰Function类型的变量,框架会抛出运行时错误。
  2. 属性类型不可更改:LocalStorage创建后,命名属性的类型不可更改。后续调用Set时必须使用相同类型的值。
  3. 页面级存储限制:getShared接口仅能获取当前Stage通过windowStage.loadContent传入的LocalStorage实例,否则返回undefined。

三、@LocalStorageProp装饰器

(一)装饰器使用规则

  1. 参数要求
    • key为常量字符串,必填且需带引号。
    • 允许装饰的变量类型包括Object、class、string、number、boolean、enum类型及其数组,API12及以上支持Map、Set、Date类型等,不支持any,API12及以上支持undefined和null类型(建议显式指定类型)。例如:
   @LocalStorageProp("AA") a: number | null = null; // 推荐
   @LocalStorageProp("AA") a: number = null; // 不推荐
Enter fullscreen mode Exit fullscreen mode
  1. 同步类型:与LocalStorage中key对应的属性建立单向数据同步,从LocalStorage到组件状态变量。即ArkUI框架支持修改@LocalStorageProp(key)在本地的值,但本地值的修改不会同步回LocalStorage中;而LocalStorage中key对应的属性值发生改变时,会同步给@LocalStorageProp(key)并覆盖本地值。
  2. 初始值要求:必须指定,若LocalStorage实例中不存在属性,则用该初始值初始化并存入LocalStorage。

(二)变量传递/访问规则

  1. 禁止从父节点初始化和更新:只能从LocalStorage中key对应的属性初始化,无对应key时用本地默认值。
  2. 支持初始化子节点:可用于初始化@State、@link@prop、@Provide。
  3. 不支持组件外访问

(三)观察变化和行为表现

  1. 观察变化类型
    • boolean、string、number类型可观察数值变化。
    • class或Object类型可观察对象整体赋值和属性变化。
    • array类型可观察数组添加、删除、更新单元变化。
    • Date类型可观察整体赋值及通过相关接口更新属性。
    • Map类型可观察整体赋值及通过接口更新值。
    • Set类型可观察整体赋值及通过接口更新值。
  2. 框架行为
    • 组件内变量值变化不回写LocalStorage。
    • 变量变化使关联组件刷新。
    • LocalStorage中值变化会覆盖本地修改。

四、@LocalStorageLink装饰器

(一)装饰器使用规则

  1. 参数要求:同@LocalStorageProp,key为常量字符串,必填且带引号,变量类型要求相同。
  2. 同步类型:与LocalStorage中key对应的属性建立双向数据同步,即本地修改会写回LocalStorage,LocalStorage中的修改也会同步到绑定的属性上(包括单向和双向绑定变量)。
  3. 初始值要求:必须指定,若LocalStorage实例中不存在属性,则用该初始值初始化并存入LocalStorage。

(二)变量传递/访问规则

  1. 禁止从父节点初始化和更新:同@LocalStorageProp。
  2. 支持初始化子节点:同@LocalStorageProp。
  3. 不支持组件外访问

(三)观察变化和行为表现

  1. 观察变化类型:与@LocalStorageProp相同。
  2. 框架行为
    • 组件内数值改变同步回LocalStorage。
    • LocalStorage中值改变,绑定的数据(包括双向和单向)同步修改。
    • 装饰的数据本身是状态变量时,改变会引起所属自定义组件重新渲染。

五、使用场景

(一)应用逻辑使用LocalStorage

通过创建LocalStorage实例,使用get、link、prop等接口操作属性,如:

let para: Record<string,number> = { 'PropA': 47 };
let storage: LocalStorage = new LocalStorage( para);
let propA: number | undefined = storage.get('PropA');
let link1: SubscribedAbstractProperty<number> = storage.link('PropA');
let link2: SubscribedAbstractProperty<number> = storage.link('PropA');
let prop: SubscribedAbstractProperty<number> = storage.prop('PropA');
link1.set(48);
prop.set(1);
link1.set(49);
Enter fullscreen mode Exit fullscreen mode

(二)从UI内部使用LocalStorage

借助@LocalStorageProp和@LocalStorageLink在UI组件内部获取状态变量,如:

class PropB {
 code: number;
 constructor(code: number) {
   this.code = code;
 }
}
let para: Record<string, number> = { 'PropA': 47 };
let storage: LocalStorage = new LocalStorage( para);
storage.setOrCreate('PropB', new PropB(50));
@Component
struct Child {
 @LocalStorageLink('PropA') childLinkNumber: number = 1;
 @LocalStorageLink('PropB') childLinkObject: PropB = new PropB(0);
 build() {
   Column() {
     Button(`Child from LocalStorage ${this.childLinkNumber}`).onClick(() => {this.childLinkNumber += 1;})
     Button(`Child from LocalStorage ${this.childLinkObject.code}`).onClick(() => {this.childLinkObject.code += 1;})
   }
 }
}
@Entry(storage)
@Component
struct CompA {
 @LocalStorageLink('PropA') parentLinkNumber: number = 1;
 @LocalStorageLink('PropB') parentLinkObject: PropB = new PropB(0);
 build() {
   Column({ space: 15 }) {
     Button(`Parent from LocalStorage ${this.parentLinkNumber}`).onClick(() => {this.parentLinkNumber += 1;})
     Button(`Parent from LocalStorage ${this.parentLinkObject.code}`).onClick(() => {this.parentLinkObject.code += 1;})
     Child()
   }
 }
}
Enter fullscreen mode Exit fullscreen mode

(三)@LocalStorageProp和LocalStorage单向同步场景

在CompA组件和Child组件中创建与storage的'PropA'单向同步数据,如:

let para: Record<string, number> = { 'PropA': 47 };
let storage: LocalStorage = new LocalStorage( para);
@Entry(storage)
@Component
struct CompA {
 @LocalStorageProp('PropA') storageProp1: number = 1;
 build() {
   Column({ space: 15 }) {
     Button(`Parent from LocalStorage ${this.storageProp1}`).onClick(() => {this.storageProp1 += 1})
     Child()
   }
 }
}
@Component
struct Child {
 @LocalStorageProp('PropA') storageProp2: number = 2;
 build() {
   Column({ space: 15 }) {
     Text(`Parent from LocalStorage ${this.storageProp2}`)
   }
 }
}
Enter fullscreen mode Exit fullscreen mode

(四)@LocalStorageLink和LocalStorage双向同步场景

如:

let para: Record<string, number> = { 'PropA': 47 };
let storage: LocalStorage = new LocalStorage( para);
let linkToPropA: SubscribedAbstractProperty<object> = storage.link('PropA');
@Entry(storage)
@Component
struct CompA {
 @LocalStorageLink('PropA') storageLink: number = 1;
 build() {
   Column() {
     Text(`incr @LocalStorageLink variable`).onClick(() => {this.storageLink += 1})
     Text(`@LocalStorageLink: ${this.storageLink} - linkToPropA: ${linkToPropA.get()}`)
   }
 }
}
Enter fullscreen mode Exit fullscreen mode

(五)兄弟组件之间同步状态变量

通过@LocalStorageLink实现兄弟组件状态同步,如:

let ls: Record<string, number> = { 'countStorage': 1 }
let storage: LocalStorage = new LocalStorage(ls);
@Component
struct Child {
 label: string = 'no name';
 @LocalStorageLink('countStorage') playCountLink: number = 0;
 build() {
   Row() {
     Text(this.label).width(50).height(60).fontSize(12)
     Text(`playCountLink ${this.playCountLink}: inc by 1`).onClick(() => {this.playCountLink += 1;})
   }.width(300).height(60)
 }
}
@Entry(storage)
@Component
struct Parent {
 @LocalStorageLink('countStorage') playCount: number = 0;
 build() {
   Column() {
     Row() {
       Text('Parent').width(50).height(60).fontSize(12)
       Text(`playCount ${this.playCount} dec by 1`).onClick(() => {this.playCount -= 1;})
     }.width(300).height(60)
     Row() {
       Text('LocalStorage').width(50).height(60).fontSize(12)
       Text(`countStorage ${this.playCount} incr by 1`).onClick(() => {storage.set<number | undefined>('countStorage', Number(storage.get<number>('countStorage')) + 1);})
     }.width(300).height(60)
     Child({ label: 'ChildA' })
     Child({ label: 'ChildB' })
     Text(`playCount in LocalStorage for debug ${storage.get<number>('countStorage')}`).width(300).height(60).fontSize(12)
   }
 }
}
Enter fullscreen mode Exit fullscreen mode

(六)将LocalStorage实例从UIAbility共享到多个视图

在UIAbility中创建LocalStorage实例并调用windowStage.loadContent共享,在页面中通过getShared获取,如:

  1. EntryAbility.ets中
import { UIAbility } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
export default class EntryAbility extends UIAbility {
para:Record<string, number> = { 'PropA': 47 };
storage: LocalStorage = new LocalStorage(this.para);
onWindowStageCreate(windowStage: window.WindowStage) {
windowStage.loadContent('pages/Index', this.storage);
}
}
Enter fullscreen mode Exit fullscreen mode
  1. index.ets中
import { router } from '@kit.ArkUI';
let storage = LocalStorage.getShared()
@Entry(storage)
@Component
struct Index {
 @LocalStorageLink('PropA') propA: number = 1;
 build() {
   Row() {
     Column() {
       Text(`${this.propA}`).fontSize(50).fontWeight(FontWeight.Bold)
       Button("To Page").onClick(() => {
         this.getUIContext().getRouter().pushUrl({
           url: 'pages/Page'
         })
       })
     }.width('100%')
   }.height('100%')
 }
}
Enter fullscreen mode Exit fullscreen mode
  1. Page.ets中
import { router } from '@kit.ArkUI';
let storage = LocalStorage.getShared()
@Entry(storage)
@Component
struct Page {
 @LocalStorageLink('PropA') propA: number = 2;
 build() {
   Row() {
     Column() {
       Text(`${this.propA}`).fontSize(50).fontWeight(FontWeight.Bold)
       Button("Change propA").onClick(() => {this.propA = 100;})
       Button("Back Index").onClick(() => {this.getUIContext().getRouter().back()})
     }.width('100%')
   }
 }
}
Enter fullscreen mode Exit fullscreen mode

(七)自定义组件接收LocalStorage实例

  1. 定义属性时接收
    • 实例必须放在第二个参数位置传递,否则报错。
    • 如:
let localStorage1: LocalStorage = new LocalStorage();
localStorage1.setOrCreate('PropA', 'PropA');
let localStorage2: LocalStorage = new LocalStorage();
localStorage2.setOrCreate('PropB', 'PropB');
@Entry(localStorage1)
@Component
struct Index {
 @LocalStorageLink('PropA') PropA: string = 'Hello World';
 @State count: number = 0;
 build() {
   Row() {
     Column() {
       Text(this.PropA).fontSize(50).fontWeight(FontWeight.Bold)
       Child({ count: this.count }, localStorage2)
     }.width('100%')
   }.height('100%')
 }
}
@Component
struct Child {
 @Link count: number;
 @LocalStorageLink('PropB') PropB: string = 'Hello World';
 build() {
   Text(this.PropB).fontSize(50).fontWeight(FontWeight.Bold)
 }
}
Enter fullscreen mode Exit fullscreen mode
  1. 未定义属性时接收:可以只传入一个LocalStorage实例作为入参,如:
let localStorage1: LocalStorage = new LocalStorage();
localStorage1.setOrCreate('PropA', 'PropA');
let localStorage2: LocalStorage = new LocalStorage();
localStorage2.setOrCreate('PropB', 'PropB');
@Entry(localStorage1)
@Component
struct Index {
 @LocalStorageLink('PropA') PropA: string = 'Hello World';
 @State count: number = 0;
 build() {
   Row() {
     Column() {
       Text(this.PropA).fontSize(50).fontWeight(FontWeight.Bold)
       Child(localStorage2)
     }.width('100%')
   }.height('100%')
 }
}
@Component
struct Child {
 build() {
   Text("hello").fontSize(50).fontWeight(FontWeight.Bold)
 }
}
Enter fullscreen mode Exit fullscreen mode
  1. 属性无需从父组件初始化时接收:第一个参数传{},如:
let localStorage1: LocalStorage = new LocalStorage();
localStorage1.setOrCreate('PropA', 'PropA');
let localStorage2: LocalStorage = new LocalStorage();
localStorage2.setOrCreate('PropB', 'PropB');
@Entry(localStorage1)
@Component
struct Index {
 @LocalStorageLink('PropA') PropA: string = 'Hello World';
 @State count: number = 0;
 build() {
   Row() {
     Column() {
       Text(this.PropA).fontSize(50).fontWeight(FontWeight.Bold)
       Child({}, localStorage2)
     }.width('100%')
   }.height('100%')
 }
}
@Component
struct Child {
 @State count: number = 5;
 @LocalStorageLink('PropB') PropB: string = 'Hello World';
 build() {
   Text(this.PropB).fontSize(50).fontWeight(FontWeight.Bold)
 }
}
Enter fullscreen mode Exit fullscreen mode

(八)Navigation组件和LocalStorage联合使用

通过传递不同LocalStorage实例给自定义组件,在navigation跳转时显示对应绑定值,如:

let localStorageA: LocalStorage = new LocalStorage();
localStorageA.setOrCreate('PropA', 'PropA');
let localStorageB: LocalStorage = new LocalStorage();
localStorageB.setOrCreate('PropB', 'PropB');
let localStorageC: LocalStorage = new LocalStorage();
localStorageC.setOrCreate('PropC', 'PropC');
@Entry
@Component
struct MyNavigationTestStack {
 @Provide('pageInfo') pageInfo: NavPathStack = new NavPathStack();
 @Builder
 PageMap(name: string) {
   if (name === 'pageOne') {
     pageOneStack({}, localStorageA)
   } else if (name === 'pageTwo') {
     pageTwoStack({}, localStorageB)
   } else if (name === 'pageThree') {
     pageThreeStack({}, localStorageC)
   }
 }
 build() {
   Column({ space: 5 }) {
     Navigation(this.pageInfo) {
       Column() {
         Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }).width('80%').height(40).margin(20).onClick(() => {
           this.pageInfo.pushPath({ name: 'pageOne' });
         })
       }
     }.title('NavIndex').navDestination(this.PageMap).mode(NavigationMode.Stack).borderWidth(1)
   }
 }
}
@Component
struct pageOneStack {
 @Consume('pageInfo') pageInfo: NavPathStack;
 @LocalStorageLink('PropA') PropA: string = 'Hello World';
 build() {
   NavDestination() {
     Column() {
       NavigationContentMsgStack()
       Text(`${this.PropA}`)
       Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }).width('80%').height(40).margin(20).onClick(() => {
         this.pageInfo.pushPathByName('pageTwo', null);
       })
     }.width('100%').height('100%')
   }.title('pageOne').onBackPressed(() => {this.pageInfo.pop(); return true;})
 }
}
Enter fullscreen mode Exit fullscreen mode

六、总结

在鸿蒙 Next 中,页面级存储 LocalStorage 为开发者提供了一种方便的方式来管理页面级别的状态变量。通过使用 @LocalStorageProp 和 @LocalStorageLink 装饰器,开发者可以实现单向和双向的数据同步,满足不同的应用场景需求。无论是在单个页面内还是在多个页面之间,LocalStorage 都能够有效地共享状态,提高应用的开发效率和用户体验。同时,开发者需要注意 LocalStorage 的限制条件,以确保正确地使用和管理状态变量。

七、延伸内容

  1. 在实际应用中,可以结合其他鸿蒙 Next 的特性和功能,如组件化开发、状态管理框架等,进一步优化应用的架构和性能。
  2. 对于大型应用,可以考虑使用更复杂的状态管理方案,如 Redux 或 Vuex 的类似实现,以更好地管理全局状态。
  3. 在使用 LocalStorage 时,要注意数据的安全性和隐私性,避免存储敏感信息。可以结合加密技术等手段来保护数据。
  4. 不断探索和尝试新的用法和技巧,可以提高开发效率和代码质量。例如,可以使用自定义的 LocalStorage 实例来管理特定模块的状态,或者结合动画效果来增强用户体验。

Top comments (0)