当前位置: 首页 > news >正文

14-退出登录要清干净状态-把持久化键水合和清理流程集中管理

第14篇|退出登录要清干净状态:把持久化 key、水合和清理流程集中管理

摘要:退出登录不是删掉 token 就结束。HarmonyOS 应用里,AppStorage、本地 Preferences、页面内存状态、最近访问缓存和导航栈只要有一层没归零,下一位用户进入时就可能看到旧头像、旧收藏、旧统计,甚至按返回回到需要登录的页面。稳定做法是先列清持久化 key 和运行态 key,启动时统一水合默认值,退出时由服务层同时清持久层、重置运行态、回收导航栈。

我遇到过一个账号切换问题:用户退出后登录页已经出现,但首页收藏数量还停在旧账号的 18;重新打开应用时,旧昵称先闪了一下才变成未登录。继续在退出按钮里补this.favoriteCount = 0没用,因为旧数据并不只在这个页面里。真正要修的是“状态从哪里来、谁负责清、页面何时重新读”。

这篇文章解决四个实际问题:

  1. 退出登录时哪些状态必须清,哪些只是页面临时状态。
  2. 新增@StorageLinkkey 后,为什么启动水合和退出重置必须一起补。
  3. 如何让 Repository 清持久层,Service 编排运行态重置,页面只处理交互。
  4. 如何用切账号、重启、返回栈和清空数据路径验证旧状态没有回潮。


适用场景和边界

本文讨论 Stage 模型下的本地账号状态清理。示例用 key-value 存储适配器表达持久化层,用AppStorage表达运行态协调,用NavPathStack表达页面栈回收。真实项目可以把持久层换成 Preferences、文件或 RDB,但分工不要变。

状态类型示例退出时怎么处理
认证状态token、refresh token、当前用户 id删除持久层,运行态设为空
用户域数据收藏 id、最近浏览、草稿缓存按用户域 key 清理或切换命名空间
运行态摘要首页统计、红点、当前头像AppStorage.setOrCreate重置默认值
页面临时态输入框内容、loading、Toast页面销毁或路由重置时自然释放
导航状态已登录页面栈清空栈并回到登录入口

退出登录的难点在于这些状态分散在不同层。只改按钮上的几行代码,很容易当前页面看起来好了,换入口或重启后又看到旧值。

先把旧状态回潮路径找出来

退出登录问题可以先用“旧状态从哪里回来的”来排查。不同路径指向的根因不一样。

现象优先怀疑说明
刚退出当前页还显示旧昵称运行态没重置AppStorage或页面@State还保留旧值
重启后旧数据又出现持久层没删干净Preferences / 文件 / RDB 仍有用户域数据
登录新账号后看到旧收藏key 没按用户域隔离收藏、最近浏览等共享了旧账号 key
按返回回到旧页面导航栈没回收退出后仍能回到登录态之外的页面
清空数据后首次进入崩溃默认值没水合@StorageLink首次读取到未准备值

这个表能帮助你少做无效修复。比如重启后旧数据回来,重点查持久层;退出瞬间 UI 没变,重点查运行态和页面刷新;按返回能回旧页面,就先看导航栈。

集中登记持久化 key 和运行态 key

不要让页面各写各的字符串 key。退出登录、账号注销、清空数据、测试重置都应该使用同一份 key 清单。

exportconstPersistKeys={sessionToken:'session_token',currentUser:'current_user',favoriteCourseIds:'favorite_course_ids',recentCourseIds:'recent_course_ids',draftCourseNote:'draft_course_note'}asconstexporttypePersistKey=typeofPersistKeys[keyoftypeofPersistKeys]exportconstUserScopedPersistKeys:PersistKey[]=[PersistKeys.sessionToken,PersistKeys.currentUser,PersistKeys.favoriteCourseIds,PersistKeys.recentCourseIds,PersistKeys.draftCourseNote]exportconstRuntimeKeys={sessionToken:PersistKeys.sessionToken,currentUser:PersistKeys.currentUser,favoriteCourseIds:PersistKeys.favoriteCourseIds,recentCourseIds:PersistKeys.recentCourseIds,homeRefreshVersion:'home_refresh_version'}asconst

这里把“持久层 key”和“运行态 key”放在一起看,是为了避免新增一个@StorageLink后只在页面里使用,却忘了启动默认值和退出重置。key 一旦跨页面,就不再属于某个页面。

启动阶段先水合默认值

