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

HarmonyOS ArkTS API 24+ 实战:登录后用户信息如何全局流转,token 存哪里、页面怎么拿当前用户

前言

很多 HarmonyOS App 的登录联调,第一步都能很快跑通:

  • 输入账号密码
  • /api/login
  • 拿到 token
  • 再调/api/auth/user

但真正难的往往是第二步:

token 放哪里?
当前用户信息放哪里?
其他页面怎么拿?
App 重启后怎么恢复?
为什么不能每个页面都重新调一次users/me

这一篇就只讲这条链路,而且直接结合真实代码来拆。

本轮实现最终分成 5 层:

  1. EntryAbility.ets:声明并初始化全局登录态存储
  2. DemoStatePersistence.ets:负责本地恢复与持久化
  3. AuthRepository.ets:统一管理 token、当前用户、登录恢复
  4. AuthApi.ets:负责login / users/me / logout
  5. Index.ets和业务页面:只消费当前用户,不自己管 token

一、先说结论:token 和用户信息不要散在页面里

这次实现里,登录态没有直接塞进某个页面组件,而是分成了两层:

  • 运行态AppStorage / PersistentStorage
  • 业务态AuthRepository

其中:

  • token 由AuthRepository统一接管
  • 当前用户对象也由AuthRepository统一产出
  • 页面只读结果,不直接操作 token

这样做的核心好处是:

  1. 登录页不用关心“别的页面怎么读 token”
  2. “我的”页和工作台不用各自再调一次users/me
  3. App 重启后恢复逻辑可以统一放在仓储层

二、应用启动时,先把登录态相关存储声明出来

EntryAbility.ets里先把几个关键状态都注册到PersistentStorage

onCreate(want:Want,launchParam:AbilityConstant.LaunchParam):void{PersistentStorage.persistProp('authLoggedIn',false);PersistentStorage.persistProp('authUserId','');PersistentStorage.persistProp('authAccessToken','');PersistentStorage.persistProp('authReady',false);PersistentStorage.persistProp('authRestoring',false);demoStatePersistence.initialize(this.context);}

这几个字段的职责非常清楚:

  • authLoggedIn:当前是否登录
  • authUserId:当前登录用户 ID
  • authAccessToken:当前 token
  • authReady:认证仓库是否初始化完成
  • authRestoring:App 是否还在恢复登录态

注意这里的重点不是“先有默认值”,而是:

这些值从应用一启动开始,就已经是全局共享状态。

这样后面页面层就可以直接通过@StorageLink感知登录态变化。

三、真正的 token 和当前用户对象,是谁在管

这次的核心角色是AuthRepository.ets

先看它内部持有的两个关键字段:

privatesignedInUser?:AppUser=undefined;privatecurrentSession?:AuthSession=undefined;

这两个字段的分工是:

  • currentSession:保存 token、用户 ID、展示名等会话信息
  • signedInUser:保存页面真正要消费的AppUser

对外暴露的方法也很直接:

currentUser():AppUser|undefined{returnthis.signedInUser;}accessToken():string{returnthis.currentSession?.accessToken??'';}

也就是说,页面如果要拿当前登录用户,不需要自己去拼 token,不需要自己发请求,也不需要自己解析接口结构,直接拿:

authRepository.currentUser()

四、登录成功后,Repository 是怎么把两次接口结果收拢成一个用户态的

这次登录不是“一次请求就完事”,而是标准两步:

  1. POST /api/login
  2. GET /api/auth/user

对应AuthRepository.login()

asynclogin(userId:string,password:string):Promise<string>{constnormalizedUserId:string=userId.trim();if(normalizedUserId.length===0)return'请输入账号或手机号。';if(password.trim().length===0)return'请输入密码。';constoperationId:number=this.beginOperation();try{appHttpClient.clearAccessToken();constsession:AuthSession=awaitauthApi.login(normalizedUserId,password);if(!this.isCurrentOperation(operationId)){return'登录请求已更新,请重试。';}appHttpClient.setAccessToken(session.accessToken);constprofile:AuthUserProfile=awaitauthApi.getCurrentUser();if(!this.isCurrentOperation(operationId)){return'登录请求已更新,请重试。';}constuser:AppUser=this.mapProfile(profile);this.setSession(session,user,true,operationId);return'';}catch(error){if(this.isCurrentOperation(operationId)){this.resetRuntimeState();}returnthis.errorMessage(error);}}

