「react缓存页面」从需求到开源(我是怎么样让产品小姐姐另眼相看的)

一 一切根源都从产品小姐姐无厘头需求开始

最近在开发业务项目的时候,产品小姐姐忽然来到我身边,而后就对着电脑一顿操做,具体场景大体是这样的。前端

场景一:vue

如上图所示,当在数万级别的数据中,选择一条,点击查看,跳转到当前数据的详情页,当点击按钮返回返回来,或者是浏览器前进后退等其余操做,返回到列表页的时候。要记录当前列表的位置。也就是要还原点击查看查看前的页面。可是当点击tab菜单按钮的时候,要清除页面信息。node

场景二:react

如上图所示,当咱们编辑内容的时候,一些数据可能从其余页面得到,因此要求,不管切换路由,切换页面,当前页面的编辑信息均不能被置空,只有点击肯定重置,表单才内容置空。git

场景三: 场景一 + 场景二 是更复杂的缓存页面信息场景。github

二 梳理需求

接这个需求的时候,咋眼一看,what ,好像是 vue 中的 keepalive + vue router功能,可是,咱们几个项目技术栈是react ,react , react! react 中没有对应的 keepalive内置 api,后来上GitHub上搜索相关项目,感受有不少不符合业务需求的状况。还有一些潜在的风险。 瞬间慌了~~~。心里有一种万只神兽奔腾的感受。算法

在漂亮产品小姐姐面前,怎么能说不,那不显得研发能力差,强行装了一波说很简单,只能硬着头皮接下来了。产品小姐姐临走前还说还鬼魅的笑了笑,说能够把几个项目的部分页面都加上这种效果。vue-router

1 解决方案

1 数据状态缓存到公共管理可行性

这个需求首先让我想到的是用redux或者是mobx来把页面的状态缓存起来,而后切换页面的时候,把这些数据缓存进去,再次切换回来的时候,将数据取出来,这样就一个问题,即使能缓存state层,可是若是一些表单组件是非受控组件,是没法缓存下来的,还有一些dom状态是缓存不了的,好比手动添加的一些样式等。还有就是实际状况比较复杂,有富文本组件,你是没法直接获取绑定的state的。npm

第二个缘由就是有好几个项目,并且页面比较多,若是都创建数据管理,那么工做量会很是的大。 因此数据状态缓存的可行性不高,即使能够实现,也须要大量的复制粘贴,这不是咱们的追求。json

2 react-keepalive-router诞生

因此咱们只能选择本身开发一个项目,而后把它开源,并应用在公司项目中来。既然选择缓存页面,那么为何不在react-router中的 Route组件和Switch组件中作文章呢,咱们须要对RouteSwitch 组件作一些功能性的拓展,正好笔者以前本身研究过react-router源码,并写了一篇(这一次完全弄懂react-router路由原理)[juejin.cn/post/688629…] ,感兴趣的同窗能够三连一波,由于项目是在router路由层面,因此给它起了一个名字react-keepalive-router。接下来就要对整个项目作一个系统的设计。

三设计阶段

1 了解react-fiber

为何咱们的项目要提到react-fiber呢,这里我先说一下,react-fiberReact Fiber 是从 v16 版本开始对 Stack Reconciler 进行的重写,是 v16 版本的核心算法实现。react在初始化构建过程当中,会产生一个由child指向子fiber,sibling指向兄弟fiber,return指向父fiber三个指针构建的fiber树结构,里面保存着dom信息,update信息,props信息等,咱们核心思想就是,在切换页面的时候,组件销毁,可是做为渲染调度的react fiber保存keepalive状态。只要fiber存活,就能获取到dom元素,数据层state等信息。

2 基于 react-router-dom 和 react 16.8

首先咱们须要对react-router库中的 Route组件和Switch组件做出改造,能够经过路由层面实现缓存路由功能。由于在设计之初,我就想着将用不一样的状态管理keepalive状态,这样的好处是,后续能够给缓存路由组件,增长一些额外的声明周期,好比说vueactivateddeactivated同样。由于设计思想是状态管理,项目依赖中不想引入redux等第三方库,因此这里选了react-hooksuseReducer恰到好处。这就是react基础库 16.8+的缘由之一。另一个缘由就是hooks中有useMemo这样防止渲染穿透的api,有助于调节路由组件的更新次数。

工做流程分析

受到react-router-cache-route开源项目的启发,我在设计整个流程的时候,采起了交换dom的方式。

初始化 : 总体设计思路第一次切入缓存页面的时候,会自动生成一个容器组件,缓存Route会把组件,交给容器组件来挂载,而后容器组件生成fiber,render以后生成对应的dom树,将dom树交给Route组件(也就是咱们的正常的页面)。

