基于React Router v6 实现的页面路由缓存(keep-alive)

4,159 阅读2分钟

基于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>

    )
}

从react调试工具查看

image.png

image.png