1. 若依App版本的“生命周期”到底在说什么
如果你接触过若依(RuoYi)这个后台管理框架,应该知道它有很多个版本:单体版、前后端分离版、微服务版,还有一个专门面向移动端的RuoYiApp版。很多人第一次看到“生命周期”这三个字,以为只是Spring Bean的生命周期,或者Vue组件的生命周期,但放在移动端App场景里,这个概念被彻底撑大了。
我做若依App版二次开发也有段时间了,从一开始只盯着页面跳转,到后来被线上环境的偶发问题逼着把整条链路啃了一遍,才真正理解:在RuoYiApp版里,生命周期至少横跨四个层面——框架层、应用层、页面层、数据层。任何一个层面处理不好,都会出现那种“用户明明登录着,突然就被踢下线”、“App切到后台再回来,页面白屏”、“数据还是昨天的”之类的诡异问题。
这篇内容我尽量不写成官方文档的复述,而是按照真实项目里会遇到的问题来拆解,把若依App版里涉及生命周期的关键节点、背后的设计逻辑、以及我自己踩过的一些坑都讲清楚。适合正在做若依App版开发、或者正准备拿它做项目底座的同学参考。
2. 框架层生命周期:登录态与Token的“续命”机制
2.1 若依App版的Token体系设计
先说框架层的生命周期。传统Web端后台管理,登录状态靠Session维持,但App端没有Session这个概念,若依App版采用的是Token + Redis缓存的方案。
整个链路是这样的:用户输入账号密码,后端校验通过后生成一个Token,这个Token同时写入Redis,并设置过期时间,默认配置一般是30分钟。App端拿到Token后存在本地存储里,后续每个请求都在Header里带上这个Token。后端每次收到请求,会先从请求头里解析Token,再去Redis里查对应信息,查到了就放行并刷新过期时间,查不到就返回401。
这套机制本身不复杂,但有几个关键点决定了App的整体生命周期表现:
第一,Token过期是“滑动”的。也就是说,只要用户一直在操作,过期时间就一直在往后推。但如果用户把App切到后台半小时,Redis里的Token已经失效,再回来操作就必然踩到401。
第二,登录用户信息缓存不能只靠App本地。若依后端把用户信息、权限信息都丢在Redis里,App端只存了一个Token字符串。这就带来一个特性:用户权限变了、账号被封了、密码重置了,后端只要清掉Redis里的缓存,App端下一次请求立即失效,不需要App做任何操作。
这里有个非常容易踩的坑:我做第一个若依App项目时,把企业微信扫码登录接了进来,用户已经通过企微认证,但若依后端根本不认识这个人。我当时的处理方式是直接调若依的注册接口创建一个账号,再把企微的userId和若依的userId做绑定。听起来没问题,但实际运行时,企微端解绑换绑、用户改了企微昵称,Redis里的登录用户信息却还是旧的,表现出来的现象就是:用户都换人了,App里显示的还是上一个账号的昵称和头像。后来我统一了处理方式:每次扫码登录成功后,强制走一次“刷新用户信息”的接口,把Redis里的LoginUser彻底重建,才把这个脏数据问题压下去。
提示:若依框架的Token默认有效期和Redis过期时间是通过配置文件控制的,常见配置在application.yml里的
token相关参数,比如expireTime。做App端时,建议把过期时间适当拉长,或者做“记住登录状态”时用“刷新Token + 短期Token”的双Token方案,而不是让用户频繁重新输入密码。
2.2 401拦截与Token自动续期策略
App端请求后端接口,最常见的返回就是401。但不同场景下的401,处理方式完全不同:
- 请求携带的Token过期了 → 正常,走刷新逻辑。
- Token格式非法或已被篡改 → 属于异常情况,直接踢到登录页。
- Redis服务重启后数据丢失,登录态全部失效 → 需要后端兜底处理,但现实里很多团队不会为这个场景做额外设计,导致线上一次Redis重启,所有用户集体掉线。
实际开发中,我看到的失败处理方式有两种极端:一种是什么都不做,用户点一个按钮就跳登录页;另一种是彻底无视401,导致所有请求重复执行。正确方案应该是:在前端统一封装Request拦截器,遇到401时按Token类型和来源做分流。
以uni-app环境下RuoYiApp版的常见实现为例,我会在请求响应拦截里做这样的判断:
// 伪代码:统一处理401 const handle401 = (res) => { // 1. 先从本地拿refreshToken const refreshToken = uni.getStorageSync('refresh_token'); if (!refreshToken) { // 本地连刷新凭证都没有,直接强制登录 reLaunch('/pages/login/index'); return; } // 2. 尝试用refreshToken换取新Token // 这里注意:要给刷新接口单独设置一个标志,避免死循环 if (res.config.url.includes('/refresh')) { // 刷新接口本身也返回401,说明刷新凭证也失效了 uni.removeStorageSync('token'); uni.removeStorageSync('refresh_token'); reLaunch('/pages/login/index'); return; } // 3. 调用刷新接口,成功后重放原请求 refreshTokenRequest() .then((newToken) => { uni.setStorageSync('token', newToken); // 重新发起刚才失败的请求 }) .catch(() => { // 刷新失败,强制重新登录 }); };这套流程看起来不难,但实际工程里细节不少。比如并发请求同时返回401时,如果不加锁,会同时发起多个刷新Token的请求,导致后端刷新凭证被反复消耗。我在一个项目里就遇到过这种情况:App启动时同时有三个请求并发出去,Token恰好过期了,三个请求全部401,然后同时去刷新Token,产生了竞态条件,最后用户被莫名其妙踢出登录。
解决办法是维护一个isRefreshTokenPending的标志位。第一个401进来时发起刷新,后续的401请求不立即刷新,而是排队等待第一个刷新完成后再统一重放。类似这样的“请求合并”与“队列等待”逻辑,是App生命周期管理的第一个真功夫。
2.3 框架层生命周期对业务的影响范围
这层生命周期的影响面,很多人一开始低估了。它不止决定“用户能不能登录”,还牵扯到:
- 推送消息里的业务数据:App收到一条待办推送,点击后要跳转到详情页面,但如果此时Token已过期,后端返回401,详情页打开后只能渲染一个空壳。
- WebView与原生页面的状态同步:很多若依App版项目会在内部加载H5页面,H5侧的登录态和原生的Token完全是两套体系。原生Token过期了,H5页面里的用户信息依旧显示正常,等到提交表单时才报错,用户根本不知道发生了什么。
- 第三方登录绑定:微信、支付宝、企微登录后的回调里获得的临时凭证(code),有效期极短(一般为5分钟到10分钟),如果这期间App被切走,回来后再去换Token,code早就失效了。
这些场景的共同特征就是:生命周期不只在接口层面,它会顺着业务逻辑渗透到功能的每个角落。处理框架层生命周期,不能只看后端的配置,必须结合App端实际的交互流程来设计。
3. 应用层生命周期:App前后台切换与状态恢复
3.1 uni-App中的App生命周期钩子
RuoYiApp版前端部分,我接触过两个主流实现:一个基于uni-app,一个基于原生Android。两种技术栈的生命周期钩子不太一样,但核心思路是相通的。我重点讲uni-app版本,因为它在中小团队里用得最多、上手最快。
uni-app的App生命周期挂在App.vue里,常用的钩子有四个:
onLaunch:App冷启动时触发一次,适合做全局初始化。比如检查本地Token是否存在、拉取全局配置、请求推送权限等。onShow:App从后台回到前台时触发,也可以理解为“用户重新看到App”的时刻。onHide:App切入后台时触发,适合做暂停操作、保存草稿、停止轮询等。onError:全局错误捕获,JS报错会走这里,可以统一上报。
这四个钩子里,onLaunch和onShow是最关键的。很多团队只用了onLaunch,把“检查登录态”这个动作放在这里,然后发现线上总出现“用户从后台切回来,数据刷新不及时”的情况。
举个例子:用户打开App后进入首页,然后切到微信聊天,回了一个小时消息。再切回App时,onLaunch不会执行(因为App没有被杀掉),但Token很可能已经过期了、首页的列表数据也早该刷新了。如果不在onShow里做处理,用户看到的就是一小时前的旧页面,点击任意按钮才跳出登录页,体验非常糟糕。
3.2 前后台切换的实战处理
我现在的做法是:onLaunch只做一次性初始化,onShow做状态校正。状态校正包含三件事:
第一,检查本地Token是否存在。如果不存在,直接跳登录页,不用等接口返回401。
第二,检查Token是否需要刷新。这里不一定要问后端接口,可以维护一个“最后活跃时间戳”,如果onShow时距离上次活跃时间超过了安全阈值(比Token过期时间短一些),就主动调一次刷新接口。这样能避免用户切回App的瞬间所有请求堆积成401。
第三,刷新关键页面数据。首页、工作台、消息中心这类对时效性要求高的页面,切回来时应该重新拉取数据,而不是展示本地缓存的旧数据。
关于第三点,不同页面有不同的处理策略。我的习惯是:给页面组件定义一个refreshByResume的方法,然后在App.vue的onShow里通过事件总线或者全局Store通知当前页面去执行。有的页面需要静默刷新,用户无感;有的页面要显示加载动画,防止用户以为点不动了。做一个判断即可。
性能上的提醒:onShow里的逻辑一定要轻量,不要做重量级的同步计算,不要在这个钩子里一次发十个请求,否则App从后台切回来的瞬间会非常卡顿。最好的做法是只触发“必要的数据校正”,其余等页面自己的生命周期去处理。
3.3 应用层的特殊场景:APP被系统杀死后的“复活”
Android系统在内存不足时会杀掉后台进程,这是Android的常规操作。被杀死后再从最近任务列表里点击图标恢复App,这时执行的不是onLaunch,而是冷启动onLaunch+ 页面恢复。很多人在这里会踩到页面栈丢失的坑。
假设用户之前停留在三级页面(比如审批详情页),App被杀后重新打开,系统会尝试恢复这个页面。但页面依赖的列表数据、状态机都已经丢了,页面渲染出来十有八九是空数据,甚至直接白屏。这种情况在RuoYiApp版里很常见,因为审批、工单这类业务页面通常深度较大,数据加载依赖上一页面传参。
解决方案主要有两种:
- 简单方案:在
onLaunch里判断本地是否有用户登录态,但页面栈不做恢复,统一回到首页或登录页。损失一点体验,但稳定。 - 深度方案:用状态管理框架(如Pinia、Vuex或uni-app内置的store)把关键页面状态持久化到本地存储,页面恢复时尝试重新组装。适合那些必须“回到原页面”的业务场景,比如填了一半的表单不能丢。
我自己在项目里倾向折中处理:对涉及表单填写、流程审批的页面做持久化恢复,对纯展示型页面直接回退。既控制了开发成本,也保住了最核心的体验。
4. 页面级生命周期:从onLoad到onUnload的业务陷阱
4.1 页面生命周期与组件生命周期的区别
RuoYiApp版里,页面级生命周期很多人想当然地拿Web端的Vue组件生命周期来套,结果在实际开发中总感觉“哪里不对劲”。
Web端的Vue组件生命周期主要是created、mounted、destroyed这些,而uni-app的页面生命周期是onLoad、onShow、onReady、onHide、onUnload这套。命名不同、触发时机不同,最核心的区别在于:页面在tabBar切换时并不会被销毁,再次切换回来时只会触发onShow,不会触发onLoad。
举一个真实的例子:若依App版里常见的“待办事项”和“已办事项”两个tab,用户从“待办”切到“已办”,再切回来。此时“待办”页面并没有被销毁,它只是进入了onHide状态,所以不会重新执行onLoad。如果你把数据加载逻辑放在onLoad里,那用户无论切换多少次tab,数据都不会刷新——除非他杀掉App或者把页面从栈里移除。
正确做法是把数据加载、状态刷新逻辑放在onShow里,或者根据业务需求,在onShow和onLoad间做合理分工。比如:onLoad里做一次数据初始化,onShow里做增量刷新。
4.2 tab切换与页面栈的心智模型
理解页面生命周期,最好建一个“页面栈”的心智模型。每次你从当前页面跳转到新页面,新页面入栈。每个页面的状态有五种:启动(load)、显示(show)、就绪(ready)、隐藏(hide)、卸载(unload)。
关键点在于,栈底页面不会轻易被卸载,除非用户一直按返回键把它弹出去。这就导致一个常见问题:若依App版里的长列表页面,比如“审批列表”,用户每次从列表进入详情,再返回列表,这个列表页其实一直是“显示恢复”的状态。如果列表页在onLoad时拉数据,那用户返回时看到的就是旧数据。列表数据被后端更新过,App里还是旧状态,用户会以为审批结果没提交成功。
踩过一次这个坑之后,我总结了列表页刷新策略三原则:
onLoad时做全量初始化,展示骨架屏。- 从详情页返回时,如果详情页有数据变更(通过事件或页面栈参数判断),列表页
onShow触发局部刷新。 - 下拉刷新和上拉加载的分页游标,尽量放在页面实例数据上,不要放在全局Store里,否则多个列表页会互相干扰。
4.3 页面栈溢出的生命周期管理
还有一个容易被忽略的问题:页面栈溢出。uni-app默认的页面栈上限是10层。也就是说,在层级较深的流程里(首页 → 列表 → 详情 → 表单 → 预览 → 提交结果),用户只要连续点击,就可能把页面栈堆满。超过上限后,uni-app不会报错,但你调用uni.navigateBack()或者uni.redirectTo()时,行为会变得不可预期,甚至出现“返回时跳到了奇怪的页面”。
这个问题与生命周期密切相关,因为栈满之后,新页面不再是push而是replace,老页面会被强制onUnload,它的数据状态就全部丢了。
处理方案是:
- 能不用层级跳转的,尽量用
switchTab或reLaunch控制页面流向。 - 长流程结束时,用
uni.reLaunch到固定入口,而不是继续push。 - 在业务上控制页面深度,比如审批详情页不赞成连续跳三层。
4.4 页面参数传递与生命周期的交互陷阱
最后再说一个真实的“生命周期级”bug:详情页参数丢失。若依App版经常会通过列表页跳详情页,参数直接用URL query传过去。正常情况下没问题,但Android系统回收页面后再次恢复时,URL参数可能已经丢失了,页面加载不到详情的ID,只能显示空白或报错。
解决方案是在onLoad里对参数做缓存,持久化到本地,恢复时优先读取缓存:
onLoad(options) { if (options.id) { uni.setStorageSync('detail_id_' + this.pageUniqueKey, options.id); } }然后在页面无法拿到参数时,从缓存中恢复上一个有效值。特别适合那些用户从推送消息直接跳转的场景。
5. 数据生命周期:缓存、持久化与版本升级的坑
5.1 本地缓存的生命周期管理
App端开发绕不开本地缓存。若依App版里常见的本地缓存有这么几类:
- 用户Token / RefreshToken:短期缓存,安全要求高。
- 用户基本信息(昵称、头像、角色):适度缓存,减少请求。
- 字典数据(状态、类型映射):长期缓存,可做版本控制。
- 页面草稿数据(表单内容、审批意见):按业务需求管理。
这些数据的生命周期完全不同。我在项目里推过一个原则:Token按会话管理,用户信息按版本管理,字典数据按业务变更管理,草稿按时间管理。
如果所有数据都放在同一个存储空间里,统一设置过期时间,麻烦就来了。比如用户头像变了一个月,App端还显示旧头像,用户会怀疑是“账号被别人登录了”;反过来,字典数据如果频繁过期,App每次冷启动都要拉一遍字典接口,既耗流量又伤体验。
比较好的做法是建立数据分级访问机制:
| 数据级别 | 示例 | 刷新策略 |
|---|---|---|
| 敏感数据 | Token、用户ID | 每次启动校验,过期即失效 |
| 用户数据 | 昵称、头像、角色 | 按后台推送或定时刷新 |
| 业务数据 | 消息列表、审批单 | 页面显示时主动刷新 |
| 配置数据 | 字典、系统配置 | 版本号比对,有变化才更新 |
5.2 清理缓存的时机
还有一个每个App都要面对的“人生终极问题”:缓存到底什么时候清?
若依App版里,我建议至少设置三个清缓存节点:
用户退出登录时:Token、用户信息、页面草稿全部清除。注意这里要区分“主动退出”和“被动踢出”。主动退出需要清干净;被动踢出时,用户草稿不应该被删,否则用户重新登录后发现自己写了一半的内容没了,心态原地爆炸。
版本升级时:部分缓存可能因为接口更换字段导致解析失败,建议升级后清掉旧的业务缓存,保留Token让用户无感升级。
用户手动清理时:提供“清理缓存”入口,但只清理非敏感数据,Token不要动。
我见过一个项目在“清理缓存”按钮里执行了uni.clearStorageSync(),结果用户点了一下,所有登录状态全部丢失,又得重新输入密码。这种教训真的一次就够了。
5.3 缓存一致性的两个经典场景
场景一:列表缓存与详情页不同步。用户打开审批列表,看到状态是“待审批”,点进详情,发现详情里显示“已审批”。原因往往是:列表页缓存了两小时前的数据,详情页实时加载了最新数据。如果列表页不感知变更,用户就会觉得系统有问题。
解决思路:详情页发生状态变更时,通过事件通知列表页重新拉取数据,或者列表页在onShow时比对本地数据与后端数据的更新时间戳。
场景二:字典缓存与业务数据不匹配。比如系统新增了一个订单状态“已取消”,但App端本地字典缓存还停在上一版本,界面显示出来的都是“未知状态”或者干脆空白。解决思路:给字典数据加版本号,后端升级时主动推送版本变更,App在启动或页面加载时做一次版本比对,不一致就重新拉取。
5.4 版本升级引起的“生命周期断裂”
最后讲一个很多人做得不到位的点:版本升级后的生命周期管理。
App版本升级后,本地持久化的数据不一定兼容。比如接口从/api/order/list改成了/api/order/v2/list,返回的数据结构也变了,但用户本地缓存里还保留着老结构的数据。此时如果App冷启动直接读取本地缓存渲染页面,轻则显示异常,重则直接白屏崩溃。
我在实际项目里的处理方式是:
- 在本地存储一个
version字段,与App的版本号绑定。 onLaunch时检查版本号,如果不一致,就清掉所有业务缓存。- 只保留
token、refresh_token这类“凭证类”数据,让用户无感完成版本升级。
这样做的好处是,版本升级过程中用户的登录状态不丢失,但业务数据全部重置,从根上杜绝了“老数据渲染新页面”的兼容性问题。
6. 常见问题与排查技巧实录
这一节我把做若依App版时实际遇到过的生命周期相关问题整理成清单,按照“现象 → 排查思路 → 解决方案”的格式写,方便直接套用。
6.1 频繁掉线的真实案例
现象:用户反馈用着用着突然跳到登录页,一天能掉三四次,且没有规律。
排查过程:我先看了后端日志,发现最后一条请求都是400或401。仔细核对后发现,并非Token过期,而是Redis中的用户登录信息数据量太大,超过了默认的内存上限,导致缓存被逐出。第一次排查方向完全错了,我一度怀疑是前端token刷新逻辑有bug,直到把Redis的maxmemory-policy检查了一遍才找到真正的元凶。
解决:调整Redis内存策略,同时把若依存入Redis的用户权限数据精简(很多角色根本没有用到,全量缓存纯属浪费)。具体做法是把字符串压缩、只存必要字段。另外在App侧也做了兜底:如果请求返回401,且本地Token存在,则尝试重新登录一次;如果重登也失败,才跳登录页。
6.2 切后台回来白屏
现象:用户刷了一会儿抖音,再切回App时,页面变成白屏,必须杀掉进程重新打开。
排查过程:WebView内存被系统回收导致的页面状态丢失。但不同机型的处理方式不太一样,有的直接白屏,有的是页面错位。
解决:在App.vue的onShow里捕获到页面异常时,使用reLaunch回到首页并重新加载数据。另外,对WebView加载的H5页面统一配置了页面恢复机制,确保H5侧能感知到原生App的onHide和onShow事件。
6.3 消息推送点击无响应
现象:收到推送,点击通知栏消息,App打开了,但停留在启动页,既不跳转也不显示数据。
排查过程:推送点击时App处于冷启动状态,onLaunch还没执行完,推送事件的回调先触发了,导致跳转逻辑拿不到Token和页面参数。
解决:推送事件统一做一个“缓存待处理事件”,在onLaunch完成初始化后再依次消费。核心代码逻辑:
// 冷启动时,推送事件先存入队列 onLaunch() { this.initApp().then(() => { this.consumePendingEvents(); }); } onPushMessage(msg) { if (!this.isAppReady) { this.pendingEvents.push(msg); } else { this.handleMessage(msg); } }6.4 页面加载顺序导致的“取不到值”
现象:在onLoad里调接口,在页面上渲染时,接口还没返回,导致页面出现一闪而过的空数据。
排查过程:这个问题的本质不是生命周期,而是异步请求与页面渲染的时序关系。onLoad只是发起了请求,并没有等待请求完成。
解决:页面用v-if控制渲染时机,或者在数据字段初始值里设置一个loading状态。这个技巧在若依App版的列表页、详情页里非常实用:
<template v-if="pageLoading"> <!-- 骨架屏 --> </template> <template v-else> <!-- 真实内容 --> </template>6.5 排查生命周期的工具推荐
调试生命周期问题,我常用这几个工具组合:
- 浏览器开发者工具的Performance面板,记录页面加载性能,看哪个阶段耗时最长。
- uni-app内置的
onError钩子,全局捕获JS报错。 - 后端接口返回的Header头中查看Token刷新时间和过期时间,确认前后端是否一致。
- 日志上报系统,把关键生命周期事件(onLaunch、onShow、401、token刷新)打点记录,线上问题可以通过日志还原用户操作路径。
7. 一些关于设计思维的总结思考
走到这里,你会发现“生命周期”这个概念在若依App版里远不止是几个钩子函数,它更像是一套运行时的调度策略。Token的刷新、页面的隐藏与显示、数据的过期与恢复、缓存的清理与重建,全都是在回答同一个问题:在某个时刻,App和用户之间的状态是否一致?
我在实际开发中有一个越来越笃定的体会:处理生命周期问题,不要试图把每个场景都做成独立的补丁,而是要建立一套统一的“状态校正”机制。每次App从非活跃状态恢复到活跃状态时,主动做一次状态校准,把Token、数据、页面这三个维度的状态拉到最新。这套机制一旦建立起来,后面加新功能、接新页面都会非常省心。
如果你是刚接触若依App版,我的建议是先花一天时间把uni-app的生命周期文档和若依的Token机制读透,然后动手写一个“登录 → 列表 → 详情 → 退出”的完整链路,每个阶段都打上日志,仔细观察触发时机。这个过程跑完之后,你对整个框架的理解会上一个台阶,再去看业务代码里的各种奇怪问题,思路会清晰很多。
最后再分享一个小经验:不要把生命周期相关的判断逻辑散落在各个页面里,最好在公共层统一处理。我在项目里就是把这些逻辑都收敛到了核心Store和请求封装层,页面代码干净很多,排查问题也不用翻几十个文件了。