背景:由于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
1 import React, { useReducer, useCallback } from "react"; 2 import CacheContext from "./CacheContext"; 3 import cacheReducer from "./cacheReducer"; 4 import * as cacheTypes from "./cache-types"; 5 function KeepAliveProvider(props) { 6 let [cacheStates, dispatch] = useReducer(cacheReducer, {}); 7 const mount = useCallback( 8 ({ cacheId, element }) => { 9 // 挂载元素方法,提供子组件调用挂载元素 10 if (cacheStates[cacheId]) { 11 let cacheState = cacheStates[cacheId]; 12 if (cacheState.status === cacheTypes.DESTROY) { 13 let doms = cacheState.doms; 14 doms.forEach((dom) => dom.parentNode.removeChild(dom)); 15 dispatch({ type: cacheTypes.CREATE, payload: { cacheId, element } }); // 创建缓存 16 } 17 } else { 18 dispatch({ type: cacheTypes.CREATE, payload: { cacheId, element } }); // 创建缓存 19 } 20 }, 21 [cacheStates] 22 ); 23 let handleScroll = useCallback( 24 // 缓存滚动条 25 (cacheId, { target }) => { 26 if (cacheStates[cacheId]) { 27 let scrolls = cacheStates[cacheId].scrolls; 28 scrolls[target] = target.scrollTop; 29 } 30 }, 31 [cacheStates] 32 ); 33 return ( 34 <CacheContext.Provider 35 value={{ mount, cacheStates, dispatch, handleScroll }} 36 > 37 {props.children} 38 {/* cacheStates维护所有缓存信息, dispatch派发修改缓存状态*/} 39 {Object.values(cacheStates) 40 .filter((cacheState) => cacheState.status !== cacheTypes.DESTROY) 41 .map(({ cacheId, element }) => ( 42 <div 43 id={`cache_${cacheId}`} 44 key={cacheId} 45 // 原生div中声明ref,当div渲染到页面,会执行ref中的回调函数,这里在id为cache_${cacheId}的div渲染完成后,会继续渲染子元素 46 ref={(dom) => { 47 let cacheState = cacheStates[cacheId]; 48 if ( 49 dom && 50 (!cacheState.doms || cacheState.status === cacheTypes.DESTROY) 51 ) { 52 let doms = Array.from(dom.childNodes); 53 dispatch({ 54 type: cacheTypes.CREATED, 55 payload: { cacheId, doms }, 56 }); 57 } 58 }} 59 > 60 {element} 61 </div> 62 ))} 63 </CacheContext.Provider> 64 ); 65 } 66 const useCacheContext = () => { 67 const context = React.useContext(CacheContext); 68 if (!context) { 69 throw new Error("useCacheContext必须在Provider中使用"); 70 } 71 return context; 72 }; 73 export { KeepAliveProvider, useCacheContext };
withKeepAlive.js
1 import React, { useContext, useRef, useEffect } from "react"; 2 import CacheContext from "./CacheContext"; 3 import * as cacheTypes from "./cache-types"; 4 function withKeepAlive( 5 OldComponent, 6 { cacheId = window.location.pathname, scroll = false } 7 ) { 8 return function (props) { 9 const { mount, cacheStates, dispatch, handleScroll } = 10 useContext(CacheContext); 11 const ref = useRef(null); 12 useEffect(() => { 13 if (scroll) { 14 // scroll = true, 监听缓存组件的滚动事件,调用handleScroll()缓存滚动条 15 ref.current.addEventListener( 16 "scroll", 17 handleScroll.bind(null, cacheId), 18 true 19 ); 20 } 21 }, [handleScroll]); 22 useEffect(() => { 23 let cacheState = cacheStates[cacheId]; 24 if ( 25 cacheState && 26 cacheState.doms && 27 cacheState.status !== cacheTypes.DESTROY 28 ) { 29 // 如果真实dom已经存在,且状态不是DESTROY,则用当前的真实dom 30 let doms = cacheState.doms; 31 doms.forEach((dom) => ref.current.appendChild(dom)); 32 if (scroll) { 33 // 如果scroll = true, 则将缓存中的scrollTop拿出来赋值给当前dom 34 doms.forEach((dom) => { 35 if (cacheState.scrolls[dom]) 36 dom.scrollTop = cacheState.scrolls[dom]; 37 }); 38 } 39 } else { 40 // 如果还没产生真实dom,派发生成 41 mount({ 42 cacheId, 43 element: <OldComponent {...props} dispatch={dispatch} />, 44 }); 45 } 46 }, [cacheStates, dispatch, mount, props]); 47 return <div id={`keepalive_${cacheId}`} ref={ref} />; 48 }; 49 } 50 export default withKeepAlive;
index.js
export { KeepAliveProvider } from "./KeepAliveProvider"; export {default as withKeepAlive} from './withKeepAlive';
使用:
1.用<KeepAliveProvider></KeepAliveProvider>将目标缓存组件或者父级包裹;
2.将需要缓存的组件,传入withKeepAlive方法中,该方法返回一个缓存组件;
3.使用该组件;
App.js
1 import React from "react"; 2 import { 3 BrowserRouter, 4 Link, 5 Route, 6 Switch, 7 } from "react-router-dom"; 8 import Home from "./Home.js"; 9 import List from "./List.js"; 10 import Detail from "./Detail.js"; 11 import { KeepAliveProvider, withKeepAlive } from "./keepalive-cpn"; 12 13 const KeepAliveList = withKeepAlive(List, { cacheId: "list", scroll: true }); 14 15 function App() { 16 return ( 17 <KeepAliveProvider> 18 <BrowserRouter> 19 <ul> 20 <li> 21 <Link to="/">首页</Link> 22 </li> 23 <li> 24 <Link to="/list">列表页</Link> 25 </li> 26 <li> 27 <Link to="/detail">详情页A</Link> 28 </li> 29 </ul> 30 <Switch> 31 <Route path="/" component={Home} exact></Route> 32 <Route path="/list" component={KeepAliveList}></Route> 33 <Route path="/detail" component={Detail}></Route> 34 </Switch> 35 </BrowserRouter> 36 </KeepAliveProvider> 37 ); 38 } 39 40 export default App;
效果:
假设有个需求,从首页到列表页,需要清空搜索条件,重新请求数据,即回到首页,需要清除列表页的缓存。
上面的KeepAliveProvider.js中,暴露了一个useCacheContext()的hook,该hook返回了缓存组件相关数据和方法,这里可以用于清除缓存:
Home.js
1 import React, { useEffect } from "react"; 2 import { DESTROY } from "./keepalive-cpn/cache-types"; 3 import { useCacheContext } from "./keepalive-cpn/KeepAliveProvider"; 4 5 const Home = () => { 6 const { cacheStates, dispatch } = useCacheContext(); 7 8 const clearCache = () => { 9 if (cacheStates && dispatch) { 10 for (let key in cacheStates) { 11 if (key === "list") { 12 dispatch({ type: DESTROY, payload: { cacheId: key } }); 13 } 14 } 15 } 16 }; 17 useEffect(() => { 18 clearCache(); 19 // eslint-disable-next-line 20 }, []); 21 return ( 22 <div> 23 <div>首页</div> 24 </div> 25 ); 26 }; 27 28 export default Home;
效果:
至此,react简易版的keepalive组件已经完成啦~
脚踏实地行,海阔天空飞