ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

鸿蒙状态管理深度解析:LocalStorage与AppStorage选型与实战

鸿蒙状态管理深度解析:LocalStorage与AppStorage选型与实战 1. 状态管理怎么分层LocalStorage和AppStorage到底解决了什么问题写鸿蒙UI不管你是刚入门的新手还是已经做过几个完整项目的老手都会碰到一个绕不开的问题多个组件、多个页面之间的数据到底怎么共享、怎么同步前面几节我们已经学过State 管的是组件自己内部的状态Prop 和 Link 管父子组件的数据传递Provide 和 Consume 管祖孙组件之间的跨层级通信。这些都属于“组件层级”的状态管理解决的范围都局限在一棵组件树的内部。但实际开发中的问题往往更复杂。用户登录成功之后头像昵称要展示在首页右上角个人中心要用购物车结算页也要用用户在设置页把语言切换成英文整个应用的所有文案都得跟着变。这种跨页面的数据共享靠一层层向子组件里传参根本不现实代码会变成一个巨大的传参黑洞。鸿蒙给出的方案就是这一节的主角LocalStorage 和 AppStorage。先给一个最直白的定义。LocalStorage 是页面级的状态存储它绑定单个页面的生命周期——进入页面时可以创建或挂载一个新的 LocalStorage这个页面内部所有组件共享同一份数据页面销毁这份数据的生命周期通常也就结束了。AppStorage 是应用级的状态存储它在应用启动时就被初始化整个应用所有页面、所有组件都能读写同一份数据应用进程不死数据就一直在。这两个存储最核心的价值在于它们里面的数据是“UI可感知”的。数据一变绑定了这些数据的组件会自动重新渲染不需要你手动 setState也不需要发通知让其他页面刷新。这也是它们和普通全局变量、单例类之间最本质的区别。用一个生活化的例子来理解LocalStorage 就像酒店每个房间门口配的共享保险柜这一层的住客都能存取客人退房了柜子也就回收了AppStorage 就像是酒店大堂的中央储物柜不管是哪一层的住客都能存取而且只要酒店不关门东西就一直在里面。这篇文章作为系列的第22节我会把 LocalStorage 和 AppStorage 从底层原理、API 用法、装饰器绑定方式到真实项目里的选型思路和踩坑记录全部捋一遍适合正在学鸿蒙UI基础、想把状态管理真正落到项目里的开发者。看完全文你应该能回答这几个问题什么时候该用页面级什么时候该用应用级LocalStorageProp 和 LocalStorageLink 到底差在哪UI 不刷新、数据丢失这类问题从哪里入手排查2. LocalStorage页面级状态存储创建、绑定与生命周期2.1 创建一个LocalStorage实例LocalStorage 是一个独立的类官方叫法有很多比如“存储实例”“状态存储单元”。使用前必须先实例化。最常见的是构造时不带参数let pageStorage new LocalStorage();如果希望页面一加载就有初始数据可以在构造函数里直接传入一个对象let pageStorage new LocalStorage({ count: 0, userName: HarmonyOS });也可以先创建空实例之后在合适的时机调用 setOrCreate 方法往里写数据let pageStorage new LocalStorage(); pageStorage.setOrCreate(count, 0); pageStorage.setOrCreate(userName, HarmonyOS);注意 setOrCreate 这个方法的名字本身就是个提醒如果 key 已经存在它更新 value如果 key 不存在它创建这条记录。合并了 set 和 create 两个动作你不需要先 get 判断 key 是否存在再决定走哪条分支。这个方法我在实际项目里用得非常频繁尤其在页面进入后根据接口返回值往存储里塞数据的时候。2.2 用装饰器把存储里的数据绑定到UI创建了 LocalStorage接下来要让 UI 组件能读到里面的数据。鸿蒙提供了两个装饰器LocalStorageProp 和 LocalStorageLink。先看 LocalStorageLinkEntry Component struct CountPage { LocalStorageLink(count) count: number 0; build() { Column() { Text(当前计数${this.count}) Button(加一) .onClick(() { this.count; }) } } }这段代码做了三件事第一声明一个名为 count 的组件变量并且告诉框架这个变量对应 LocalStorage 里 key 为 count 的那条数据第二组件初始化时count 的值会自动从 LocalStorage 里取出来第三当你在组件里修改 this.count 的值修改会写回 LocalStorage同时所有绑定了 count 这个 key 的组件都会收到通知并刷新。LocalStorageLink 是双向绑定。双向的意思是存储到组件值变了写回 LocalStorage组件到存储LocalStorage 里的值被别的地方改了组件同步更新。这正好对应了前面学过的 Link 的语义。再看 LocalStoragePropLocalStorageProp(count) count: number 0;它只建立一条单向的数据通道LocalStorage 里的值可以流向组件组件初始化时能拿到存储里的值但你在组件里修改 this.count这个改动不会写回 LocalStorage也不会影响其他组件。它的语义对应 Prop。我见过不少刚接触的人在这两个装饰器之间纠结其实判断标准很简单如果这个值在组件里改了之后你希望所有相关页面都跟着变用 LocalStorageLink如果你只是想在组件初始化时读一次或者只允许组件内部临时修改、不想影响全局数据用 LocalStorageProp。这里有一个很多人忽略的细节LocalStorageProp 虽然不会把改动写回 LocalStorage但组件内部仍然可以正常读写这个变量UI 也会因为本地改动而刷新。它是“组件内可改、但不回传存储”的语义和 Prop 的局部可变性非常像。所以它并不是只读的意思不要理解成 readonly。2.3 LocalStorage怎么挂到页面上实例创建好了装饰器也写好了下一步是把 LocalStorage 和页面关联起来。关联方式和页面的跳转方式有关这里重点说两个场景。第一个场景从导航栈里启动一个新页面并把 LocalStorage 传给它。import { router } from kit.ArkUI; let pageStorage new LocalStorage({ count: 0, userName: HarmonyOS }); router.pushUrl({ url: pages/CountPage, storage: pageStorage });这里的关键是 pushUrl 的 storage 参数。页面路由时会把这个 LocalStorage 实例挂到新页面上。新页面里 Entry 组件如果声明了 LocalStorageLink 或 LocalStorageProp就能直接取到这份数据。第二个场景在应用入口EntryAbility里创建 LocalStorage然后传给第一个加载的页面。export default class EntryAbility extends UIAbility { onWindowStageCreate(windowStage: window.WindowStage): void { let storage new LocalStorage({ count: 0 }); windowStage.loadContent(pages/Index, storage); } }loadContent 的第二个参数就是 LocalStorage。这种方式适合应用启动时就需要把一些数据带给首页的场景比如冷启动时从本地缓存里还原用户偏好设置。还有一点需要明确页面销毁时LocalStorage 不一定会立即被回收。鸿蒙官方文档的表述是“一个页面可以引用多个 LocalStorage 实例一个 LocalStorage 实例也可以被多个页面引用”实际项目中常见的是每个页面持有自己的实例页面栈弹出后实例随页面销毁。但如果你把一个 LocalStorage 传给了多个页面那么它的生命周期取决于最后一个引用它的页面被销毁的时刻。这个在设计共享数据时要格外注意别以为页面关闭了数据就必然释放了。2.4 LocalStorage的常用API速查除了装饰器LocalStorage 本身还有一些常用方法适合在非UI逻辑里操作数据。方法作用使用场景get(key)读取指定key的值在非UI逻辑里获取存储数据setOrCreate(key, value)有则更新、无则创建最常见的写入方式set(key, value)更新已有key的值key不存在会报错确定key已存在时使用delete(key)删除指定key清理不需要的数据keys()获取所有key的集合调试、批量遍历size()获取存储记录的数量排查数据泄漏一个容易踩的坑是 set 和 setOrCreate 的区别。如果你用 set 去写一个 key 不存在的数据运行时会抛异常。这个设计其实是为了帮你尽早发现 key 拼写错误这类问题。如果你不确定 key 是否已经存在老老实实用 setOrCreate 更安全。3. AppStorage应用级状态存储全局共享与持久化配合3.1 AppStorage的初始化与全局访问AppStorage 是应用级的单例存储不需要像 LocalStorage 那样手动 new 出来。只要应用进程活着AppStorage 就一直存在。它的常用写入方式同样是 setOrCreateAppStorage.setOrCreate(userInfo, { name: 张三, age: 18 });读取方式let userInfo AppStorage.get(userInfo);需要注意的是AppStorage 里存的值类型有限制支持 number、string、boolean、对象、数组以及这些类型的联合类型。不要往里存 Date 这类实例对象取出来之后类型和预期会不一致。实际项目中我通常只放可序列化的数据这也是为了后面配合持久化存储时不会出问题。类和对象在存储时的处理也值得留意。如果你 seal 了一个类那么 class 必须用 Observed 装饰否则属性变化可能不会被 UI 感知。这个属于进阶话题基础阶段你只需要记住存普通对象、数组、基础类型基本不会出问题复杂嵌套对象要配合 Observed 使用才能触发UI更新这点后面单开一节细说。3.2 StorageProp和StorageLink的使用方式AppStorage 和 UI 组件的绑定方式和 LocalStorage 几乎一模一样只是装饰器换成了 StorageProp 和 StorageLink。Entry Component struct UserPage { StorageLink(userInfo) userInfo: UserInfo {}; StorageProp(language) language: string zh-CN; build() { Column() { Text(this.userInfo.name) Text(this.language) } } }StorageLink 是双向绑定组件里改了值会写回 AppStorage其他绑定了同一个 key 的组件会自动刷新。StorageProp 是单向绑定组件初始化时读取值组件内部修改不会影响 AppStorage。看到这里你会发现LocalStorage 和 AppStorage 的使用模式是完全对称的。这种对称设计是有意为之你先学会其中一个另一个基本是换汤不换药。区别只在于作用范围——LocalStorage 需要在页面初始化时创建并传入AppStorage 则全局只有一个任何地方直接使用即可。3.3 持久化AppStorage和PersistentStorage的配合AppStorage 有一个非常重要的问题它只存在于内存中。应用被杀掉、设备重启AppStorage 里的数据会全部清空。如果我们希望某些数据跨应用重启仍然存在就需要另一个角色PersistentStorage。PersistentStorage 的作用是把 AppStorage 里的指定 key 持久化到本地文件系统。配合方式非常简单PersistentStorage.persistProp(language, zh-CN);这行代码会做几件事检查本地持久化文件里有没有 language 这个 key有的话就把它读出来写入 AppStorage没有的话就在持久化文件里创建 zh-CN 作为默认值同时写入 AppStorage。之后你通过 AppStorage 修改这个 key 的值PersistentStorage 会自动把新值同步到本地文件里不需要你手动调任何保存方法。我在项目里最常见的用法是在 EntryAbility 的 onCreate 里集中注册需要持久化的字段比如用户设置、主题模式、语言、上次登录账号等PersistentStorage.persistProp(language, zh-CN); PersistentStorage.persistProp(themeMode, light); PersistentStorage.persistProp(lastLoginAccount, );这样做的收益是应用重启后第一次创建组件读取 StorageLink(language) 时拿到的就是上次保存的值UI 直接就是正确的状态不需要自己写一套“读取本地缓存并设置全局状态”的逻辑。3.4 为什么不直接用全局变量很多从普通应用开发转过来的朋友会下意识地问AppStorage 能做的事情定义一个全局单例类也能做为什么非要用它关键差异在于“UI感知”。你自己定义一个 GlobalData 单例改了一个属性之后组件不会收到任何通知。你可能需要在每个页面里手动调一个回调函数或者用事件总线比如 AppStorage 之外的 EventHub发通知再在收到通知的组件里手动重新读取数据。这套流程写起来又啰嗦又容易漏。而 AppStorage 装饰器方案是声明式的。你声明了 StorageLink(userInfo) userInfo框架帮你维护了存储和组件之间的订阅关系值改变时所有绑定的组件自动更新。这不仅仅是少写几行代码的问题更重要的是逻辑清晰、不容易出bug。4. 场景实战一个设置页同时验证LocalStorage和AppStorage4.1 场景设定与存储选型为了把这套东西用起来我设计一个非常常见的小场景应用有一个设置页用户可以修改昵称和主题模式。需求有三个第一从首页跳到设置页设置页需要显示当前昵称和主题模式并且这两个值在首页已经展示。这说明两个页面需要共享数据页面级存储不够用必须放到应用级 AppStorage。第二设置页里用户改昵称是一个临时编辑过程用户点了“保存”才生效点了“取消”就还原。这种场景如果用 StorageLink 双向绑定用户每敲一个字符AppStorage 里的值就实时被修改首页的 UI 也会跟着跳来跳去体验很糟糕。正确做法是在设置页内部维护一个临时状态保存时再写回应用级存储。第三主题模式得在应用重启后仍然生效所以需要 PersistentStorage 配合持久化。选型结论昵称和主题模式放 AppStorage设置页编辑过程中的临时草稿用组件内部状态或者 LocalStorage 都行我用 LocalStorage 来演示页面级存储的典型用法。4.2 第一步在应用入口注册持久化在 EntryAbility 里注册持久化字段export default class EntryAbility extends UIAbility { onCreate(): void { AppStorage.setOrCreate(nickName, 未设置昵称); AppStorage.setOrCreate(themeMode, light); PersistentStorage.persistProp(nickName, 未设置昵称); PersistentStorage.persistProp(themeMode, light); } onWindowStageCreate(windowStage: window.WindowStage): void { windowStage.loadContent(pages/Index); } }这里的顺序值得注意先 setOrCreate再 persistProp。persistProp 在首次调用时会尝试从持久化文件读取旧值如果读取到旧值它会覆盖 AppStorage 里的当前值。所以如果你先调用 persistProp再手动 setOrCreate手动设置的值可能被持久化文件里的旧值覆盖掉容易产生“为什么我设的值没生效”的困惑。4.3 第二步首页展示全局状态首页用 StorageLink 绑定两个全局字段。因为首页只是展示理论上用 StorageProp 也可以但考虑到后续可能需要直接修改这里用 StorageLink 更通用Entry Component struct Index { StorageLink(nickName) nickName: string ; StorageLink(themeMode) themeMode: string light; build() { Column() { Text(当前用户${this.nickName}) Text(当前主题${this.themeMode}) Button(进入设置页) .onClick(() { router.pushUrl({ url: pages/SettingPage }); }) } } }当用户从设置页保存了新的昵称回到首页这两行 Text 会自动刷新不需要 onPageShow 里做任何手动刷新操作。我在实际项目里测过只要数据写入 AppStorage 成功页面回退或者跳转回来后UI 都是新的不需要额外处理。4.4 第三步设置页用LocalStorage管理草稿设置页比较复杂。进入页面时我们需要把当前全局昵称同步成本地草稿用户编辑的是草稿保存时才提交到 AppStorage。做法是进入设置页时创建一个 LocalStorage用全局值初始化草稿字段然后用 LocalStorageLink 绑定草稿。let settingStorage new LocalStorage(); Entry Component struct SettingPage { LocalStorageLink(draftNickName) draftNickName: string ; LocalStorageLink(draftThemeMode) draftThemeMode: string light; aboutToAppear(): void { // 用全局值初始化草稿 let globalNick AppStorage.getstring(nickName) ?? ; let globalTheme AppStorage.getstring(themeMode) ?? light; settingStorage.setOrCreate(draftNickName, globalNick); settingStorage.setOrCreate(draftThemeMode, globalTheme); } build() { Column() { TextInput({ text: this.draftNickName }) .onChange((value: string) { this.draftNickName value; }) Row() { Button(浅色).onClick(() { this.draftThemeMode light; }) Button(深色).onClick(() { this.draftThemeMode dark; }) } Button(保存) .onClick(() { AppStorage.setOrCreate(nickName, this.draftNickName); AppStorage.setOrCreate(themeMode, this.draftThemeMode); router.back(); }) Button(取消) .onClick(() { router.back(); }) } } }注意这里有个细节页面是跳转前创建的 LocalStorage那么草稿 key 需要再初始化时确定。我在页面里用LocalStorageLink(draftNickName)声明指的是 LocalStorage 实例里 key 为 draftNickName 的数据。如果 LocalStorage 实例里没有这个 key装饰器会使用变量声明的默认值。关于创建 LocalStorage 的时机还需要特别注意。如果把 LocalStorage 对象放在 router.pushUrl 也就是页面跳转前创建好那么所有传给该页面的存储数据在页面生命周期开始前就已经初始化了进入页面后可以直接读取。新手经常踩的坑是在组件内部才创建 LocalStorage 实例再想把数据同步给 LocalStorageLink 时发现流程变成了“先有鸡还是先有蛋”的问题所以页面级的数据初始化尽量提前到跳转之前。保存按钮的逻辑是核心用户每次点击保存我们把草稿值写入 AppStorage。写入之后 AppStorage 自动通知所有绑定的组件刷新包括首页。取消按钮什么都不做直接返回草稿自然被丢弃。这个场景的交互逻辑很清晰用上了 LocalStorage 和 AppStorage 各自的优势。4.5 第四步验证持久化效果运行一次应用把昵称改成“开发者老王”主题切到深色保存。然后把应用从后台划掉重新启动。你会发现首页显示的昵称还是“开发者老王”主题依然是深色。这就是 PersistentStorage 在起作用。为了确认持久化确实在走本地文件你可以在修改之后立刻调用AppStorage.get(nickName)查看内存值的状态再下次启动时通过首页显示的文案确认恢复值。如果重启后配置回退了常见原因依次排查persistProp 注册时机太晚比如写在了组件创建之后、key 名称不一致、或者手动 setOrCreate 覆盖了持久化恢复值。5. 常见问题与排查技巧实录5.1 问题速查表现象可能原因解决思路页面启动后装饰器变量是默认值不是存储里的值LocalStorage 没有正确传给页面key 拼写不一致检查 loadContent/pushUrl 是否传了 storage 实例组件里修改了 StorageProp/LocalStorageProp 的值其他页面没变化这两个装饰器是单向的修改不会写回存储换成 StorageLink/LocalStorageLink应用重启后 AppStorage 数据丢失没有注册 PersistentStorage.persistProp在应用入口统一注册数据写入了UI 没有刷新存入了不支持的类型如 Date对象属性变化未使用 Observed换成可序列化类型或给类加 Observedset 方法报 key 不存在set 和 setOrCreate 语义不同不确定 key 是否存在时用 setOrCreate页面销毁后数据被其他页面读到LocalStorage 被多个页面共享合理规划每个页面的存储实例边界5.2 UI不刷新的排查套路UI 不刷新可以说是状态管理类问题里最让人头疼的。我自己的排查顺序是这样的先确认写入操作是否真的执行了。在写入处打日志或者用console.info输出写入值确认代码走到了这一步。再确认 key 是否一致。这是最常见的问题写的时候nickName读的时候nickname服务端又不会给你报错UI 就安静地显示默认值。只要出现这种“不报错但没效果”的情况第一反应就是检查字符串拼写和大小写。接着确认读取的存储实例是同一个。LocalStorage 如果每次跳转都 new 一个全新的实例那么旧实例里的数据自然不存在于新实例中。最后确认是不是单向绑定。用了 StorageProp 却期待修改传播到其他地方这是概念层面的误解解决办法就是换成 StorageLink。5.3 关于复杂对象的实战补充AppStorage 和 LocalStorage 里存复杂对象时如果希望对象属性变化也能触发 UI 更新需要给对象所属的类加上 Observed 装饰器。但这里有个容易混淆的点加了 Observed 的类只对类本身的属性变化生效如果你把整个对象重新赋值给存储的 key那么触发机制和属性级别的变化是不同的。具体来说AppStorage.setOrCreate(user, newUserObject)这种整体替换会触发所有绑定组件刷新这个不受 Observed 影响。而this.user.name 新名字这种属性级修改必须要类上加了 Observed 才能被 UI 感知。官方推荐的方式是需要属性级感知用 Observed不需要时整个替换对象即可。我个人的建议是状态管理里能不用嵌套对象就不用嵌套对象。把数据拆成多个扁平 key每个 key 存基础类型UI 绑定和排查问题都会简单很多。如果确实需要对象尽量采用整体替换的方式更新减少属性级修改带来的不确定性。6. 页面级还是应用级一张图帮你做选型6.1 从作用范围、生命周期、API 三个维度对比对比维度LocalStorageAppStorage作用范围单个页面及其子组件整个应用生命周期跟随页面引用页面销毁且无引用时回收跟随应用进程创建方式手动 new LocalStorage()全局单例直接使用绑定装饰器LocalStorageProp / LocalStorageLinkStorageProp / StorageLink持久化支持无直接配合需要自己处理配合 PersistentStorage典型场景列表筛选条件、表单草稿、临时编辑状态用户信息、主题设置、登录状态、语言设置6.2 我的选型建议在一个真实项目里两个存储不是二选一而是互相配合。我的习惯是全局且需要跨页面共享的数据比如登录态、用户信息、全局设置、主题统一放 AppStorage并且能持久化的都注册 persistProp。页面内部临时状态比如筛选条件、表单草稿、列表的滚动位置放 LocalStorage。如果拿不准还有一个简单的判断方法数据从页面 A 跳到页面 B 之后还需要吗需要就放 AppStorage不需要就放 LocalStorage。数据在页面内几个组件之间共享但不需要带出页面用 LocalStorage 也够用甚至用 Provide/Consume 也行。还有一种情况我强烈建议用 LocalStorage进入页面时从接口拉数据拉到之后要把数据分发到多个子组件。这种情况下在页面入口创建一个 LocalStorage设置好 key子组件通过 LocalStorageLink 获取数据数据驱动逻辑非常清晰。等页面退出数据自动失去引用不需要担心内存泄漏。6.3 性能与内存注意点AppStorage 虽然方便但也不是什么都往里塞。它的本质是一个全局状态管理中心所有值都会在 UI 线程的依赖图上注册。如果一个 key 被几十个组件同时绑定值一改这几十个组件全部刷新性能消耗是可观的。我在一个稍微复杂的页面上遇到过真实问题一个全局对象被十几个子组件通过 StorageProp 绑定每次修改对象的一个属性整个页面所有组件几乎同时重新渲染明显能感觉到卡顿。后面把绑定的范围缩小只有真正依赖该数据的组件才绑定问题就解决了。所以这里有两个建议。第一不要“顺手”把不相关的数据都挂到 AppStorage 上保持存储里的 key 尽可能精简。第二绑定范围控制好能用局部状态解决的不升级到全局能用 StorageProp 单向绑定的别用 StorageLink。6.4 数据量边界什么时候该换方案AppStorage 适合存“频率中低、变化需要同步UI”的数据。但如果你要存的是大量列表数据比如几百条日志、一个完整的缓存数据库AppStorage 就不合适了。这些数据不参与 UI 渲染或者只在用户主动查看时才渲染更适合放在数据库或者文件缓存里渲染时再读取。判断标准很简单如果一份数据只有“少数几个页面关心”并且“不是每个变化都需要实时刷新UI”就别放 AppStorage。我自己见过一个反面案例把一整个商品列表塞进 AppStorage每次商品状态变化都触发所有绑定了列表 key 的组件刷新最后发现性能瓶颈就在这里。后来改成列表数据单独管理AppStorage 只放必要的状态标识整体流畅度明显提升。7. 写在最后这套状态存储最打动我的几个细节说一点我自己写鸿蒙状态管理的体会。LocalStorage 和 AppStorage 的设计思路其实跟前端框架里的全局状态管理比如 Vuex、Redux非常像核心都是把“共享数据”和“UI 更新”绑定在一起让开发者从手动通知刷新的繁琐逻辑里解脱出来。我个人在项目中最大的感受是一旦养成“页面级数据进 LocalStorage应用级数据进 AppStorage”的习惯写页面之间的数据联动会顺手很多。比如登录之后刷新用户信息只需要在接口回调里写一句 AppStorage.setOrCreate所有页面的头像、昵称瞬间全部更新这种体验是写传统全局变量永远体会不到的。最后补充一个调试小技巧在 DevEco Studio 里你可以通过日志过滤关键字 LocalStorage 和 AppStorage 观察框架对存储的读写情况。当某个数据没有按预期刷新时先看日志里有没有对应的 set 操作记录再判断是写入没发生还是读取没生效。这个方法帮我定位过好几次棘手的“数据丢了”问题比猜要快得多。鸿蒙的状态管理体系还在持续演进但 LocalStorage 和 AppStorage 这两个基础能力是构建复杂应用绕不开的地基。把这一节的内容吃透后面的路由传参、组件状态同步、持久化设计都会轻松不少。
返回列表