这里最值得注意的不是“先 login 后 users/me”,而是这三步:

  1. 登录成功后先把 token 注入appHttpClient
  2. 再去请求当前用户资料
  3. 最后统一mapProfile()变成 App 内部用户对象

这样页面消费的永远是AppUser,不会直接吃后端原始对象。

五、为什么还要有一个AppUser,不能直接把users/me返回给页面

因为页面真正需要的是一个稳定的 App 内部模型,而不是后端原始响应。

当前AuthUserProfile -> AppUser的映射长这样:

privatemapProfile(profile:AuthUserProfile):AppUser{returnnewAppUser(profile.userId,profile.displayName,profile.roleName,profile.shiftName,profile.machineIds,profile.phone,profile.email,profile.emergencyContact,profile.avatarLabel,profile.teamName,profile.accountStatus,profile.permissions);}

这样做的价值很大:

  1. 后端字段名变化不会直接波及页面
  2. 页面只认识AppUser
  3. 后续想追加本地派生字段,也不会污染接口层

比如现在AppUser.ets已经不只保存姓名和手机号了,还包括:

teamName:string;accountStatus:string;permissions:string[];

甚至还补了一个权限判断方法:

hasPermission(permissionCode:string):boolean{constnormalizedCode:string=permissionCode.trim();if(normalizedCode.length===0){returnfalse;}returnthis.permissions.includes(normalizedCode);}

这就意味着,后面无论是“我的”页显示账号状态,还是业务页做权限判断,都不用再回头改接口解析结构。

六、token 最终存在哪里

这一点很多人最关心。

答案是:

  • 运行中:保存在AuthRepository.currentSessionAppStorage
  • 重启恢复:通过Preferences持久化

setSession()成功时,会同步把这几个值写进AppStorage

privatesetSession(session:AuthSession,user:AppUser,persist:boolean,operationId?:number):void{if(operationId!==undefined&&!this.isCurrentOperation(operationId)){return;}this.currentSession=session;this.signedInUser=user;AppStorage.setOrCreate('authLoggedIn',true);AppStorage.setOrCreate('authUserId',user.userId);AppStorage.setOrCreate('authAccessToken',session.accessToken);if(persist){this.persistSession(true,user.userId,session.accessToken);this.notifyStateChanged();}}

而持久化动作不是AuthRepository自己直接碰Preferences,而是交给了外部注入的持久化回调:

connectPersistence(writer:(loggedIn:boolean,userId:string,accessToken:string)=>void,stateChanged:()=>void):void{this.persistSession=writer;this.notifyStateChanged=stateChanged;}

这就是一个非常好的分层:

  • AuthRepository负责认证业务
  • DemoStatePersistence负责本地存储

七、App 重启后,登录态是怎么恢复的

恢复登录态的主入口在DemoStatePersistence.initialize()

先看启动时的准备动作:

initialize(context:Context):void{AppStorage.setOrCreate('authRestoring',true);authRepository.connectPersistence((loggedIn:boolean,userId:string,accessToken:string)=>this.saveAuthSession(loggedIn,userId,accessToken),()=>this.bumpStateVersion());authRepository.markReady();preferences.getPreferences(context,PREFERENCE_NAME).then((store:preferences.Preferences)=>{this.preferenceStore=store;returnstore.get(AUTH_ACCESS_TOKEN_KEY,'');}).then((accessTokenValue:preferences.ValueType)=>{constaccessToken:string=typeofaccessTokenValue==='string'?accessTokenValue:'';returnauthRepository.restoreSession(accessToken).finally(()=>{this.finishRestoring('restore-auth-session');});});}

这段链路的核心逻辑非常清楚:

  1. 启动时先把authRestoring设为true
  2. Preferences里取出上次保存的 token
  3. authRepository.restoreSession(accessToken)
  4. 恢复结束后把authRestoring设回false

restoreSession()内部并不是盲目信任旧 token,而是再请求一次真实用户:

