基于 React Router 实现 keepalive

2,100 阅读4分钟

我想做过vue的同学都知道keepalive是什么吧,就是把那些加载过的组件存到缓存里面,即使页面切换了,组件已经销毁,但是在缓存里面的组件依旧在,等下次用户用到这个组件的时候,直接会读取缓存里面处理好的dom就好了,不用再进行编译解析。

那么在react里也会有这样的场景,比如我在手机上要填写一个信息,但是我不知道该写什么,就跳出去查找了一下,回来发现表单上填写的东西全部没有了,你会不会头顶飘过一万个草泥马?

还有翻看大型购物网站的时候,你进入一个商品的详情,看了一会,又退出来,发现居然依旧停留在刚才进入的地方,难道它没有销毁?不对呀,它应该是销毁了又重新加载的页面呀?它是如何实现的?

其实在生活中,我们有很多场景都会用到缓存组件,它最大的优点就是提升用户的体验感。

那就动动小手自己实现一下看看:

1.初始化项目

npx create-vite

创建一个vite项目,之后就是创造一个简单的页面出来了,一般情况下,我们都是缓存路由组件的。在app。jsx里面写一个简单的路由和页面。

2.测试组件

写三个组件,很简单如下:

image.png

具体代码如下:

import { useState } from 'react';
import { Link, useLocation, RouterProvider, createBrowserRouter, useOutlet } from 'react-router-dom';

const Layout = () => {
  const { pathname } = useLocation();
  const element = useOutlet();

  console.log(pathname, 999);
  //用outLet指定组件的children,类似useOutlet

  return <div>
    <div>现在的路由是:{pathname}</div>
    <div>我是首页</div>
    {element}
  </div>;
};

const DemoA = () => {
  const [num, setNum] = useState(0);

  const handleClick = () => {
    setNum(() => num + 1);
  };

  return <div style={{ width: "500px", height: "200px", marginLeft: "100px" }}>
    <p>我是demoA页面,你不要看错了</p>
    <p>我现在是:{num}</p>
    <p>
      <button onClick={handleClick}>点你</button><br />
      <Link to="/demob">去DemoB页面</Link><br />
      <Link to="/democ">去DemoC页面</Link><br />
    </p>
  </div>;
};

const DemoB = () => {
  const [num, setNum] = useState(0);

  const handleClick = () => {
    setNum(() => num + 2);
  };

  return <div style={{ width: "500px", height: "200px", marginLeft: "100px" }}>
    <p>我是demoB页面, 我的数据需要被记住</p>
    <p>我现在是:{num}</p>
    <p>
      <button onClick={handleClick}>点你</button>
      <Link to="/">我要去首页</Link>
    </p>
  </div>;
};

const DemoC = () => {
  return <div style={{ width: "500px", height: "200px", marginLeft: "100px" }}>
    <p>我是demoC页面,我需要干点啥呢</p>
    <p>我是C页面,我想干点啥呢?</p>
    <Link to='/'>去首面</Link>
  </div>;
};

const routes = [{
  path: '/',
  element: <Layout></Layout>,
  children: [{
    path: '/',
    element: <DemoA></DemoA>
  }, {
    path: '/demob',
    element: <DemoB></DemoB>
  }, {
    path: '/democ',
    element: <DemoC></DemoC>
  }]
}];

export const router = createBrowserRouter(routes);

function App() {
  return (
    <RouterProvider router={router} />
  );
}

export default App;

首先我创建routes数组,创建router对象

image.png

再次我用RouterProvider,他是个context对象,负责把路由派发下去

image.png

这样react-router会根据具体的路由,从配置文件里面拿到具体的组件,最主要的组件就是这个Layout,他是个根组件,一般会将网页的 Nav、公告,用户等放在里面,供整个网站共用。

Layout上使用 useOutlet 拿到了路由对应的组件,然后在首页 Layout 上显示出来,然后,

image.png

这个组件很简单不再赘述,我们看看KeepAlive组件

3.KeepAlive组件

KeepAlive组件组件的核心思想就是:我要在App上创建一个context,然后用useLocation拿到当前路由,然后用useOutlet拿到路由对应的组件。然后把他们一对一的存到这个context里面。但是你想一下总不能把所有的组件都存起来吧,所以存哪些组件是不是需要一个参数去确定,所以这个组件的第一个入参就是要缓存的路由地址,它应该是个数组,一个应用不可能只缓存一个页面的。所以基础代码如下:

image.png

使用:

image.png

数据已经派发下去了,怎么渲染呢?就要看Layout了,他才是专门负责渲染组件的根组件,在这里它会用 useOutlet 获取到子组件,然后去渲染。

那我们是不是也应该在这里去处理当前的组件?继续在KeepAlive.jsx里面写一个hooks出来解决这个事情。

image.png

其实它就做了两件事:用uselocation和useOutlet拿到当前的路由和组件,看当前路由是不是在keepPaths里面,如果在,我们就缓存,如果不在,就不管了。2.遍历keepElements数组,渲染所有在缓存里面的组件。其实在缓存里面的组件它一直都在页面上,并没有被销毁,它只是被隐藏了而已!

使用:

image.png

测试:

多次切换路由,页面数据依旧是上一次的。 image.png

/*eslint-disable */
import React, { createContext, useContext } from 'react';
import { useOutlet, useLocation, matchPath } from 'react-router-dom';

const keepElements = {};

//创建一个全局变量来存储组件,在content里面存放路径,组件,及其删除缓存的方法
export const KeepAliveContext = createContext({
  keepPaths: [],
  keepElements,
  dropByPath(path) {
    keepElements[path] = null;
  }
});

console.log(KeepAliveContext, 99999);

//keepPaths和当前路径做比较,keepPaths可以是字符串也可以是正则表达式,所以通过if else判断即可,
const isKeepPath = (keepPaths, path) => {
  let isKeep = false;
  for (let i = 0; i < keepPaths.length; i++) {
    let item = keepPaths[i];
    if (item === path) {
      isKeep = true;
    }
    if (item instanceof RegExp && item.test(path)) {
      isKeep = true;
    }
    if (typeof item === 'string' && item.toLowerCase() === path) {
      isKeep = true;
    }
  }
  return isKeep;
};

export function useKeepOutlet() {
  const location = useLocation();
  const element = useOutlet();

  const { keepElements, keepPaths } = useContext(KeepAliveContext);
  const isKeep = isKeepPath(keepPaths, location.pathname);

  if (isKeep) {
    keepElements[location.pathname] = element;
  }

  return <>
    {!isKeep && element}
    {
      Object.entries(keepElements).map(([pathname, element]) => (
        <div
          key={pathname}
          style={{ height: '100%', width: '100%', position: 'relative', overflow: 'hidden auto' }}
          className="keep-alive-page"
          hidden={!matchPath(location.pathname, pathname)}
        >
          {element}
        </div>
      ))
    }

  </>;
}

const KeepAliveLayout = (props) => {
  const { keepPaths, ...other } = props;
  const { keepElements, dropByPath } = useContext(KeepAliveContext);

  return (
    <KeepAliveContext.Provider value={{ keepPaths, keepElements, dropByPath }} {...other} />
  );
};

export default KeepAliveLayout;