React中实现keepalive组件缓存效果的方法详解
coder__wang 人气:0背景
由于react官方并没有提供缓存组件相关的api(类似vue中的keepalive),在某些场景,会使得页面交互性变的很差,比如在有搜索条件的表格页面,点击某一条数据跳转到详情页面,再返回表格页面,会重新请求数据,搜索条件也将清空,用户得重新输入搜索条件,再次请求数据,大大降低办公效率,如图:
目标:封装keepalive缓存组件,实现组件的缓存,并暴露相关方法,可以手动清除缓存。
版本:React 17,react-router-dom 5
结构
代码
cache-types.js
// 缓存状态 export const CREATE = 'CREATE'; // 创建 export const CREATED = 'CREATED'; // 创建成功 export const ACTIVE = 'ACTIVE'; // 激活 export const DESTROY = 'DESTROY'; // 销毁
CacheContext.js
import React from 'react'; const CacheContext = React.createContext(); export default CacheContext;
KeepAliveProvider.js
import React, { useReducer, useCallback } from "react"; import CacheContext from "./CacheContext"; import cacheReducer from "./cacheReducer"; import * as cacheTypes from "./cache-types"; function KeepAliveProvider(props) { let [cacheStates, dispatch] = useReducer(cacheReducer, {}); const mount = useCallback( ({ cacheId, element }) => { // 挂载元素方法,提供子组件调用挂载元素 if (cacheStates[cacheId]) { let cacheState = cacheStates[cacheId]; if (cacheState.status === cacheTypes.DESTROY) { let doms = cacheState.doms; doms.forEach((dom) => dom.parentNode.removeChild(dom)); dispatch({ type: cacheTypes.CREATE, payload: { cacheId, element } }); // 创建缓存 } } else { dispatch({ type: cacheTypes.CREATE, payload: { cacheId, element } }); // 创建缓存 } }, [cacheStates] ); let handleScroll = useCallback( // 缓存滚动条 (cacheId, { target }) => { if (cacheStates[cacheId]) { let scrolls = cacheStates[cacheId].scrolls; scrolls[target] = target.scrollTop; } }, [cacheStates] ); return ( <CacheContext.Provider value={{ mount, cacheStates, dispatch, handleScroll }} > {props.children} {/* cacheStates维护所有缓存信息, dispatch派发修改缓存状态*/} {Object.values(cacheStates) .filter((cacheState) => cacheState.status !== cacheTypes.DESTROY) .map(({ cacheId, element }) => ( <div id={`cache_${cacheId}`} key={cacheId} // 原生div中声明ref,当div渲染到页面,会执行ref中的回调函数,这里在id为cache_${cacheId}的div渲染完成后,会继续渲染子元素 ref={(dom) => { let cacheState = cacheStates[cacheId]; if ( dom && (!cacheState.doms || cacheState.status === cacheTypes.DESTROY) ) { let doms = Array.from(dom.childNodes); dispatch({ type: cacheTypes.CREATED, payload: { cacheId, doms }, }); } }} > {element} </div> ))} </CacheContext.Provider> ); } const useCacheContext = () => { const context = React.useContext(CacheContext); if (!context) { throw new Error("useCacheContext必须在Provider中使用"); } return context; }; export { KeepAliveProvider, useCacheContext };
withKeepAlive.js
import React, { useContext, useRef, useEffect } from "react"; import CacheContext from "./CacheContext"; import * as cacheTypes from "./cache-types"; function withKeepAlive( OldComponent, { cacheId = window.location.pathname, scroll = false } ) { return function (props) { const { mount, cacheStates, dispatch, handleScroll } = useContext(CacheContext); const ref = useRef(null); useEffect(() => { if (scroll) { // scroll = true, 监听缓存组件的滚动事件,调用handleScroll()缓存滚动条 ref.current.addEventListener( "scroll", handleScroll.bind(null, cacheId), true ); } }, [handleScroll]); useEffect(() => { let cacheState = cacheStates[cacheId]; if ( cacheState && cacheState.doms && cacheState.status !== cacheTypes.DESTROY ) { // 如果真实dom已经存在,且状态不是DESTROY,则用当前的真实dom let doms = cacheState.doms; doms.forEach((dom) => ref.current.appendChild(dom)); if (scroll) { // 如果scroll = true, 则将缓存中的scrollTop拿出来赋值给当前dom doms.forEach((dom) => { if (cacheState.scrolls[dom]) dom.scrollTop = cacheState.scrolls[dom]; }); } } else { // 如果还没产生真实dom,派发生成 mount({ cacheId, element: <OldComponent {...props} dispatch={dispatch} />, }); } }, [cacheStates, dispatch, mount, props]); return <div id={`keepalive_${cacheId}`} ref={ref} />; }; } export default withKeepAlive;
index.js
export { KeepAliveProvider } from "./KeepAliveProvider"; export {default as withKeepAlive} from './withKeepAlive';
使用:
1.用<KeepAliveProvider></KeepAliveProvider>将目标缓存组件或者父级包裹;
2.将需要缓存的组件,传入withKeepAlive方法中,该方法返回一个缓存组件;
3.使用该组件;
App.js
import React from "react"; import { BrowserRouter, Link, Route, Switch, } from "react-router-dom"; import Home from "./Home.js"; import List from "./List.js"; import Detail from "./Detail.js"; import { KeepAliveProvider, withKeepAlive } from "./keepalive-cpn"; const KeepAliveList = withKeepAlive(List, { cacheId: "list", scroll: true }); function App() { return ( <KeepAliveProvider> <BrowserRouter> <ul> <li> <Link to="/">首页</Link> </li> <li> <Link to="/list">列表页</Link> </li> <li> <Link to="/detail">详情页A</Link> </li> </ul> <Switch> <Route path="/" component={Home} exact></Route> <Route path="/list" component={KeepAliveList}></Route> <Route path="/detail" component={Detail}></Route> </Switch> </BrowserRouter> </KeepAliveProvider> ); } export default App;
效果:
假设有个需求,从首页到列表页,需要清空搜索条件,重新请求数据,即回到首页,需要清除列表页的缓存。
上面的KeepAliveProvider.js中,暴露了一个useCacheContext()的hook,该hook返回了缓存组件相关数据和方法,这里可以用于清除缓存:
Home.js
import React, { useEffect } from "react"; import { DESTROY } from "./keepalive-cpn/cache-types"; import { useCacheContext } from "./keepalive-cpn/KeepAliveProvider"; const Home = () => { const { cacheStates, dispatch } = useCacheContext(); const clearCache = () => { if (cacheStates && dispatch) { for (let key in cacheStates) { if (key === "list") { dispatch({ type: DESTROY, payload: { cacheId: key } }); } } } }; useEffect(() => { clearCache(); // eslint-disable-next-line }, []); return ( <div> <div>首页</div> </div> ); }; export default Home;
效果:
至此,react简易版的keepalive组件已经完成啦~
加载全部内容