14-退出登录要清干净状态-把持久化键水合和清理流程集中管理
第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 清单。
export const PersistKeys = {
sessionToken: 'session_token',
currentUser: 'current_user',
favoriteCourseIds: 'favorite_course_ids',
recentCourseIds: 'recent_course_ids',
draftCourseNote: 'draft_course_note'
} as const
export type PersistKey = typeof PersistKeys[keyof typeof PersistKeys]
export const UserScopedPersistKeys: PersistKey[] = [
PersistKeys.sessionToken,
PersistKeys.currentUser,
PersistKeys.favoriteCourseIds,
PersistKeys.recentCourseIds,
PersistKeys.draftCourseNote
]
export const RuntimeKeys = {
sessionToken: PersistKeys.sessionToken,
currentUser: PersistKeys.currentUser,
favoriteCourseIds: PersistKeys.favoriteCourseIds,
recentCourseIds: PersistKeys.recentCourseIds,
homeRefreshVersion: 'home_refresh_version'
} as const
这里把“持久层 key”和“运行态 key”放在一起看,是为了避免新增一个 @StorageLink 后只在页面里使用,却忘了启动默认值和退出重置。key 一旦跨页面,就不再属于某个页面。
启动阶段先水合默认值
页面首次挂载时,运行态可能还没有从持久层恢复完成。尤其是 @StorageLink 数组或对象,如果没有安全默认值,Builder 里一调用 .length 或字符串插值就可能出错。
export function hydrateSessionRuntimeKeys(): 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)
}
export class EntryAbility extends UIAbility {
onCreate(): void {
hydrateSessionRuntimeKeys()
}
}
这段代码只做一件事:让运行态 key 在页面出现前有安全默认值。它不负责读取真实用户信息,也不决定是否登录。真实恢复逻辑可以放到 SessionService,但默认值必须足够早。
Repository 只处理持久层删除
持久层清理要集中在 Repository 或存储适配器里。它不弹 Toast,不跳页面,也不关心首页怎么展示。
export interface LocalKeyValueStore {
remove(key: string): Promise<void>
putString(key: string, value: string): Promise<void>
}
export class SessionRepository {
constructor(private readonly store: LocalKeyValueStore) {}
async clearUserScopedData(): Promise<void> {
for (const key of UserScopedPersistKeys) {
await this.store.remove(key)
}
}
async markLoggedOut(): Promise<void> {
await this.store.putString(PersistKeys.sessionToken, '')
await this.store.putString(PersistKeys.currentUser, '')
}
}
clearUserScopedData() 处理收藏、最近浏览、草稿这类用户域数据,markLoggedOut() 处理认证状态兜底。真实项目里如果某些缓存要保留到设备级,也应该从 UserScopedPersistKeys 里排除,而不是在退出流程里临时判断。
Service 编排持久层和运行态一起归零
退出按钮不应该直接调用十几个清理方法。它只发起退出动作,Service 负责按顺序编排。
export interface LogoutResult {
success: boolean
message: string
}
export class LogoutService {
constructor(private readonly repository: SessionRepository) {}
async logout(): Promise<LogoutResult> {
try {
await this.repository.clearUserScopedData()
await this.repository.markLoggedOut()
this.resetRuntimeState()
return { success: true, message: '已退出登录' }
} catch (err) {
return { success: false, message: '退出登录失败,请重试' }
}
}
private resetRuntimeState(): 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。否则退出、注销、账号过期、清空数据会走出多套逻辑。
@Component
struct AccountSettingsPage {
@State loggingOut: boolean = false
@State errorText: string = ''
private logoutService: LogoutService = createLogoutService()
private accountNavigator: AccountNavigator = createAccountNavigator()
private async handleLogout(): Promise<void> {
if (this.loggingOut) {
return
}
this.loggingOut = true
const result = await this.logoutService.logout()
this.loggingOut = false
if (!result.success) {
this.errorText = result.message
return
}
this.accountNavigator.backToLogin()
}
}
这段页面代码只有交互职责:防止重复点击、展示失败、成功后跳转。清理范围不在页面里决定,后面新增“账号注销”也可以复用同一个 Service。
Builder 里读取运行态要给安全出口
@StorageLink 值可能在首次挂载、清空数据、退出瞬间处于默认状态。页面展示时不要在 Builder 里直接假设数组或对象一定有业务值。
@Component
struct ProfileHeader {
@StorageLink(RuntimeKeys.currentUser) currentUser: string = ''
@StorageLink(RuntimeKeys.favoriteCourseIds) favoriteCourseIds: string[] = []
private getUserName(): string {
return this.currentUser.length > 0 ? this.currentUser : '未登录'
}
private getFavoriteCount(): number {
return this.favoriteCourseIds?.length ?? 0
}
build() {
Column({ space: 8 }) {
Text(this.getUserName())
Text(`收藏课程 ${this.getFavoriteCount()} 门`)
}
}
}
这里用普通方法返回安全默认值,不用复杂 getter 参与 Builder 状态读取。这样退出瞬间、清空数据后首次打开、账号状态恢复前,页面都能展示可预期内容。
退出后要回收导航栈
状态清干净之后,如果导航栈还保留已登录页面,用户按返回仍可能回到旧页面。退出成功后应该清空栈,再进入登录页或未登录首页。
export class AccountNavigator {
constructor(private readonly navStack: 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。
验证路径要覆盖切账号和重启
退出登录问题不能只点一次按钮。至少要走下面这些路径:
- 账号 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,就同步补启动水合、退出重置和验证路径。这样账号切换、重启和清空数据后,旧状态才不会回潮。
更多推荐



所有评论(0)