目录
前言
react 路由的使用依靠 react-router-dom,今天进行一个系统的使用示范进行记录学习。
内容包括:基本路由结构、顶级路由的提取、实现keepAlive的方法。
注意:以下代码都运行于 react 项目中,学习前请自行创建简单 react 项目。
基本路由结构
最外层由 HashRouter 包裹,还有另一个可选组件 BrowserRouter,最浅显的区别是路由中是否携带 # 隔开路由,前者携带后者不携带。
然后 Routes 组件中间开始包裹 Route 组件编写每个路由,Route 中 path 属性为路由名,element 为路由组件。
Link 用于使用 to 属性进行路由间跳转。
import React from 'react'; import ReactDOM from 'react-dom/client'; import {HashRouter, Routes, Route, Link} from "react-router-dom"; const Home = () => { return ( <> <p>当前的路由:/</p> <Link to='/users'>跳转用户页</Link> </>); }; const Users = () => { return ( <> <p>当前的路由:/users</p> <Link to='/'>go Home</Link> </>); }; const App = () => { return ( <HashRouter> <Routes> <Route path="/" element={<Home/>}/> <Route path="/users" element={<Users/>}/> </Routes> </HashRouter> ); } const root = ReactDOM.createRoot(document.getElementById('root')); root.render(React.createElement(App));
至此我们完成了最简单的路由页面:
路由公共部分提取
有时候我们需要对路由公共的部分提取,比如一个每个路由页面都存在的导航,每个页面都会现实的提示等。
比如我们简单页面显示的一行当前的路由,我们可以提取到公共的部分,我喜欢叫它顶级路由。
很简单,我们只要用一个 Route 创建一个顶级路由(这里的Top)包裹所有的路由页面。
然后这里用到了两个函数:
useLocation,因为我们的公共部分需要用到当前路由名,useLocation 可以得到对应的 pathname,因为当前路由这段话被提到了公共部分,因此各个路由组件中的这句都可以删除。
useOutlet,可以得到当前包裹的路由组件,怎么理解呢?大概就是可以理解为进入每个路由之前都会先进入 Top ,比如进入 Home 路由时,先进入 Top,然后 Top 中可以使用 useOutlet 得到真正要进入的路由组件,也就是 Home 组件,进入 User 路由时同理。
import React, {useState} from 'react'; import ReactDOM from 'react-dom/client'; import {HashRouter, Routes, Route, Link, useOutlet, useLocation} from "react-router-dom"; const Top = () => { const {pathname} = useLocation(); const child = useOutlet(); return ( <div> <p>当前的路由:{pathname}</p> <div> {child} </div> </div> ) } const Home = () => { return ( <> <Link to='/users'>跳转用户页</Link> </>); }; const Users = () => { return ( <> <Link to='/'>go Home</Link> </>); }; const App = () => { return ( <HashRouter> <Routes> <Route path="/" element={<Top/>}> <Route path="/" element={<Home/>}/> <Route path="/users" element={<Users/>}/> </Route> </Routes> </HashRouter> ); } const root = ReactDOM.createRoot(document.getElementById('root')); root.render(React.createElement(App));
实现keepalive
首先我们要知道一个问题,那就是路由切换时,路由的组件会被卸载,因此状态不会保留。
我们将 Users 组件换成以下内容测试:
const Users = () => { const [num, setNum] = useState(0) return ( <> <div>{num}</div> <div> <button onClick={() => { setNum(num + 1) }}>数字加一 </button> </div> <Link to='/'>go Home</Link> </>); };
当我们切换之后,数字又变为0了。
在某些场景下我们不希望如此。
比如填写表单时,我们不希望自己不小心退出了表单页面就需要重新填写已填写过的内容。
那这时候我们就需要 keepalive,实现对路由状态的保存。
怎么实现呢,我们可以做一个假设,我们在路由切换时只是隐藏了路由组件而不是销毁它。
那我们的第一步就是要让所有路由组件显示在一起。
第一步 —— 保存所有路由组件
注意:以下内容可能需要掌握 Context 和 Ref 的用法,不了解得先去 React 官网学习
我们可以使用 Context 作为路由的全局状态,然后状态中维护一个 Ref 对象 allRouteElements ,用它来保存所有路由组件,所有路由组件中都可以访问到 allRouteElements。
使用前不要忘了引入createContext, useContext, useRef。
import React, {useState, createContext, useContext, useRef} from 'react'; const KeepAliveContext = createContext() const App = () => { const allRouteElements = useRef({}) return ( // 使用KeepAliveContext.Provider包裹,value提供一个全局状态 // 状态中有一个allRouteElements <KeepAliveContext.Provider value={{allRouteElements}}> <HashRouter> <Routes> <Route path="/" element={<Top/>}> <Route path="/" element={<Home/>}/> <Route path="/users" element={<Users/>}/> </Route> </Routes> </HashRouter> </KeepAliveContext.Provider> ); }
然后我们可以在顶级路由 Top 中进行各路由组件的保存,然后再遍历 allRouteElements.current 将所有路由组件渲染在一起。
const Top = () => { const {pathname} = useLocation(); const child = useOutlet(); // 使用useContext得到全局状态中的allRouteElements const {allRouteElements} = useContext(KeepAliveContext) // 每次访问到新路由组件时,都会加进allRouteElements allRouteElements.current[pathname] = child return ( <div> <p>当前的路由:{pathname}</p> {Object.entries(allRouteElements.current).map(([curPathname, curChild]) => { return <div key={curPathname} >{curChild}</div> })} </div> ) }
我们的第二步就是,让路由组件根据是否在自己的路由下进行隐藏操作。
第二步 —— 控制路由组件显隐
我们通过 hidden={curPathname !== pathname} ,如果遍历到的路由名与当前真实路由不一致,则隐藏。
const Top = () => { const {pathname} = useLocation(); const child = useOutlet(); const {allRouteElements} = useContext(KeepAliveContext) allRouteElements.current[pathname] = child return ( <div> <p>当前的路由:{pathname}</p> {Object.entries(allRouteElements.current).map(([curPathname, curChild]) => { // 遍历到的路由名与当前真实路由不一致,则隐藏 return <div key={curPathname} hidden={curPathname !== pathname}>{curChild}</div> })} </div> ) }
至此我们实现了路由的 keepalive,我们测试一下。
第三步 —— 控制需要keepalive的路由
我们可能不是所有页面都希望实现 keepalive ,因此我们可以通过一些参数来控制,比如我们可以手动传入需要 keepalive 的路由。
我们在 Context 状态中多加一个全局属性,用数组的形式保存我们需要 keepalive 的路由。
const App = () => { const allRouteElements = useRef({}) const needAlive = ['/users'] return ( <KeepAliveContext.Provider value={{allRouteElements, needAlive}}> <HashRouter> <Routes> <Route path="/" element={<Top/>}> <Route path="/" element={<Home/>}/> <Route path="/users" element={<Users/>}/> </Route> </Routes> </HashRouter> </KeepAliveContext.Provider> ); }
然后我们再去顶级路由做判断:
const Top = () => { const {pathname} = useLocation(); const child = useOutlet(); const {allRouteElements, needAlive} = useContext(KeepAliveContext) // 通过 needAlive 数组来判断当前路由是否需要 keepAlive let isNeed = false needAlive.map(cur => { if (cur === pathname) { isNeed = true } }) if (isNeed) { allRouteElements.current[pathname] = child } return ( <div> <p>当前的路由:{pathname}</p> {Object.entries(allRouteElements.current).map(([curPathname, curChild]) => { return <div key={curPathname} hidden={curPathname !== pathname}>{curChild}</div> })} {/* 不需要 keepAlive 的不存在于 allRouteElements.current,需要额外渲染与销毁 */} {!isNeed ? <div>{child}</div> : null} </div> ) }
此时我们的首页没有 keepAlive 而 用户页有,因此我们在首页也加上一个状态变更的计数器,进行对比:
const Home = () => { const [num, setNum] = useState(0) return ( <> <div>{num}</div> <div> <button onClick={() => { setNum(num - 1) }}>数字减一 </button> </div> <Link to='/users'>跳转用户页</Link> </>); };
可以看到我们首页的数字变化之后切换路由就又变为0了,而有用户页加的数字并没有变化。
第四步 —— 清除缓存
即便我们加入了 keepAlive ,有时候我们有需求需要手动清除页面的状态,也可以当作清除缓存。
怎么做呢?我们现在实现 keepAlive 的路由组件全部保存在 allRouteElements 中,渲染时只是不停在渲染 allRouteElements 这个装了路由组件的类数组而已,我们只需要添加一个可以删除 allRouteElements 对应路由组件的方法不就可以了吗。
const Users = () => { const [num, setNum] = useState(0) // 获取该函数 const {delCache} = useContext(KeepAliveContext) const {pathname} = useLocation() return ( <> <div>{num}</div> <div> <button onClick={() => { setNum(num + 1) }}>数字加一 </button> </div> <div> {/* 可以通过事件控制删除缓存 */} <button onClick={() => { delCache(pathname) }}>清除缓存 </button> </div> <Link to='/'>go Home</Link> </>); }; const App = () => { const allRouteElements = useRef({}) const needAlive = ['/users'] // 添加一个该函数 const delCache = (pathname) => { allRouteElements.current[pathname] = null } return ( <KeepAliveContext.Provider value={{allRouteElements, needAlive, delCache}}> <HashRouter> <Routes> <Route path="/" element={<Top/>}> <Route path="/" element={<Home/>}/> <Route path="/users" element={<Users/>}/> </Route> </Routes> </HashRouter> </KeepAliveContext.Provider> ); }
按步骤理解:
allRouteElements中本来保存了 Users ,离开 Users 路由时只是将 Users组件隐藏了。
你现在使用 delCache 删除 allRouteElements 中的 Users 组件,离开 Users 路由时就不再是隐藏而是销毁它了。
然后再次进入 Users 时,顶级路由 Top 又重新将 Users 组件保存到 allRouteElements,但是原来的状态已经丢失,就实现了清除缓存的要求。
完整代码,出现问题可以核对
import React, {useState, createContext, useContext, useRef} from 'react'; import ReactDOM from 'react-dom/client'; import {HashRouter, Routes, Route, Link, useOutlet, useLocation} from "react-router-dom"; const KeepAliveContext = createContext() const Top = () => { const {pathname} = useLocation(); const child = useOutlet(); const {allRouteElements, needAlive} = useContext(KeepAliveContext) // 通过needAlive数组来判断当前路由是否需要keepAlive let isNeed = false needAlive.map(cur => { if (cur === pathname) { isNeed = true } }) if (isNeed) { allRouteElements.current[pathname] = child } return ( <div> <p>当前的路由:{pathname}</p> {Object.entries(allRouteElements.current).map(([curPathname, curChild]) => { return <div key={curPathname} hidden={curPathname !== pathname}>{curChild}</div> })} {/*不需要keepAlive的不存在于allRouteElements.current,需要额外渲染与销毁*/} {!isNeed ? <div>{child}</div> : null} </div> ) } const Home = () => { const [num, setNum] = useState(0) return ( <> <div>{num}</div> <div> <button onClick={() => { setNum(num - 1) }}>数字减一 </button> </div> <Link to='/users'>跳转用户页</Link> </>); }; const Users = () => { const [num, setNum] = useState(0) // 获取该函数 const {delCache} = useContext(KeepAliveContext) const {pathname} = useLocation() return ( <> <div>{num}</div> <div> <button onClick={() => { setNum(num + 1) }}>数字加一 </button> </div> <div> {/* 可以通过事件控制删除缓存 */} <button onClick={() => { delCache(pathname) }}>清除缓存 </button> </div> <Link to='/'>go Home</Link> </>); }; const App = () => { const allRouteElements = useRef({}) const needAlive = ['/users'] // 添加一个该函数 const delCache = (pathname) => { allRouteElements.current[pathname] = null } return ( <KeepAliveContext.Provider value={{allRouteElements, needAlive, delCache}}> <HashRouter> <Routes> <Route path="/" element={<Top/>}> <Route path="/" element={<Home/>}/> <Route path="/users" element={<Users/>}/> </Route> </Routes> </HashRouter> </KeepAliveContext.Provider> ); } const root = ReactDOM.createRoot(document.getElementById('root')); root.render(React.createElement(App));
第五步 —— 使用依赖包
我将以上内容封装为npm包,可以直接使用,地址:@chanjs/keepalive。
尾言
如果觉得文章对你有帮助的话,欢迎点赞收藏哦,有什么错误或者意见建议也可以留言,感谢~