切换页面: 切换页面的时候,路由组件是确定卸载的,这时候须要将咱们的dom还给容器组件,而后容器组件进入冻结状态。

再次切换到缓存页面:再次进入路由页面的时候,首先从容器中,发现有该页面的缓存,那么将容器解封状态,而后将dom树,还给当前路由页面。完成keepalive状态。

缓存销毁:: 项目支持销毁缓存功能,调用销毁方法,会卸载当前缓存容器,进一步销毁fiberdom ,完成整个销毁功能。

工做流程图

工做原理图

设计的优点在哪里?

设计优点:

1 由于内部运用了 useReducer 状态管理,管理缓存状态,能够更灵活,操纵缓存路由组件,采用react hooks全新api,渲染节流,手动解除缓存,增长了缓存的状态周期,监听函数等。

2 这套缓存页面的思想,不只仅能够用在路由页面级别,后期能够迁移的component组件级别上来。也是后续维护和开发的方向。

四 使用简介 + 快速上手

咱们开始设计项目的用法,api,已经应用场景。经过上述工做原理,讲述了 keepliveRouteSwitchkeepliveRoute 在整个缓存过程当中的做用,

下载

由于咱们是把项目上传到了npm方便其余项目用,因此能够直接从 npm 上下载。

npm install react-keepalive-router --save
# or
yarn add react-keepalive-router
复制代码

使用

1 基本用法

KeepaliveRouterSwitch

KeepaliveRouterSwitch能够理解为常规的Switch,也能够理解为 keepaliveScope,咱们确保整个缓存做用域,只有一个 KeepaliveRouterSwitch 就能够了

常规用法

import { BrowserRouter as Router, Route, Redirect ,useHistory  } from 'react-router-dom'
import { KeepaliveRouterSwitch ,KeepaliveRoute ,addKeeperListener } from 'react-keepalive-router'

const index = () => {
  useEffect(()=>{
    /* 增长缓存监听器 */
    addKeeperListener((history,cacheKey)=>{
      if(history)console.log('当前激活状态缓存组件:'+ cacheKey )
    })
  },[])
  return <div > <div > <Router > <Meuns/> <KeepaliveRouterSwitch> <Route path={'/index'} component={Index} ></Route> <Route path={'/list'} component={List} ></Route> { /* 咱们将详情页加入缓存 */ } <KeepaliveRoute path={'/detail'} component={ Detail } ></KeepaliveRoute> <Redirect from='/*' to='/index' /> </KeepaliveRouterSwitch> </Router> </div> </div>
}
复制代码

这里应该注意⚠️的是对于复杂的路由结构。或者KeepaliveRouterSwitch 包裹的子组件不是Route ,咱们要给 KeepaliveRouterSwitch 增长特有的属性 withoutRoute 就能够了。以下例子🌰🌰🌰:

例子一

<KeepaliveRouterSwitch withoutRoute >
  <div> <Route path="/a" component={ComponentA} /> <Route path="/b" component={ComponentB} /> <KeepaliveRoute path={'/detail'} component={ Detail } ></KeepaliveRoute> </div>
</KeepaliveRouterSwitch>

复制代码

例子二

或者咱们可使用 renderRoutesapi配合 KeepliveRouterSwitch 使用 。

import {renderRoutes} from "react-router-config"
<KeepliveRouterSwitch withoutRoute  >{ renderRoutes(routes) }</KeepliveRouterSwitch> 
复制代码

KeepaliveRoute

KeepaliveRoute 基本使用和 Route没有任何区别。

在当前版本中⚠️⚠️⚠️若是 KeepaliveRoute 若是没有被 KeepaliveRouterSwitch包裹就会失去缓存做用。

效果

2 其余功能

1 缓存组件激活监听器

若是咱们但愿对当前激活的组件,有一些额外的操做,咱们能够添加监听器,用来监听缓存组件的激活状态。

addKeeperListener((history,cacheKey)=>{
  if(history)console.log('当前激活状态缓存组件:'+ cacheKey )
})
复制代码

第一个参数未history对象,第二个参数为当前缓存路由的惟一标识cacheKey

2 清除缓存

缓存的组件,或是被route包裹的组件,会在props增长额外的方法cacheDispatch用来清除缓存。

若是props没有cacheDispatch方法,能够经过

import React from 'react'
import { useCacheDispatch } from 'react-keepalive-router'

function index(){
    const cacheDispatch = useCacheDispatch()
    return <div>我是首页 <button onClick={()=> cacheDispatch({ type:'reset' }) } >清除缓存</button> </div>
}

export default index
复制代码

1 清除全部缓存

cacheDispatch({ type:'reset' }) 
复制代码