asyncrestoreSession(accessToken:string):Promise<void>{constnormalizedToken:string=accessToken.trim();if(normalizedToken.length===0){this.clearSession(false);return;}appHttpClient.setAccessToken(normalizedToken);try{constprofile:AuthUserProfile=awaitauthApi.getCurrentUser();constuser:AppUser=this.mapProfile(profile);constsession:AuthSession=newAuthSession(normalizedToken,'Bearer',0,profile.userId,profile.displayName);this.setSession(session,user,false);}catch(_){this.clearSession(false);}}

这样处理的好处是:

  • token 过期会自动恢复失败
  • 不会把失效 token 当成有效登录态继续带着跑

八、其他页面怎么拿当前用户

这套结构里,其他页面不直接操作AuthRepository的内部状态,而是由页面容器统一取出当前用户,再往下传。

Index.ets里:

privatecurrentUser():AppUser|undefined{if(!this.authLoggedIn||this.authUserId.length===0)returnundefined;returnauthRepository.currentUser();}

然后业务页直接消费这个用户对象:

WorkBench({user:this.currentUser()asAppUser,onOpen:(kind:string,id:string)=>this.openDetail(kindasDetailKind,id),onRefresh:()=>demoBusinessRepository.resetDemoData()}).layoutWeight(1)

“我的”页也是同样模式:

ProfilePage({user:this.currentUser()asAppUser,onLogout:()=>this.handleLogout(),onOpenAvatarEditor:()=>{this.profileAvatarEditing=true;}}).layoutWeight(1)

这就是为什么我前面一直强调:

不要让每个页面自己去调users/me

只要顶层已经拿到当前用户,后面页面直接吃AppUser就够了。

九、登录页为什么不会一直卡在“恢复中”

这一轮里还有一个很重要的体验点,就是把“恢复中”状态和“登录页是否可操作”分清楚。

根状态来自:

@StorageLink('authRestoring')authRestoring:boolean=false;

Index.ets决定当前到底显示登录页还是业务页:

if(this.currentUser()===undefined){LoginPage({restoring:this.authRestoring,onSubmit:()=>this.handleLogin()}).layoutWeight(1)}

这意味着“恢复中”不是页面自己猜的,而是启动恢复链路真实给出来的状态。
恢复结束后,finishRestoring()会把它切回false,登录页自然就恢复正常交互。

十、退出登录时,token 和用户态如何清理

退出登录不是只清一个页面布尔值,而是统一走AuthRepository.clearSession()

privateclearSession(persist:boolean,operationId?:number):void{if(operationId!==undefined&&!this.isCurrentOperation(operationId)){return;}this.resetRuntimeState();AppStorage.setOrCreate('authLoggedIn',false);AppStorage.setOrCreate('authUserId','');AppStorage.setOrCreate('authAccessToken','');if(persist){this.persistSession(false,'','');this.notifyStateChanged();}}

resetRuntimeState()里还会一起清掉 HTTP 客户端的 token:

privateresetRuntimeState():void{this.currentSession=undefined;this.signedInUser=undefined;appHttpClient.clearAccessToken();}

这就保证了:

  1. 页面态清了
  2. 仓储态清了
  3. 请求头里的 token 也清了

十一、总结

这套登录态流转的关键,不是“把 token 存起来”这么简单,而是把职责拆清楚:

  1. EntryAbility声明全局状态
  2. DemoStatePersistence负责本地恢复与持久化
  3. AuthRepository统一管理 token 和当前用户
  4. AuthApi只负责网络交互
  5. 页面只消费AppUser

如果你也在做 HarmonyOS ArkTS App,我很建议按这个思路来:

  • 不要让页面自己存 token
  • 不要让每个页面自己调users/me
  • 不要把后端原始用户对象直接扔给页面

把这些边界收好之后,后面再做统一鉴权请求封装,就会顺很多。下一篇我会继续把这部分收口:Authorization请求头怎么统一注入,为什么后续业务 API 不需要每个接口手写鉴权代码。

附录:工程配置与版本说明

为了便于读者复现本文中的代码片段和运行现象,这里把当前文章系列对应的工程基线单独列出。本文所说的“当前工程”,指e_notebook项目的 HarmonyOS ArkTS 客户端,应用名称为“注塑工程师助手”,主要用于脱敏演示机台档案、产品档案、调机记录、参数模板、异常闭环、生产批次和看板报表等业务路径。

