第14篇退出登录要清干净状态把持久化 key、水合和清理流程集中管理摘要退出登录不是删掉 token 就结束。HarmonyOS 应用里AppStorage、本地 Preferences、页面内存状态、最近访问缓存和导航栈只要有一层没归零下一位用户进入时就可能看到旧头像、旧收藏、旧统计甚至按返回回到需要登录的页面。稳定做法是先列清持久化 key 和运行态 key启动时统一水合默认值退出时由服务层同时清持久层、重置运行态、回收导航栈。我遇到过一个账号切换问题用户退出后登录页已经出现但首页收藏数量还停在旧账号的 18重新打开应用时旧昵称先闪了一下才变成未登录。继续在退出按钮里补this.favoriteCount 0没用因为旧数据并不只在这个页面里。真正要修的是“状态从哪里来、谁负责清、页面何时重新读”。这篇文章解决四个实际问题退出登录时哪些状态必须清哪些只是页面临时状态。新增StorageLinkkey 后为什么启动水合和退出重置必须一起补。如何让 Repository 清持久层Service 编排运行态重置页面只处理交互。如何用切账号、重启、返回栈和清空数据路径验证旧状态没有回潮。适用场景和边界本文讨论 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}asconstexporttypePersistKeytypeofPersistKeys[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.setOrCreatestring(RuntimeKeys.sessionToken,)AppStorage.setOrCreatestring(RuntimeKeys.currentUser,)AppStorage.setOrCreatestring[](RuntimeKeys.favoriteCourseIds,[])AppStorage.setOrCreatestring[](RuntimeKeys.recentCourseIds,[])AppStorage.setOrCreatenumber(RuntimeKeys.homeRefreshVersion,0)}exportclassEntryAbilityextendsUIAbility{onCreate():void{hydrateSessionRuntimeKeys()}}这段代码只做一件事让运行态 key 在页面出现前有安全默认值。它不负责读取真实用户信息也不决定是否登录。真实恢复逻辑可以放到 SessionService但默认值必须足够早。Repository 只处理持久层删除持久层清理要集中在 Repository 或存储适配器里。它不弹 Toast不跳页面也不关心首页怎么展示。exportinterfaceLocalKeyValueStore{remove(key:string):PromisevoidputString(key:string,value:string):Promisevoid}exportclassSessionRepository{constructor(privatereadonlystore:LocalKeyValueStore){}asyncclearUserScopedData():Promisevoid{for(constkeyofUserScopedPersistKeys){awaitthis.store.remove(key)}}asyncmarkLoggedOut():Promisevoid{awaitthis.store.putString(PersistKeys.sessionToken,)awaitthis.store.putString(PersistKeys.currentUser,)}}clearUserScopedData()处理收藏、最近浏览、草稿这类用户域数据markLoggedOut()处理认证状态兜底。真实项目里如果某些缓存要保留到设备级也应该从UserScopedPersistKeys里排除而不是在退出流程里临时判断。Service 编排持久层和运行态一起归零退出按钮不应该直接调用十几个清理方法。它只发起退出动作Service 负责按顺序编排。exportinterfaceLogoutResult{success:booleanmessage:string}exportclassLogoutService{constructor(privatereadonlyrepository:SessionRepository){}asynclogout():PromiseLogoutResult{try{awaitthis.repository.clearUserScopedData()awaitthis.repository.markLoggedOut()this.resetRuntimeState()return{success:true,message:已退出登录}}catch(err){return{success:false,message:退出登录失败请重试}}}privateresetRuntimeState():void{AppStorage.setOrCreatestring(RuntimeKeys.sessionToken,)AppStorage.setOrCreatestring(RuntimeKeys.currentUser,)AppStorage.setOrCreatestring[](RuntimeKeys.favoriteCourseIds,[])AppStorage.setOrCreatestring[](RuntimeKeys.recentCourseIds,[])AppStorage.setOrCreatenumber(RuntimeKeys.homeRefreshVersion,Date.now())}}顺序上先清持久层再重置运行态。这样就算页面马上读取AppStorage也只能读到空状态应用重启后持久层也不会把旧状态恢复回来。homeRefreshVersion用来提醒首页、我的页、收藏页重新读取空状态。页面只触发动作不直接清 key页面可以处理按钮 loading、错误提示和跳转但不要自己删除持久化 key。否则退出、注销、账号过期、清空数据会走出多套逻辑。Componentstruct AccountSettingsPage{StateloggingOut:booleanfalseStateerrorText:stringprivatelogoutService:LogoutServicecreateLogoutService()privateaccountNavigator:AccountNavigatorcreateAccountNavigator()privateasynchandleLogout():Promisevoid{if(this.loggingOut){return}this.loggingOuttrueconstresultawaitthis.logoutService.logout()this.loggingOutfalseif(!result.success){this.errorTextresult.messagereturn}this.accountNavigator.backToLogin()}}这段页面代码只有交互职责防止重复点击、展示失败、成功后跳转。清理范围不在页面里决定后面新增“账号注销”也可以复用同一个 Service。Builder 里读取运行态要给安全出口StorageLink值可能在首次挂载、清空数据、退出瞬间处于默认状态。页面展示时不要在 Builder 里直接假设数组或对象一定有业务值。Componentstruct ProfileHeader{StorageLink(RuntimeKeys.currentUser)currentUser:stringStorageLink(RuntimeKeys.favoriteCourseIds)favoriteCourseIds:string[][]privategetUserName():string{returnthis.currentUser.length0?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 的入口。rgsession_token|current_user|favorite_course_ids|recent_course_idsentry features common rgAppStorage\\.set|AppStorage\\.setOrCreate|StorageLink|remove\\(entry features common rglogout|clearUserScopedData|backToLogin|NavPathStackentry features common第一条确认 key 是否还有散落字符串第二条找页面或组件是否直接操作运行态第三条确认退出、清理和导航回收是否形成同一条链路。命中不代表一定错误但每一处都要能说清 owner。验证路径要覆盖切账号和重启退出登录问题不能只点一次按钮。至少要走下面这些路径账号 A 登录后收藏两门课程确认首页和我的页都有统计。点击退出登录确认头像、昵称、收藏数量、最近记录都立即回到未登录态。按系统返回键确认不能回到账号 A 的页面。关闭应用再打开确认旧头像和旧收藏不会闪现。登录账号 B确认看不到账号 A 的收藏、草稿和最近浏览。清空应用数据后首次进入确认StorageLink字符串、数组都有安全默认值。人为制造退出失败确认页面不会半清状态并且有重试入口。如果第 2 步失败先查运行态如果第 4 步失败先查持久层如果第 3 步失败先查导航栈。把失败归类后修起来会快很多。风险提示和回退方案退出链路改动面不小适合分阶段落地。风险触发场景处理方式清错设备级数据把主题、引导页状态也放进用户域 key区分用户域 key 和设备级 key当前页半更新退出失败后已经重置运行态先清持久层成功再重置运行态首页旧值闪现启动先渲染旧缓存启动水合默认值恢复完成后再展示用户态新增 key 漏清新页面新增StorageLink同步加入 key 清单、启动水合、退出重置返回旧页面只跳登录页没清栈使用NavPathStack.clear()后再进入登录页如果已经上线不建议一次性重写所有持久化。可以先把退出链路中最危险的认证状态、当前用户、用户域缓存收口再逐步把页面里的散落 key 迁出来。常见问题和处理方式现象常见原因处理方式退出后当前页还有旧昵称AppStorage没重置Service 中统一setOrCreate默认值重启后旧收藏回来持久层 key 没删Repository 按UserScopedPersistKeys清理新账号看到旧草稿用户域数据共用 key清理旧用户域或按 userId 隔离清空数据后页面崩溃StorageLink默认值缺失启动阶段水合字符串、数组和对象退出后返回旧页面导航栈保留已登录页面清栈后进入登录页这些问题看似是 UI 现象根因通常在状态边界。先把持久层、运行态、页面状态和导航栈分清楚再决定改哪里。复查清单改完后我会按这份清单收尾用户域持久化 key 已集中到UserScopedPersistKeys。新增的StorageLinkkey 已在启动阶段setOrCreate默认值。退出登录由 Service 编排页面没有直接删持久化 key。持久层清理和运行态重置同时发生不只改其中一层。首页、我的页、收藏页都能在退出后读到未登录态。导航栈已回收系统返回不会回到旧页面。切账号、重启、清空数据、退出失败路径都走过。这次问题给我的复盘退出登录最容易被误修成按钮问题。按钮只是入口真正的链路包含 key 清单、默认值水合、持久层删除、运行态重置、页面安全读取和导航回收。任何一层漏掉旧状态都会从另一个入口回来。如果重新处理一次我会先列出所有用户域 key再看哪些页面通过StorageLink读取它们接着把清理动作收到 Service最后用切账号和重启验证。这个顺序比在页面上补零慢一点但能把问题一次收成一类。小结退出登录是一条状态归零链路退出登录要清干净关键不是多写几个set()而是让状态有清晰 owner。Repository 删除持久层Service 重置运行态页面只触发动作和展示结果导航层回收已登录页面栈。新增一个跨页面 key就同步补启动水合、退出重置和验证路径。这样账号切换、重启和清空数据后旧状态才不会回潮。