2 清除单个缓存

cacheDispatch({ type:'reset',payload:'cacheId' }) 
复制代码

3 清除多个缓存

cacheDispatch({ type:'reset',payload:['cacheId1''cacheId2'] }) 
复制代码

五 验证阶段

因为这里使用公司项目不是很合适,我用了一个本身的项目作demo:

接下来就是验证阶段首先咱们看一下产品小姐姐第一个需求:

第二个需求:

完美实现产品需求。

六 打包阶段 + 发布npm阶段

rollup打包

接下来就是 rollup 打包阶段,rollup打包阶段。项目结构是这样的。

rollup.config.js是整个rollup的配置文件,而后咱们经过 rollup 打包后的文件存在 lib文件夹下。

rollup.config.js 内容以下

import resolve from 'rollup-plugin-node-resolve'
import babel from 'rollup-plugin-babel'
import { uglify } from 'rollup-plugin-uglify'

export default [
    {
      input: 'src/index.js',
      output: {
        name: 'keepaliveRouter',
        file: 'lib/index.js',
        format: 'cjs',
        sourcemap: true
      },
      external: [
        'react',
        'react-router-dom',
        'invariant'
      ],
      plugins: [
        resolve(),
        babel({
          exclude: 'node_modules/**'
        })
      ]
    },
    /* 压缩` */
    {
      input: 'src/index.js',
      output: {
        name: 'keepaliveRouter',
        file: 'lib/index.min.js',
        format: 'umd'
      },
      external: [
        'react',
        'react-router-dom',
        'invariant'
      ],
      plugins: [
        resolve(),
        babel({
          exclude: 'node_modules/**'
        }),
        uglify()
      ]
    }
  ]
复制代码

发布npm

对于发布npm

第一步:须要在npm注册帐号。https://www.npmjs.com/signup

第二步: 登录 npm login

第三步:建立 package.json

{
  "name": "react-keepalive-router", /* 名称 */
  "version": "1.1.0",  /* 版本号 */
  "description": "基于`react 16.8+` ,`react-router 4+` 开发的`react`缓存组件,能够用于缓存页面组件,相似`vue`的`keepalive`包裹`vue-router`的效果功能。", /* 描述 */
  "main": "index.js", /* 入口文件 */
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "rollup --config"
  },
  "keywords": [  /* npm 关键词 */
    "keep alive",
    "react",
    "react router",
    "react keep alive route",
    "react hooks"
  ],
  "homepage": "https://github.com/GoodLuckAlien/react-keepalive-router", /* 指向 github */
  "peerDependencies": { /* npm 项目依赖 */
    "react": ">=16.8",
    "react-router-dom": ">=4",
    "invariant": ">=2"
  },
  "author": "alien",
  "license": "ISC",
  "devDependencies": {  /* 开发环境下依赖 */
    "@babel/core": "^7.12.3",
    "@babel/preset-react": "^7.12.5",
    "@babel/preset-env": "^7.12.1",
    "@babel/plugin-proposal-class-properties": "^7.12.1",
    "rollup": "^2.33.3",
    "rollup-plugin-node-resolve": "^5.2.0",
    "rollup-plugin-babel": "^4.4.0",
    "rollup-plugin-uglify": "^6.0.4"
  },
  "dependencies": { /* 生产环境依赖 */
    "invariant": "^2.2.4"
  }
}
复制代码

第四步:万事俱备以后,用 npm publish 发布。

第五步:升级版本,升级版本很简单,须要咱们在package.json 升级版本号,而后从新 npm publish 就能够了。

废弃版本号

若是咱们想废弃某个版本 , 执行命令 npm deprecate <pkg>[@<version>] <message>

废弃包

若是咱们想废弃包 npm unpublish <pkg> --force

.npmignore

.npmignore里面声明的文件和文件价名称,不会被上传到 npm , 个人项目除了 README.md ,package.jsonlib 下打包的文件以外,大部分文件是开发时候或者编译阶段用到的,不须要上传到npm,因此须要在 .npmignore 这么写

docs
node_modules
src
md
.babelrc
.gitignore
.npmignore
.prettierrc
rollup.config.js
yarn.lock
复制代码

七 总结

项目地址

react-keepalive-router

从需求到开源的流程跑通以后,会有很大的成就感,刚开始独立开发的项目确定颇有不少bug,不怕有bug,要有一颗敢于修复bug并把项目维护下去的决心。

送人玫瑰,手留余香,阅读的朋友能够给笔者**点赞,关注一波。**陆续更新前端文章。

感受有用的朋友能够关注笔者公众号 前端Sharing 持续更新前端文章。

喜欢笔者的能够给笔者投票,海报以下🙏🙏🙏感谢