页面首次挂载时,运行态可能还没有从持久层恢复完成。尤其是@StorageLink数组或对象,如果没有安全默认值,Builder 里一调用.length或字符串插值就可能出错。

exportfunctionhydrateSessionRuntimeKeys():void{AppStorage.setOrCreate<string>(RuntimeKeys.sessionToken,'')AppStorage.setOrCreate<string>(RuntimeKeys.currentUser,'')AppStorage.setOrCreate<string[]>(RuntimeKeys.favoriteCourseIds,[])AppStorage.setOrCreate<string[]>(RuntimeKeys.recentCourseIds,[])AppStorage.setOrCreate<number>(RuntimeKeys.homeRefreshVersion,0)}exportclassEntryAbilityextendsUIAbility{onCreate():void{hydrateSessionRuntimeKeys()}}

这段代码只做一件事:让运行态 key 在页面出现前有安全默认值。它不负责读取真实用户信息,也不决定是否登录。真实恢复逻辑可以放到 SessionService,但默认值必须足够早。

Repository 只处理持久层删除

持久层清理要集中在 Repository 或存储适配器里。它不弹 Toast,不跳页面,也不关心首页怎么展示。

exportinterfaceLocalKeyValueStore{remove(key:string):Promise<void>putString(key:string,value:string):Promise<void>}exportclassSessionRepository{constructor(privatereadonlystore:LocalKeyValueStore){}asyncclearUserScopedData():Promise<void>{for(constkeyofUserScopedPersistKeys){awaitthis.store.remove(key)}}asyncmarkLoggedOut():Promise<void>{awaitthis.store.putString(PersistKeys.sessionToken,'')awaitthis.store.putString(PersistKeys.currentUser,'')}}

clearUserScopedData()处理收藏、最近浏览、草稿这类用户域数据,markLoggedOut()处理认证状态兜底。真实项目里如果某些缓存要保留到设备级,也应该从UserScopedPersistKeys里排除,而不是在退出流程里临时判断。

Service 编排持久层和运行态一起归零

退出按钮不应该直接调用十几个清理方法。它只发起退出动作,Service 负责按顺序编排。

exportinterfaceLogoutResult{success:booleanmessage:string}exportclassLogoutService{constructor(privatereadonlyrepository:SessionRepository){}asynclogout():Promise<LogoutResult>{try{awaitthis.repository.clearUserScopedData()awaitthis.repository.markLoggedOut()this.resetRuntimeState()return{success:true,message:'已退出登录'}}catch(err){return{success:false,message:'退出登录失败,请重试'}}}privateresetRuntimeState():void{AppStorage.setOrCreate<string>(RuntimeKeys.sessionToken,'')AppStorage.setOrCreate<string>(RuntimeKeys.currentUser,'')AppStorage.setOrCreate<string[]>(RuntimeKeys.favoriteCourseIds,[])AppStorage.setOrCreate<string[]>(RuntimeKeys.recentCourseIds,[])AppStorage.setOrCreate<number>(RuntimeKeys.homeRefreshVersion,Date.now())}}

顺序上先清持久层,再重置运行态。这样就算页面马上读取AppStorage,也只能读到空状态;应用重启后,持久层也不会把旧状态恢复回来。homeRefreshVersion用来提醒首页、我的页、收藏页重新读取空状态。

页面只触发动作,不直接清 key

页面可以处理按钮 loading、错误提示和跳转,但不要自己删除持久化 key。否则退出、注销、账号过期、清空数据会走出多套逻辑。

@Componentstruct AccountSettingsPage{@StateloggingOut:boolean=false@StateerrorText:string=''privatelogoutService:LogoutService=createLogoutService()privateaccountNavigator:AccountNavigator=createAccountNavigator()privateasynchandleLogout():Promise<void>{if(this.loggingOut){return}this.loggingOut=trueconstresult=awaitthis.logoutService.logout()this.loggingOut=falseif(!result.success){this.errorText=result.messagereturn}this.accountNavigator.backToLogin()}}

这段页面代码只有交互职责:防止重复点击、展示失败、成功后跳转。清理范围不在页面里决定,后面新增“账号注销”也可以复用同一个 Service。

Builder 里读取运行态要给安全出口

@StorageLink值可能在首次挂载、清空数据、退出瞬间处于默认状态。页面展示时不要在 Builder 里直接假设数组或对象一定有业务值。