1. 应用与模块配置

  • 应用包名:com.atan.enotebook
  • 应用版本:versionName1.0.0versionCode1000000
  • 工程模型:ArkTS / ArkUI Stage 模型。
  • 主模块:entry,模块类型为entry
  • 入口 Ability:EntryAbility,入口文件为entry/src/main/ets/entryability/EntryAbility.ets
  • 主页面配置:模块通过pages: "$profile:main_pages"读取页面列表。
  • 设备类型:当前模块声明支持phonetablet2in1
  • 安装方式:deliveryWithInstalltrueinstallationFreefalse,属于随应用安装的普通 entry 模块。

2. SDK 与 API 版本



  • DevEco Studio 版本:DevEco Studio Beta26.0.0.461
  • 编译 SDK:HarmonyOS SDK API 26 Beta1,SDK 包版本为26.0.0.23
  • SDK 平台信息:apiVersion26platformVersion26.0.0releaseType/stageBeta1
  • targetSdkVersion26.0.0
  • compatibleSdkVersion6.1.1(24)
  • API 口径说明:文章系列以 API 24 作为兼容目标进行表述;当前工程实际由 API 26 Beta SDK 编译,并在 API 24 模拟器上做过安装、启动和交互观察。因此,文中的“API 24 运行观察”表示兼容目标环境下的模拟器验证结果,不等同于使用 API 24 SDK 重新完成编译验证。

3. 构建与运行工具

  • 开发工具 IDE:DevEco Studio Beta,安装目录指向D:/Program Files/Huawei/DevEco Studio Beta
  • SDK 路径:D:/Program Files/Huawei/DevEco Studio Beta/sdk
  • 构建系统:Hvigor,工程入口hvigorfile.ts使用@ohos/hvigor-ohos-pluginappTasks
  • Hvigor 执行配置:开启 daemon、incremental、parallel 和 typeCheck,日志级别为info
  • 构建脚本:本地build.ps1优先使用 DevEco Studio 自带的 JBR、Node.js、SDK 与 Hvigor,避免系统环境变量中的 Java 或 Node.js 版本干扰构建结果。
  • 调试产物:未配置签名时,本地构建生成entry/build/default/outputs/default/entry-default-unsigned.hap。这类 unsigned HAP 只用于本地调试和模拟器验证,正式发布前需要在 DevEco Studio 中补充签名配置。

4. 本系列文章的验证边界

  • 本系列代码以脱敏演示数据为主,Repository、Store、页面状态和组件边界都围绕本地演示闭环展开。
  • 已观察过的运行现象以文中对应截图、布局树和人工核对记录为准;没有重新核对的页面,不在单篇文章中扩大为完整结论。
  • 如果读者使用更新的 DevEco Studio、HarmonyOS SDK 或真机系统版本复现,API 差异、控件行为和签名流程可能会发生变化。遇到差异时,建议优先核对build-profile.json5module.json5、SDK Manager 中安装的 API 版本,以及当前设备或模拟器的系统 API 等级。

附录 2:项目目录结构与设计意图

下面这份目录说明对应当前 DevEco Studio 中打开的harmonyos-app工程。截图里能看到的目录并不只是文件摆放习惯,它反映了一个 ArkTS Stage 工程的分层方式:应用级配置、业务模块、页面源码、资源文件、构建配置和过程归档分别放在不同位置,方便后续排查问题时先判断“问题属于配置、页面、数据、状态、资源,还是构建产物”。

