likes
comments
collection
share

挑战21天手写前端框架 day9 50行代码实现页面状态保持 keepalive

作者站长头像
站长
· 阅读数 19

阅读本文需要 15 分钟,编写本文耗时 2 小时

什么是页面状态保持

所谓的页面状态保持就是从 A 页面到 B 页面,再回到 A 页面的时候,希望 A 页面能够保持在离开前的状态。常见的业务场景就是从列表页进去详情页面,再回到列表页面时,期望页面的搜索查询状态,页面的滚动状态能够保持在原来的位置上。这个需求特别是在做移动端的时候,可以说是必备的需求,因为在 pc 端页面上我们还可以通过在 url 上保存分页信息来让返回体验保留在一个可以接受的状态上。但是在移动端,我们的长列表页面都是通过滚动加载更多的方式请求的数据,如果没有状态保持,那可能用户滚动了十几页然后进入详情页回来,又要从第一页开始重新滚动,这样的交互体验是极差的。

还有一个比较常见的场景就是移动端填写长表单,其中的某一个值需要跳转一个新页面获取,回来还要将数据还原。如果没有状态保持,我们都是将临时的表单数据放到全局的数据流中,等到返回页面的时候,再从全局的数据流中加载数据。特别是有些数据复杂度很高的时候,这个开发非常的耗费精力,因为你要时刻关注用户什么时间点会离开。

可能我们的交付项目多是在移动端,所以状态保持几乎都是每一个项目的强需求。这在 vue 中很早就提供了一个配置实现,而在 React 中缺迟迟没有官方提供。所以社区上也有不少的朋友在鼓捣这个方案。

像我的好朋友CJY,就特别擅长和热衷于此。在 react-route@6 之前,我们提供了两种不同的方案在 umi 生态中使用。用的人还不少,反响也挺好。但是实现上还是比较复杂的,没有一点基础的朋友,要掌握这些是比较困难的。

但是在 react-route@6 发布之后,我们有了一种更加简单优雅的方式实现,这就是我今天要介绍的内容。

实现原理

先简单的说一下实现原理吧。先来看两段简单的代码吧。他们的作用都是一样的,都是控制组件显隐。

import React, { useState } from 'react';

const CountItem = () => {
    const [count, setCount] = useState(0);
    return <div onClick={() => setCount(count + 1)}>{count}</div>
}
const Hello = () => {
    const [show, setShow] = useState(true);
    return <>
        {show && <CountItem />}
    </>
}

上面的逻辑是当 showtrue 时,渲染 <CountItem />,当 showfalse 时,<CountItem /> 会被销毁。

import React, { useState } from 'react';

const CountItem = () => {
    const [count, setCount] = useState(0);
    return <div onClick={() => setCount(count + 1)}>{count}</div>
}
const Hello = () => {
    const [show, setShow] = useState(true);
    return <>
        {<div hidden={!show}><CountItem /></div>}
    </>
}

上面的逻辑最大的差别就是 当 showfalse 时,<CountItem /> 的根节点 <div> 被隐藏,而不会被销毁。

还有一个关键的知识点,在不使用 react-route@6 的情况下,它是必须的。那就是 key 在 React 中的作用。

const Hello = () => {
    const [show, setShow] = useState(true);
    return <>
        {<div key="保持key不变"><CountItem /></div>}
    </>
}

只要在返回的 dom 中,保持 key 不变,就不会触发 React 的重绘。这也是为什么在 list map 的时候,一定要指定一个 key 的关键原因。这个内容不是今天的重点,这里就不深入展开,如果你的项目中用不上 react-route@6 但你有需要状态保持,那你可以参考 alita@2 中的实现

效果展示

挑战21天手写前端框架 day9 50行代码实现页面状态保持 keepalive

从上面的动图可以看出,我们在首页和用户页面,页面状态都得到了保持,在用户页面点击清除缓存之后,用户页面的状态被重置到初始状态。

50行的源码实现

上面提到的效果,完全符合我们的预期需求,并且实现也非常的简单和优雅。主要是使用了上下文来保存页面数据,通过调用 react-route@6useOutlet 来取到真实的 demo。

import React, { useRef, createContext, useContext } from 'react';
import { useOutlet, useLocation, matchPath } from 'react-router-dom'
import type { FC } from 'react';

export const KeepAliveContext = createContext<KeepAliveLayoutProps>({ keepalive: [], keepElements: {} });

