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

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

image.png

阅读本文需要 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 时,渲染 ,当 showfalse 时, 会被销毁。

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 时, 的根节点

被隐藏,而不会被销毁。


还有一个关键的知识点,在不使用 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 中的实现



效果展示

image.png

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



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({}) 来做页面数据保存的节点,是因为我们的上下文不被重新渲染的话 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 ,发布成功之后大家就可以在项目中安装使用它了。

image.png

这个包是在 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>
    )
}
复制代码


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


源码归档




目录
相关文章
|
11天前
|
前端开发 数据安全/隐私保护
.自定义认证前端页面
.自定义认证前端页面
6 1
.自定义认证前端页面
|
4月前
|
开发框架 前端开发 JavaScript
循序渐进VUE+Element 前端应用开发(5)--- 表格列表页面的查询,列表展示和字段转义处理
循序渐进VUE+Element 前端应用开发(5)--- 表格列表页面的查询,列表展示和字段转义处理
|
6天前
|
前端开发 安全 JavaScript
在阿里云快速启动Appsmith搭建前端页面
本文介绍了Appsmith的基本信息,并通过阿里云计算巢完成了Appsmith的快速部署,使用者不需要自己下载代码,不需要自己安装复杂的依赖,不需要了解底层技术,只需要在控制台图形界面点击几下鼠标就可以快速部署并启动Appsmith,非技术同学也能轻松搞定。
|
4月前
|
前端开发 JavaScript Java
基于Vue+ElementUI框架实现学生管理系统前端页面设计
基于Vue+ElementUI框架实现学生管理系统前端页面设计
|
29天前
|
前端开发 JavaScript
回顾前端页面发送ajax请求方式
回顾前端页面发送ajax请求方式
36 18
|
2月前
|
前端开发 JavaScript API
前端JS读取文件内容并展示到页面上
前端JavaScript使用FileReader API读取文件内容,支持文本类型文件。在文件读取成功后,可以通过onload事件处理函数获取文件内容,然后展示到页面上。
79 2
前端JS读取文件内容并展示到页面上
|
1月前
|
前端开发 数据安全/隐私保护
angular前端基本页面验证
angular前端基本页面验证
29 1
|
1月前
|
前端开发
搭建个人博客--1、前端页面
搭建个人博客--1、前端页面
20 1
|
3月前
|
开发框架 前端开发 Java
【前端学java】SpringBootWeb极速入门-实现一个简单的web页面01
【8月更文挑战第12天】SpringBootWeb极速入门-实现一个简单的web页面01
68 3
【前端学java】SpringBootWeb极速入门-实现一个简单的web页面01
|
2月前
|
前端开发 JavaScript
前端基础(一)_前端页面构成
本文介绍了前端页面的基本构成,包括HTML(负责页面的结构和语义)、CSS(负责页面的样式和表现)和JavaScript(负责页面的行为和动态效果)。文章通过示例代码展示了如何使用这三种技术来创建一个简单的网页,并解释了HTML文档的结构和语法。
34 0