harmonyos-app/ ├── AppScope/ # 应用级配置与全局资源入口 │ ├── app.json5 # bundleName、版本号、图标、应用标签等应用级元信息 │ └── resources/ # 应用级图标、字符串和基础资源 ├── entry/ # 主业务模块,当前 App 的主要页面和业务代码都在这里 │ ├── src/main/ets/ # ArkTS 源码根目录 │ │ ├── components/ # 可复用 ArkUI 组件,如底部导航、数据状态面板 │ │ ├── entryability/ # Stage 模型入口 Ability,负责应用启动入口 │ │ ├── features/ # 按业务域拆分的功能页面 │ │ │ ├── debug/ # 调机记录相关页面 │ │ │ ├── exceptions/ # 异常处置与闭环相关页面 │ │ │ ├── home/ # 首页看板与概览入口 │ │ │ ├── machines/ # 机台档案列表、详情和机台相关交互 │ │ │ ├── production/ # 生产批次、报工和结案门禁相关页面 │ │ │ ├── products/ # 产品档案、产品详情和关联信息 │ │ │ ├── reports/ # 周报、月报、班次报表和下钻入口 │ │ │ └── templates/ # 参数模板列表与详情 │ │ ├── models/ # 业务对象的数据结构,如 Machine、Product、DebugRecord │ │ ├── pages/ # 页面容器与导航装配,如 Index.ets │ │ ├── repositories/ # 脱敏演示数据、查询方法、快照持久化和数据重置边界 │ │ ├── stores/ # 页面路由、导航选择和共享状态规则 │ │ └── utils/ # 主题令牌、校验函数等通用工具 │ ├── src/main/resources/base/ # 模块级资源目录 │ │ ├── element/ # 字符串、颜色等基础资源声明 │ │ ├── media/ # 图标、启动图等媒体资源 │ │ └── profile/ # 页面 profile 配置,如 main_pages.json │ ├── src/main/module.json5 # entry 模块配置,声明 EntryAbility、设备类型和页面入口 │ ├── build-profile.json5 # 模块级构建目标、混淆和 target 配置 │ └── oh-package.json5 # entry 模块包信息与依赖声明 ├── hvigor/ # Hvigor 构建系统配置 │ └── hvigor-config.json5 # 构建执行参数,如增量、并行和类型检查 ├── build-profile.json5 # 工程级 SDK、targetSdkVersion、compatibleSdkVersion 配置 ├── hvigorfile.ts # 工程级构建任务入口,接入 appTasks ├── local.properties # 本机 SDK 路径配置 ├── oh-package.json5 # 工程级包信息与依赖声明 ├── build.ps1 # 本地构建脚本,固定使用 DevEco Studio 自带工具链 ├── document_claude/ # 开发过程归档、测试记录和验证材料 ├── .hvigor/ # Hvigor 生成的缓存和构建记录,不作为手写源码维护 ├── .idea/ # DevEco Studio / IntelliJ 工程配置,不承载业务逻辑 └── entry/build/ # 构建输出目录,HAP 和中间产物由构建流程生成

1. 为什么应用级配置放在AppScope

AppScope负责应用整体身份,而不是某个页面的业务逻辑。app.json5中的bundleNameversionNameversionCode、应用图标和应用标签,会影响安装包身份、桌面展示和版本识别。把这类配置放在应用级目录,可以避免业务页面为了改一个标题或图标而混入应用发布配置。

在当前工程中,AppScope更像“应用身份证”。它回答的是“这个 App 是谁、版本是多少、展示什么图标”,而不是“机台列表怎么筛选、详情页怎么返回”。

2. 为什么业务代码集中在entry/src/main/ets

entry是当前工程的主业务模块,src/main/ets是 ArkTS 源码根目录。截图里打开的MachineDetail.ets就位于features/machines下面,说明机台详情页被归入“机台业务域”,而不是随意放在全局页面目录中。

这种组织方式的好处是定位明确:机台问题优先看features/machines,产品问题优先看features/products,生产批次问题优先看features/production。当文章里讨论某个业务链路时,读者也能从目录直接反推代码位置。

3.componentsfeaturespages的边界

components放的是可复用组件,例如底部导航、加载/空态/失败态面板。它们不应该直接知道“当前打开的是哪台机台”,而是通过参数和回调服务于不同页面。

features放的是业务域页面。每个子目录都围绕一个业务主题组织,例如machines负责机台档案,templates负责参数模板,exceptions负责异常闭环。业务页面可以组合组件,也可以读取模型和仓储,但应尽量把本业务域的显示和交互留在本目录内。

pages更偏页面容器和入口装配。当前Index.ets承担主页面状态切换、底部导航和详情路径分发等职责。它不应该塞满所有业务细节,而是负责把用户当前所在位置、打开对象和页面分支组织起来。

4.modelsrepositoriesstores分别解决什么问题

models定义数据形状,例如机台、产品、调机记录、生产批次等对象有哪些字段。它让页面和仓储使用同一套类型语言,避免每个页面临时拼对象。

