背景:由於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元件已經完成啦~
腳踏實地行,海闊天空飛