更新
2022.08.11
-
- 改用一个全局变量去记录前一个页面
fromPathCache
- 改用一个全局变量去记录前一个页面
-
- 为了避免useEffect闭包的问题,我把history.listen需要执行的方法抽出来,然后写了一个
useLatestFn自定义hook,保证监听里面拿到的数据总是最新的
- 为了避免useEffect闭包的问题,我把history.listen需要执行的方法抽出来,然后写了一个
-
- 不知道为什么新版本的react-activation中的
useAliveController执行完得到的是一个空对象,导致我无法清除keepAlive缓存,我把react-activation的版本锁定为0.9.12就好了
- 不知道为什么新版本的react-activation中的
2022.08.28
aliveController?.refreshScope 改成了 aliveController?.dropScope,否则离开的页面相当于重新加载一遍,里面的请求也会执行。
const aliveController = useAliveController()
...
if (nameKey) {
aliveController?.dropScope && aliveController.dropScope(nameKey) // aliveController?.refreshScope 改成了 aliveController?.dropScope
} else {
aliveController?.clear && aliveController.clear()
}
...
2022.08.30
react18里有一个未开放的api <Offscreen>,这个是官方将要推出的keepAlive,目前还没有稳定版
参考资料:github.com/reactwg/rea…
2022.11.07
往github上传了一个demo,项目是一个微前端项目,react-keep-alive用在子应用mall-web中,可以搜索下一下react-keep-alive关键字找到使用的地方
原文
介绍1
后台管理系统,列表页跳转详情页,想要实现类似vue keep-alive的效果。
具体表现为:
- 从列表页A跳转A的详情页,列表页A缓存
- 详情页没做任何操作,跳回列表页A,列表页A不刷新,列表页A页码不变
- 详情页进行了
编辑操作,跳回列表页A,列表页A刷新,列表页A页码不变 - 详情页进行了
新建操作,跳回列表页A,列表页A刷新,列表页A页码变为1
- 从列表页A跳转列表页B,列表页A不缓存,即各个列表之间跳转不缓存
总结就是,一个页面只有跳转指定页面的时候才缓存,并且当返回这个被缓存的页面时,可以控制是否刷新(返回传参)。
介绍2
上一篇文章中,是配合状态管理实现的,但是这样感觉耦合比较大,这个版本不依赖状态管理。
所需依赖
"react": "17.x",
"react-dom": "17.x",
"react-router-dom": "5.3.3",
"react-activation": "0.9.12", // 2022.08.11 锁定版本
注意:这里react-router的版本是5
注意:这里react-router的版本是5
注意:这里react-router的版本是5
完整代码
components/react-keep-alive
components
└───react-keep-alive
index.js
useLatestFn.js // 2022.08.11 新增
utils.js
react-keep-alive/index.js
import { useEffect, useRef } from 'react'
import { useLocation, useHistory } from 'react-router-dom'
import KeepAlive, { AliveScope, useAliveController, useActivate } from 'react-activation'
import { treeToList } from './utils'
import useLatestFn from './useLatestFn'
let allFlatRoutes = []
let keepAliveRoutes = []
let HISTORY_UNLISTEN = null
let fromPathCache = '' // 记录前一个页面
// 初始化 allFlatRoutes keepAliveRoutes
function initRoutes(routes) {
allFlatRoutes = treeToList(routes) // 所有路由
keepAliveRoutes = allFlatRoutes.filter((item) => item.meta?.keepAlive) // keepAlive的路由
}
// 判断props.children是否需要被 <KeepAlive> 包裹
function KeepAliveWrapper(props) {
const location = useLocation()
const curPath = location.pathname
const routeItem = keepAliveRoutes.find((item) => item.path == curPath)
let dom = props.children
if (routeItem) {
dom = (
<KeepAlive
id={curPath} // id 用于多个keepAlive // id 一定要加 否则 keepAlive的页面 跳转 另一个keepAlive的页面 会有问题
name={curPath} // name 用于手动控制缓存
>
{props.children}
</KeepAlive>
)
}
return dom
}
// 监听路由 手动控制 keepAlive缓存
function useKeepAlive() {
const location = useLocation()
const history = useHistory()
const aliveController = useAliveController()
// 清除keep alive缓存
function clearKeepAlive(nameKey) {
setTimeout(() => {
try {
const cachingNodes = aliveController?.getCachingNodes()
console.log('cachingNodes', cachingNodes)
if (nameKey) {
aliveController?.dropScope && aliveController.dropScope(nameKey)
} else {
aliveController?.clear && aliveController.clear()
}
} catch (e) {
console.log(e)
}
}, 10)
}
const listenCallbackRef = useLatestFn((to, type) => {
const toPath = to.pathname
const fromPath = fromPathCache || toPath
console.log('fromPath 当前页面', fromPath)
console.log('toPath', toPath)
fromPathCache = toPath
const routeItem = keepAliveRoutes.find((item) => item.path == fromPath) // from页面 是一个需要keepAlive的页面
const curPathIsKeepAliveToPath_ParentItem = keepAliveRoutes.find((item) => item.meta?.keepAlive?.toPath == fromPath) // from页面 是一个 需要keepAlive的页面的toPath
// 控制keepAlive缓存
if (routeItem) {
console.log('from页面 是一个需要keepAlive的列表页面', routeItem)
if (toPath == routeItem.meta?.keepAlive.toPath) {
console.log('toPath 正好是当前这个路由的 keepAlive.toPath')
} else {
console.log('toPath 不是当前这个路由的 keepAlive.toPath,清除')
clearKeepAlive(fromPath)
}
}
// from页面 是一个 需要keepAlive的页面的toPath
if (curPathIsKeepAliveToPath_ParentItem) {
console.log('from页面 是某个keepAlive的页面 的toPath')
if (curPathIsKeepAliveToPath_ParentItem.path == toPath) {
console.log('所去的 页面是 parentItem.path,不做什么')
} else {
console.log('清除 parentItem.path keepAlive')
clearKeepAlive(curPathIsKeepAliveToPath_ParentItem.path)
}
}
})
useEffect(() => {
HISTORY_UNLISTEN = history.listen((to, type) => {
listenCallbackRef.current && listenCallbackRef.current(to, type)
})
return () => {
HISTORY_UNLISTEN && HISTORY_UNLISTEN()
}
}, [])
return KeepAliveWrapper
}
/**
*
* activate 传参
*/
const KEEP_ALIVE_OPTIONS_KEY = (key) => {
return window.location.origin + '_KEEP_ALIVE_OPTIONS_KEY_' + key
}
// 页面activate的时候触发 一般就是从detail页返回
function useActivateWithOptions(activatePageKey, callback) {
useActivate(() => {
let options = {}
try {
let optionsJsonStr = localStorage.getItem(KEEP_ALIVE_OPTIONS_KEY(activatePageKey))
const obj = JSON.parse(optionsJsonStr)
if (Object.prototype.toString.call(obj) === '[object Object]') {
options = obj
}
} catch {}
localStorage.removeItem(KEEP_ALIVE_OPTIONS_KEY(activatePageKey))
callback && callback(options)
})
}
function setActivateOptions(activatePageKey, options = {}) {
const optionsJsonStr = JSON.stringify(options)
localStorage.setItem(KEEP_ALIVE_OPTIONS_KEY(activatePageKey), optionsJsonStr)
}
export {
initRoutes,
AliveScope,
KeepAliveWrapper,
useKeepAlive,
//
useActivateWithOptions as useActivate,
setActivateOptions,
}
react-keep-alive/useLatestFn.js
import { useEffect, useRef } from 'react'
// 2022.08.11 新增
function useLatestFn(fn) {
const fnRef = useRef(null)
useEffect(() => {
fnRef.current = fn
})
return fnRef
}
export default useLatestFn
react-keep-alive/utils.js
// tree扁平化
export function treeToList(tree, childrenKey = 'routes') {
let arr = []
tree.forEach((item) => {
if (item[childrenKey] && item[childrenKey].length > 0) {
arr = [...arr, ...treeToList(item[childrenKey], childrenKey)]
} else {
arr.push(item)
}
})
return arr
}
解析/使用
1、给路由增加meta
这个项目使用的是集中式配置路由,我增加了meta属性,meta.keepAlive存在表示这是一个需要被keepAlive的路由,meta.keepAlive.toPath表示只有当前往这个路由的时候,需要缓存
const routes = [
...
{
name: '商品管理',
path: '/web/supplier/goods/mallgoodsmgr',
component: './supplier/goods/goodsManage',
meta: {
keepAlive: {
toPath: '/web/supplier/goods/mallgoodsmgr/detail', // 只有去详情页的时候 才需要缓存 商品管理(列表页)路由
},
},
}
...
]
2、根组件中代码
import { observer } from 'mobx-react'
import { Card } from 'antd'
import ContentLayout from '@/components/layout/ContentLayout'
import HeaderAccount from '@/components/HeaderAccount'
import { initRoutes, AliveScope, KeepAliveWrapper, useKeepAlive } from '@/components/react-keep-alive'
import routes from '../../../config/routes'
import styles from './index.less'
initRoutes(routes) // 这里先初始化一下routes
function Index(props) {
useKeepAlive()
return (
<div className={styles.main_wrap}>
<AliveScope>
<Card bordered={false} bodyStyle={{ padding: 10 }} style={{ minHeight: '50vh' }}>
<KeepAliveWrapper>{props.children}</KeepAliveWrapper>
</Card>
</AliveScope>
</div>
)
}
export default observer(Index)
(1) initRoutes
initRoutes传入树形的routes,通过tree的扁平化计算,拿到全部的路由allFlatRoutes,和需要keepAlive的(即有meta.keepAlive属性)路由keepAliveRoutes,这两个变量后面会用到
// 初始化 allFlatRoutes keepAliveRoutes
function initRoutes(routes) {
allFlatRoutes = treeToList(routes) // 所有路由
keepAliveRoutes = allFlatRoutes.filter((item) => item.meta?.keepAlive) // keepAlive的路由
}
(2) <AliveScope/>、<KeepAliveWrapper>
在根组件中,用<AliveScope/>包裹整个应用,react-activation中需要缓存的页面要用<KeepAlive/>包裹。文档中这部分写在<App/>中,因为我的项目是umi所以写在了layouts里。
我封装了一个<KeepAliveWrapper>,其实里面就是做了一个判断,如果当前的页面可以从keepAliveRoutes中找到,说明当前页面是一个需要keepAlive的页面,就用<KeepAlive>包裹
// 判断props.children是否需要被 <KeepAlive> 包裹
function KeepAliveWrapper(props) {
const location = useLocation()
const curPath = location.pathname
const routeItem = keepAliveRoutes.find((item) => item.path == curPath)
let dom = props.children
if (routeItem) {
dom = (
<KeepAlive
id={curPath} // id 用于多个keepAlive // id 一定要加 否则 keepAlive的页面 跳转 另一个keepAlive的页面 会有问题
name={curPath} // name 用于手动控制缓存
>
{props.children}
</KeepAlive>
)
}
return dom
}
(3) useKeepAlive 主要用于监听路由的变化,手动控制keepAlive的缓存
介绍概念:
- fromPath指从哪个页面跳转的,即当前所在页面
- toPath指跳转的页面
我用fromPathCache这个变量记录前一个页面。
之后就是用fromPath和toPath,去判断是否清除fromPath的keepAlive缓存
3、从详情页返回列表时传参,控制列表页是否刷新
react-activation提供的useActivate勾子是不支持传参,这里我们优化一下。
我的思路是,在详情页把参数options保存在本地缓存,返回列表的时候会触发react-activation的useActivate,读取本地缓存中的options返出来,然后立即localStorage.removeItem掉,这样就不会污染本地缓存环境。
然后我还增加了一个activatePageKey,指定是给哪个keepAlive的页面传参
const KEEP_ALIVE_OPTIONS_KEY = (key) => {
return window.location.origin + '_KEEP_ALIVE_OPTIONS_KEY_' + key
}
// 页面activate的时候触发 一般就是从detail页返回
function useActivateWithOptions(activatePageKey, callback) {
useActivate(() => {
let options = {}
try {
let optionsJsonStr = localStorage.getItem(KEEP_ALIVE_OPTIONS_KEY(activatePageKey))
const obj = JSON.parse(optionsJsonStr)
if (Object.prototype.toString.call(obj) === '[object Object]') {
options = obj
}
} catch {}
localStorage.removeItem(KEEP_ALIVE_OPTIONS_KEY(activatePageKey))
callback && callback(options)
})
}
function setActivateOptions(activatePageKey, options = {}) {
const optionsJsonStr = JSON.stringify(options)
localStorage.setItem(KEEP_ALIVE_OPTIONS_KEY(activatePageKey), optionsJsonStr)
}
使用——列表页
useActivateWithOptions导出的时候我修改了名字,直接引入useActivate就行
export {
initRoutes,
AliveScope,
KeepAliveWrapper,
useKeepAlive,
//
useActivateWithOptions as useActivate,
setActivateOptions,
}
import { useActivate } from '@/components/react-keep-alive'
function Index() {
...
// 从详情页返回的时候 会触发
useActivate('supplier_goodsMng', (options) => {
console.log('supplier_goodsMng active options', options)
})
...
}
export default Index
使用——详情页
在新建/编辑成功的时候setActivateOptions设置options,然后正常返回就可以了
import { setActivateOptions } from '@/components/react-keep-alive'
...
...
.finally(() => {
setSubmitLoading(false)
})
.then(() => {
message.success('操作成功')
setActivateOptions('supplier_goodsMng', isEdit ? { isEditSuccess: true } : { isAddSuccess: true })
history.goBack()
})
...
相关文档
关于react-router v6
最近在一个react-router v6的项目上尝试增加react-activation,没有成功。。。0.0,我看react-activation的issues中说是支持v6,但我的项目中,被<KeepAlive>包裹的页面总是缓存不下来。
后来了解到react-router v6提供了一个useOuLet,过段时间研究一下是否可以用这个api实现。。
思考
经过这段时间的探索,我产生了一个疑问,为什么vue拥有了很久的keep-alive组件,react实现起来要这么麻烦,需要依赖第三方的插件,官方的想法是怎样的,为什么不推出一个类似keepAlive的功能呢?
我找到了这几个对react keepAlive进行讨论的帖子,总结了一下,react不推出keepalive的原因主要有这几点:
- 页面很多/很重的情况,可能会引起
内存泄漏 - keepAlive的功能会破坏生命周期,引入了keepalive之后,原有的生命周期会失效,为了解决就需要增加和keepalive相关的勾子,比如
useActivate,为了解决一个东西,增加了更多的东西 不符合react的设计理念,react不推崇缓存vnode,推崇缓存状态,这个我想应该是最重要的,可以在react核心开发者dan参与回答的issues中可见一斑
react核心开发者dan参与回答的issues推荐阅读
vue的keep-alive非常实用,为什么angular和react都不跟进呢?
Vue中keep-alive对体验提升的作用与问题及解决方案
结论
可见react貌似不会给出类似keepalive的功能,dan给出的解决方案是:
- 要么保存状态(state)
- 要么直接使用最简洁的display:none;