repositories定义数据来源和查询边界。当前工程使用脱敏演示数据和本地持久化快照,因此仓储层负责“从哪里取数据、按什么 ID 查询、怎样重置演示数据”。页面不直接关心数据是内置数组、Preferences 快照,还是后续真实接口。

stores定义页面级或应用级状态规则,例如当前导航项、路由分支、打开详情的类型和 ID。把状态规则从具体组件中抽出来,可以减少“列表、详情、导航互相覆盖状态”的问题。

5. 为什么资源放在resources/base

resources/base/element管字符串、颜色等声明,resources/base/media管图标和图片,resources/base/profile管页面 profile。它们和 ArkTS 页面代码分开,是为了让“界面逻辑”和“静态资源”各自清晰。

如果页面显示异常,先判断是布局代码问题还是资源引用问题。比如图标不显示,应优先检查media和资源引用;页面无法进入,应检查profile/main_pages.jsonmodule.json5的页面声明;颜色或字符串不符合预期,则回到element下核对。

6. 构建目录和生成目录不要手工维护

.hvigorentry/build和部分中间产物目录由构建系统生成,主要用于缓存、编译记录、HAP 输出和临时文件。它们可以帮助排查构建结果,但不应该作为手写业务代码维护。

当前调试 HAP 位于entry/build/default/outputs/default/entry-default-unsigned.hap。这个路径说明构建已经产出安装包,但它仍是 unsigned 调试产物;正式发布前应回到 DevEco Studio 的签名配置和发布流程,而不是直接修改build目录里的文件。

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

相关文章:

  • deepseek-v4-flash 正式版 68 元免费体验
  • 嵌入式处理器流水线原理:从基础概念到性能优化
  • Seata分布式事务:原理、实践与性能优化
  • 安卓模拟器本地OCR集成方案:基于PaddleOCR与按键精灵的自动化脚本优化
  • 游戏玩家30天无痛掌握Python:从零到实战项目全攻略
  • 2026年8月自贡市移动200M单宽带申请避坑与实测攻略 - 找卡家园
  • 2026 年当下,汕头比较好的新能源电车托运企业找哪家,把车远迁老家居然比开回去还划算?这玩意儿的门道我居然现在才懂!-盛世华航轿车托运 - 企业推荐官【认证官方】
  • 如何彻底掌控你的微信聊天记录:三步实现个人数据备份与管理的终极方案
  • 2026广东纤维素抑尘剂厂家移动电话汇总 - 品牌排行榜
  • Unity协程全解析:从原理到实战,掌握异步编程核心技能
  • Modbus RTU协议深度解析:从通信原理到工业现场实战应用
  • 算法面试——深度优先搜索:全排列、组合总和、岛屿数量
  • 舒尔补:矩阵降维与条件化的核心工具及其应用
  • Linux 权限管理——chmod、chown、sudo、ACL
  • 2026年8月山东省济南市移动融合宽带办理指南 - 找卡家园
  • H3c RC3000 开启 Telnet 及解除锁网
  • 2026 年当下,上海知名的异型雕塑平台推荐几家,小区角落突然冒出来的这玩意儿,竟让路过的人都停下了脚步?-宏观雕塑 - 行业推荐官【认证】
  • 页面置换算法深度解析:从缺页计算到系统性能调优实战
  • Autodock Vina 1.2.5 在Linux系统下的完整安装与配置指南
  • 2026年市面上耐用的101三角龙骨生产厂家联络方式 - 品牌排行榜
  • CTF杂项挑战实战:从盲文、音频频谱到图片隐写的全流程解析
  • 深入解析C++内存模型:从竞争条件到无锁编程实战
  • 2026年8月重庆市联通融合宽带怎么报装 - 找卡家园
  • NS-USBLoader终极指南:免费跨平台Switch游戏管理工具
  • 微服务边界别再凭感觉:用 DDD 识别业务边界
  • 2026年8月南昌市联通500M单宽带怎么报装 - 找卡家园
  • B端与C端产品核心差异:从用户角色到技术架构的深度解析
  • 2026年8月上海市奉贤区移动单宽带小白避坑指南 - 找卡家园
  • 2026年优选:上海房地产律师咨询怎么选才专业 - 装修教育财税推荐2026
  • 2026年江苏泰州粉末涂料供货商有哪些?你想知道的都在这里 - 品牌排行榜