const isKeepPath = (aliveList: any[], path: string) => {
    let isKeep = false;
    aliveList.map(item => {
        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 useKeepOutlets() {
    const location = useLocation();
    const element = useOutlet();
    const { keepElements, keepalive } = useContext<any>(KeepAliveContext);
    const isKeep = isKeepPath(keepalive, location.pathname);
    if (isKeep) {
        keepElements.current[location.pathname] = element;
    }
    return <>
        {
            Object.entries(keepElements.current).map(([pathname, element]: any) => (
                <div key={pathname} style={{ height: '100%', width: '100%', position: 'relative', overflow: 'hidden auto' }} className="rumtime-keep-alive-layout" hidden={!matchPath(location.pathname, pathname)}>
                    {element}
                </div>
            ))
        }
        <div hidden={isKeep} style={{ height: '100%', width: '100%', position: 'relative', overflow: 'hidden auto' }} className="rumtime-keep-alive-layout-no">
            {!isKeep && element}
        </div>
    </>
}

interface KeepAliveLayoutProps {
    keepalive: any[];
    keepElements?: any;
    dropByCacheKey?: (path: string) => void;
}

const KeepAliveLayout: FC<KeepAliveLayoutProps> = (props) => {
    const { keepalive, ...other } = props;
    const keepElements = React.useRef<any>({})
    function dropByCacheKey(path: string) {
        keepElements.current[path] = null;
    }
    return (
        <KeepAliveContext.Provider value={{ keepalive, keepElements, dropByCacheKey }} {...other} />
    )
}

export default KeepAliveLayout;

配置 keepalive

配置 keepalive 支持字符串和正则,通过它来判断,当前页面是否需要状态保持,因为如果整个项目的页面都保持状态的话,对性能是很大的消耗。方法 isKeepPath 的实现也很简单。

useKeepOutlets

使用 useKeepOutlets 取到需要渲染的组件,它包含了当前页面的组件,和缓存中的组件。通过判断当前页面是否是需要保持的页面来对页面 DOM 做一个 hidden 显隐开关。

指的注意的是所有被指定状态保持的页面在首次渲染之后,都会被挂载在页面 DOM 树上,仅仅是使用 !matchPath(location.pathname, pathname) 控制显隐。

而没有被指定状态保持的页面,则是使用 {!isKeep && element} 控制,走 React 组件正常的生命周期。

React.useRef({}) 与 {}

const keepElements = React.useRef<any>({})

使用 React.useRef<any>({}) 来做页面数据保存的节点,是因为我们的上下文不被重新渲染的话 keepElements 就不会被重置。用它替代了 key 的特性。

发布 @malitajs 组织下的包

新建 packages/keepalive/src/index.tsx,将上面的代码放进去。

修改包名 packages/keepalive/package.json@malitajs/keepalive

修改 main 入口 "main": "lib/index.js", 和 types 文件路径 "types": "lib.index.d.ts",

添加发包配置 "publishConfig": { "access": "public" },

增加构建脚本 "scripts": { "build": "tsc", },

增加 tsconfig packages/keepalive/tsconfig.json,值得注意的是 jsxdeclaration 配置,"declaration": true 才会输出 .d.ts 文件。

{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "jsx": "react-jsx",
    "declaration": true,
    "outDir": "./lib",
    "rootDir": "./src"
  },
  "include": ["src","client"]
}

构建后执行发包 npm publish ,发布成功之后大家就可以在项目中安装使用它了。

挑战21天手写前端框架 day9 50行代码实现页面状态保持 keepalive

这个包是在 alita@3 中经过 20 几个项目验证过生产环境的,比较靠谱的,可以酌情考虑是否用到生产上。

授之以鱼 @malita/keepalive

将今天的内容封装成独立的 React 中的状态保持组件

安装

yarn add @malita/keepalive

使用

import KeepAliveLayout, { useKeepOutlets, KeepAliveContext } from '@malita/keepalive';
import { useLocation } from 'react-router-dom';
import React, { useState, useContext } from 'react';

// 使用 useKeepOutlets 取到当前渲染的页面内容,可能是缓存内容
const Layout = () => {
    const element = useKeepOutlets();
    return (
        {element}
    )
}

// 使用 KeepAliveLayout 包裹上下文
const App = () => {
    return (
        <KeepAliveLayout keepalive={[/./]}>
            // App
        </KeepAliveLayout>
    );
}

// 使用 useContext 取到 dropByCacheKey 清除缓存
const Home = () => {
    const { dropByCacheKey } = useContext<any>(KeepAliveContext);
    const { pathname } = useLocation();
    return (
       <button onClick={() => dropByCacheKey(pathname)}> Click Me! Clear Cache!</button>
    )
}

感谢阅读,今天的内容实现事比较简单的,但是确实我们踩过好多坑之后才走出的比较舒服的一条路,如果你觉得对你有说帮助,别忘了给我点赞哦。感谢感谢。

源码归档

转载自:https://juejin.cn/post/7088705614603354142
评论
请登录