基于React Router v6 实现的页面路由缓存(keep-alive)包含组件销毁功能与页面active功能
网上查阅资料后发现大部分不是很符合自己的使用想法,遂借鉴了别人的代码,代码如下
KeepAlive组件
import ReactDOM from 'react-dom'
import { equals, isNil, map, filter,propEq, findIndex } from 'ramda'
import { useUpdate } from 'ahooks'
import {
JSXElementConstructor,
memo,
ReactElement,
RefObject,
useEffect,
useRef,
useState,
createContext,
} from 'react'
import { useLocation } from 'react-router'
type Children = ReactElement<any, string | JSXElementConstructor<any>> | null
interface context {
destroy: (params: string, render?: boolean) => void,
isActive: boolean
}
export const KeepAliveContext = createContext<context>({ destroy: () => { }, isActive: false })
interface Props {
activeName?: string
include?: Array<string>
exclude?: Array<string>
maxLen?: number
children: Children
}
function KeepAlive({ children, exclude, include, maxLen = 5 }: Props,) {
const containerRef = useRef<HTMLDivElement>(null)
const components = useRef<Array<{ name: string; ele: Children }>>([])
const { pathname } = useLocation()
const update = useUpdate()
const isActive = findIndex(propEq('name', pathname))(components.current)
//如果没有配置include,exclude 则不缓存
if (isNil(exclude) && isNil(include)) {
components.current = [
{
name: pathname,
ele: children,
}
]
} else {
// 缓存超过上限的 干掉第一个缓存
if (components.current.length >= maxLen) {
components.current = components.current.slice(1)
}
components.current = filter(({ name }) => {
if (exclude && exclude.includes(name)) {
return false
}
if (include) {
return include.includes(name)
}
return true
}, components.current)
const component = components.current.find((res) => equals(res.name, pathname))
if (isNil(component)) {
components.current = [
...components.current,
{
name: pathname,
ele: children,
},
]
}
}
//销毁缓存的路由
function destroy(params: string, render = false) {
components.current = filter(({ name }) => {
if (params === name) {
return false
}
return true
}, components.current)
//是否需要立即刷新 一般是不需要的
if (render) {
update()
}
}
const context = {
destroy,
isActive: isActive !== -1
}
return (
<>
<div ref={containerRef} className="keep-alive" />
<KeepAliveContext.Provider value={context}>
{map(
({ name, ele }) => (
<Component active={equals(name, pathname)} renderDiv={containerRef} name={name} key={name} >
{ele}
</Component>
),
components.current
)}
</KeepAliveContext.Provider >
</>
)
}
export default memo(KeepAlive)
interface ComponentProps {
active: boolean
children: Children
name: string
renderDiv: RefObject<HTMLDivElement>
}
// 渲染 当前匹配的路由 不匹配的 利用createPortal 移动到 document.createElement('div') 里面
function Component({ active, children, name, renderDiv }: ComponentProps) {
const [targetElement] = useState(() => document.createElement('div'))
const activatedRef = useRef(false)
activatedRef.current = activatedRef.current || active
useEffect(() => {
if (active) {// 渲染匹配的组件
if (renderDiv.current?.firstChild) {
renderDiv.current?.replaceChild(targetElement, renderDiv.current?.firstChild)
} else {
renderDiv.current?.appendChild(targetElement)
}
}
}, [active])
useEffect(() => {// 添加一个id 作为标识 并没有什么太多作用
targetElement.setAttribute('id', name)
}, [name])
// 把vnode 渲染到document.createElement('div') 里面
return <>{activatedRef.current && ReactDOM.createPortal(children, targetElement)}</>
}
export const KeepAliveComponent = memo(Component)
使用方式如下
Layout组件
import { useOutlet } from "react-router-dom"
import QueueAnim from "rc-queue-anim";
import ErrorBoundary from "@/common/errorBoundary/ErrorBoundary"
import KeepAlive from "@/components/keepalive/KeepAlive";
function AdminLayout(){
、、略、、
const Outlet = useOutlet()
return (
、、略、、
<ErrorBoundary history={Location}>
<KeepAlive include={['/admin/zip', '/admin/clipboard']}>
<QueueAnim style={{ height: '100%' }}>
<div key={1}>
{Outlet}
</div>
</QueueAnim>
</KeepAlive>
</ErrorBoundary>
)
}
Dashboard组件
import { useContext, } from "react"
import { Button } from "antd";
import { KeepAliveContext } from "@/components/keepalive/KeepAlive";
export default function Dashboard() {
const { destroy } = useContext(KeepAliveContext)
return (
<div style={{ height: '100%' }}>
<Button type="primary" onClickCapture={() => destroy('/admin/clipboard', true)}>销毁组件</Button>
</div>
)
}
zip组件
import { Button, Input } from "antd"
import { KeepAliveContext } from "@/components/keepalive/KeepAlive";
import { useEffect, useState, useContext, } from "react"
export default function Zip() {
let [name, setName] = useState("")
const { destroy, isActive } = useContext(KeepAliveContext)
const download = () => {
''略''
}
useEffect(() => {
console.log('页面激活', isActive)
}, [isActive])
return (
<div>
<Input.Group>
<Input value={name} onChange={({ target }) => setName(target.value)} style={{ width: '200px' }} placeholder="请输入下载文件名"></Input>
<Button onClick={download} type="primary">下载zip</Button>
</Input.Group>
</div>
)
}