React中实现keepalive组件缓存效果

背景:由于react官方并没有提供缓存组件相关的api(类似vue中的keepalive),在某些场景,会使得页面交互性变的很差,比如在有搜索条件的表格页面,点击某一条数据跳转到详情页面,再返回表格页面,会重新请求数据,搜索条件也将清空,用户得重新输入搜索条件,再次请求数据,大大降低办公效率,如图:

React中实现keepalive组件缓存效果

目标:封装keepalive缓存组件,实现组件的缓存,并暴露相关方法,可以手动清除缓存。

版本:React 17,react-router-dom 5

结构

React中实现keepalive组件缓存效果

代码:

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;

效果

React中实现keepalive组件缓存效果

假设有个需求,从首页到列表页,需要清空搜索条件,重新请求数据,即回到首页,需要清除列表页的缓存。

上面的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组件缓存效果

至此,react简易版的keepalive组件已经完成啦~

 

脚踏实地行,海阔天空飞

 

发表评论

相关文章