@Componentstruct ProfileHeader{@StorageLink(RuntimeKeys.currentUser)currentUser:string=''@StorageLink(RuntimeKeys.favoriteCourseIds)favoriteCourseIds:string[]=[]privategetUserName():string{returnthis.currentUser.length>0?this.currentUser:'未登录'}privategetFavoriteCount():number{returnthis.favoriteCourseIds?.length??0}build(){Column({space:8}){Text(this.getUserName())Text(`收藏课程${this.getFavoriteCount()}`)}}}

这里用普通方法返回安全默认值,不用复杂 getter 参与 Builder 状态读取。这样退出瞬间、清空数据后首次打开、账号状态恢复前,页面都能展示可预期内容。

退出后要回收导航栈

状态清干净之后,如果导航栈还保留已登录页面,用户按返回仍可能回到旧页面。退出成功后应该清空栈,再进入登录页或未登录首页。

exportclassAccountNavigator{constructor(privatereadonlynavStack:NavPathStack){}backToLogin():void{this.navStack.clear()this.navStack.pushPathByName('LoginPage',undefined)}}

如果项目里有底部 Tab 或多个导航容器,要确认真正清的是当前可见容器对应的栈。不要只改一个入口,另一个 Tab 里仍然保留旧页面。

清空数据和账号注销也要复用同一套链路

退出登录不是唯一会清状态的入口。账号注销、token 过期、清空本地数据、切换账号,都应该尽量复用相同的 key 清单和运行态重置。

入口是否清持久层是否重置运行态是否回收导航
主动退出登录
token 过期认证状态必须清
账号注销用户域数据必须清
清空本地数据按用户选择清视入口决定
切换账号旧用户域隔离或清理进入新账号首屏

把这些入口放在一起看,能发现很多遗漏。比如主动退出清了收藏,但 token 过期只跳登录页,没有清运行态,旧收藏仍可能显示。

用反向搜索清掉旧路径

改完后要搜索旧 key、直接写存储、页面内清理和绕过 Service 的入口。

rg"session_token|current_user|favorite_course_ids|recent_course_ids"entry features common rg"AppStorage\\.set|AppStorage\\.setOrCreate|@StorageLink|remove\\("entry features common rg"logout|clearUserScopedData|backToLogin|NavPathStack"entry features common

第一条确认 key 是否还有散落字符串;第二条找页面或组件是否直接操作运行态;第三条确认退出、清理和导航回收是否形成同一条链路。命中不代表一定错误,但每一处都要能说清 owner。

验证路径要覆盖切账号和重启

退出登录问题不能只点一次按钮。至少要走下面这些路径:

  1. 账号 A 登录后收藏两门课程,确认首页和我的页都有统计。
  2. 点击退出登录,确认头像、昵称、收藏数量、最近记录都立即回到未登录态。
  3. 按系统返回键,确认不能回到账号 A 的页面。
  4. 关闭应用再打开,确认旧头像和旧收藏不会闪现。
  5. 登录账号 B,确认看不到账号 A 的收藏、草稿和最近浏览。
  6. 清空应用数据后首次进入,确认@StorageLink字符串、数组都有安全默认值。
  7. 人为制造退出失败,确认页面不会半清状态并且有重试入口。

如果第 2 步失败,先查运行态;如果第 4 步失败,先查持久层;如果第 3 步失败,先查导航栈。把失败归类后,修起来会快很多。

风险提示和回退方案

退出链路改动面不小,适合分阶段落地。

风险触发场景处理方式
清错设备级数据把主题、引导页状态也放进用户域 key区分用户域 key 和设备级 key
当前页半更新退出失败后已经重置运行态先清持久层成功,再重置运行态
首页旧值闪现启动先渲染旧缓存启动水合默认值,恢复完成后再展示用户态
新增 key 漏清新页面新增@StorageLink同步加入 key 清单、启动水合、退出重置
返回旧页面只跳登录页没清栈使用NavPathStack.clear()后再进入登录页

如果已经上线,不建议一次性重写所有持久化。可以先把退出链路中最危险的认证状态、当前用户、用户域缓存收口,再逐步把页面里的散落 key 迁出来。

常见问题和处理方式

现象常见原因处理方式
退出后当前页还有旧昵称AppStorage没重置Service 中统一setOrCreate默认值
重启后旧收藏回来持久层 key 没删Repository 按UserScopedPersistKeys清理
新账号看到旧草稿用户域数据共用 key清理旧用户域或按 userId 隔离
清空数据后页面崩溃@StorageLink默认值缺失启动阶段水合字符串、数组和对象
退出后返回旧页面导航栈保留已登录页面清栈后进入登录页

这些问题看似是 UI 现象,根因通常在状态边界。先把持久层、运行态、页面状态和导航栈分清楚,再决定改哪里。

复查清单

改完后我会按这份清单收尾:

  1. 用户域持久化 key 已集中到UserScopedPersistKeys
  2. 新增的@StorageLinkkey 已在启动阶段setOrCreate默认值。
  3. 退出登录由 Service 编排,页面没有直接删持久化 key。
  4. 持久层清理和运行态重置同时发生,不只改其中一层。
  5. 首页、我的页、收藏页都能在退出后读到未登录态。
  6. 导航栈已回收,系统返回不会回到旧页面。
  7. 切账号、重启、清空数据、退出失败路径都走过。

这次问题给我的复盘

退出登录最容易被误修成按钮问题。按钮只是入口,真正的链路包含 key 清单、默认值水合、持久层删除、运行态重置、页面安全读取和导航回收。任何一层漏掉,旧状态都会从另一个入口回来。

如果重新处理一次,我会先列出所有用户域 key,再看哪些页面通过@StorageLink读取它们,接着把清理动作收到 Service,最后用切账号和重启验证。这个顺序比在页面上补零慢一点,但能把问题一次收成一类。

小结:退出登录是一条状态归零链路

退出登录要清干净,关键不是多写几个set(''),而是让状态有清晰 owner。Repository 删除持久层,Service 重置运行态,页面只触发动作和展示结果,导航层回收已登录页面栈。新增一个跨页面 key,就同步补启动水合、退出重置和验证路径。这样账号切换、重启和清空数据后,旧状态才不会回潮。

http://www.jsqmd.com/news/1151691/

相关文章:

  • 【JAVA毕设源码分享】基于SpringBoot的拍立得购买平台的设计与实现(程序+文档+代码讲解+一条龙定制)
  • 2026年浙江正规的全塑泡沫枪/高压泡沫枪工厂哪家可靠
  • Python的基本数据类型 整数进制转换 数据类型转换函数
  • Python 入门必看:彻底搞懂数据类型、进制转换与类型转换
  • 告别文档堆成山!本地部署知识库,让检索效率翻倍
  • Python dict判空?别再犯傻,一行代码让空字典无处遁形
  • Zabbix搭建及简单使用
  • 毕业论文写作全流程指南:从选题到答辩,2026年最新攻略
  • 艺学启航:线上服务稳不稳定,取决于你的Python部署方案选型
  • npm 安全审计与依赖风险排查:5步构建供应链安全防线
  • Mysql 平衡二叉树、红黑树、B树、B+树区别以及应用场景(五)
  • BERT 与 GPT 架构对比:3 种 Transformer 变体的核心差异与应用场景分析
  • Code Combat | 极客战记 攻略【Kithgard地牢篇 24/42】轰轰
  • Linux tcpdump 命令超全详解:网络抓包与协议分析必备神器
  • 如何避免重新训练,用上下文就可以让世界模型进化?
  • 电脑桌面出现查找工具
  • wordpress企业主题推荐
  • 基于STM32单片机车载儿童防窒息 车载儿童滞留检测安全座椅系统13(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_文章底部可以扫码
  • 2026年优选指南:寻找最靠谱的苦荞全麦片供应商
  • 仅需10分钟数据!RVC语音转换技术如何打破传统壁垒?
  • 影刀RPA Excel数据透视实战:自动汇总分析你的业务数据
  • 公开课报名 | 掌握AI测试智能体,让测试效率轻松翻倍!
  • Python入门必修课:基本数据类型、进制转换与类型转换函数全解析
  • 功能、价格、售后三维大比拼:4 款主流 AI 无人直播系统,最便宜的反而最适合小商家?
  • 告别职业枷锁!《墨香情》手游官网下载,唯美水墨复古武侠,公平生态不氪不肝!
  • 3 种编码器架构对比:RNN vs LSTM vs Transformer 在文本摘要中的性能差异
  • 构建家庭游戏串流平台:Sunshine自托管解决方案深度指南
  • 曲辕RPA-停止监听网页请求
  • 架构师实战+ 大模型实战1.0
  • 如何将联系人从三星转移到 iPhone