├── .gitignore
├── md
├── 111.gif
├── form.gif
├── scroll.gif
└── lifecycle.gif
├── example
├── demo
│ ├── src
│ │ ├── page
│ │ │ ├── lifecycle
│ │ │ │ ├── style.scss
│ │ │ │ └── index.tsx
│ │ │ ├── home
│ │ │ │ ├── index.scss
│ │ │ │ └── index.tsx
│ │ │ ├── input
│ │ │ │ └── index.tsx
│ │ │ └── goodsList
│ │ │ │ ├── index.tsx
│ │ │ │ └── index.scss
│ │ ├── assets
│ │ │ ├── images
│ │ │ │ └── alien.jpg
│ │ │ └── styles
│ │ │ │ └── common.scss
│ │ ├── index.js
│ │ ├── app.scss
│ │ ├── model
│ │ │ └── index.ts
│ │ ├── asyncRouter.js
│ │ ├── app.tsx
│ │ └── mock.js
│ ├── rux.config.js
│ ├── tsconfig.json
│ ├── .babelrc
│ ├── index.html
│ ├── package.json
│ └── .eslintrc.js
└── renderRoutesDemo.js
├── src
├── core
│ ├── cacheContext.js
│ └── keeper.js
├── hoc
│ ├── extendsSelf.js
│ └── lifecycle.js
├── index.js
├── utils
│ ├── const.js
│ └── index.js
└── components
│ ├── keepliveRouteSwitch.js
│ ├── keepCache.js
│ └── keepliveRoute.js
├── .npmignore
├── .babelrc
├── index.js
├── rollup.config.js
├── package.json
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | lib
2 | node_modules
3 |
--------------------------------------------------------------------------------
/md/111.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoodLuckAlien/react-keepalive-router/HEAD/md/111.gif
--------------------------------------------------------------------------------
/example/demo/src/page/lifecycle/style.scss:
--------------------------------------------------------------------------------
1 | .box{
2 | width: 500px;
3 | padding: 30px;
4 | }
--------------------------------------------------------------------------------
/md/form.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoodLuckAlien/react-keepalive-router/HEAD/md/form.gif
--------------------------------------------------------------------------------
/md/scroll.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoodLuckAlien/react-keepalive-router/HEAD/md/scroll.gif
--------------------------------------------------------------------------------
/md/lifecycle.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoodLuckAlien/react-keepalive-router/HEAD/md/lifecycle.gif
--------------------------------------------------------------------------------
/example/demo/rux.config.js:
--------------------------------------------------------------------------------
1 | module.exports ={
2 | dev:{
3 | },
4 | pro:{
5 | },
6 | base:{
7 | }
8 | }
--------------------------------------------------------------------------------
/example/demo/src/assets/images/alien.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoodLuckAlien/react-keepalive-router/HEAD/example/demo/src/assets/images/alien.jpg
--------------------------------------------------------------------------------
/src/core/cacheContext.js:
--------------------------------------------------------------------------------
1 | import {createContext} from 'react'
2 |
3 | const cacheRouterContext = createContext()
4 |
5 | export default cacheRouterContext
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | docs
2 | node_modules
3 | src
4 | md
5 | .babelrc
6 | .gitignore
7 | .npmignore
8 | .prettierrc
9 | rollup.config.js
10 | yarn.lock
11 | example
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "presets": [
4 | "@babel/preset-env",
5 | "@babel/preset-react"
6 | ],
7 | "plugins":[
8 | "@babel/plugin-proposal-class-properties"
9 | ]
10 | }
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | if (process.env.NODE_ENV === 'production') {
4 | module.exports = require('./lib/index.min.js');
5 | } else {
6 | module.exports = require('./lib/index.js');
7 | }
--------------------------------------------------------------------------------
/example/demo/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './app'
4 | import rux from 'ruxjs'
5 | import './assets/styles/common.scss'
6 |
7 |
8 | ReactDOM.render(
9 | rux.create({}, () => < App / > ),
10 | document.getElementById('app')
11 | )
12 |
13 |
--------------------------------------------------------------------------------
/src/hoc/extendsSelf.js:
--------------------------------------------------------------------------------
1 | import { isFuntion } from '../utils/index'
2 |
3 | export default function ExtendsSelfHoc(Componet,getCurrent){
4 | return class Hoc extends Componet{
5 | constructor(props){
6 | super(props)
7 | isFuntion(getCurrent) && getCurrent(this)
8 | }
9 | }
10 | }
11 |
12 |
--------------------------------------------------------------------------------
/example/demo/src/page/home/index.scss:
--------------------------------------------------------------------------------
1 | .box{
2 | margin-top: 50px;
3 | }
4 |
5 | .item{
6 | height: 50px;
7 | line-height: 50px;
8 | border-radius: 20px;
9 | width: 200px;
10 | font-size: 15px;
11 | background-color: orange;
12 | color: #fff;
13 | font-weight: bold;
14 | text-align: center;
15 | margin-bottom: 20px;
16 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 |
2 | import KeepaliveRouterSwitch from './components/keepliveRouteSwitch'
3 | import KeepaliveRoute from './components/keepliveRoute'
4 | import {GetCacheContext, useCacheDispatch} from './components/keepCache'
5 |
6 | import cacheRouterContext from './core/cacheContext'
7 | import {addKeeperListener} from './core/keeper'
8 |
9 | import keepaliveLifeCycle from './hoc/lifecycle'
10 |
11 | export {KeepaliveRoute, KeepaliveRouterSwitch, cacheRouterContext, GetCacheContext, useCacheDispatch, addKeeperListener,keepaliveLifeCycle}
12 |
--------------------------------------------------------------------------------
/example/demo/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strictNullChecks": true,
4 | "moduleResolution": "node",
5 | "allowSyntheticDefaultImports": true,
6 | "experimentalDecorators": true,
7 | "jsx": "react",
8 | "noUnusedParameters": true,
9 | "noUnusedLocals": true,
10 | "target": "es6",
11 | "module": "esnext",
12 | "lib": [
13 | "dom",
14 | "es7"
15 | ]
16 | },
17 | "exclude": [
18 | "node_modules",
19 | "lib",
20 | "es"
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/src/utils/const.js:
--------------------------------------------------------------------------------
1 | export const KEEPLIVE_ROUTE_SWITCH = 'KEEPLIVE_ROUTE_SWITCH'
2 |
3 | export const KEEPLIVE_ROUTE_COMPONENT = 'KEEPLIVE_ROUTE_COMPONENT'
4 |
5 | export const ACITON_CREATED = 'created' /* 缓存创建 */
6 | export const ACTION_ACTIVE = 'active' /* 缓存激活 */
7 | export const ACTION_ACTIVED = 'actived' /* 激活完成 */
8 | export const ACITON_UNACTIVE = 'unActive' /* 缓存休眠 */
9 | export const ACTION_UNACTIVED = 'unActived' /* 休眠完成 */
10 | export const ACTION_RESERT = 'reset' /* 设置摧毁状态 */
11 | export const ACTION_DESTORYED = 'destoryed' /* 摧毁缓存 */
12 | export const ACTION_CLEAR = 'clear' /* 清除缓存 */
--------------------------------------------------------------------------------
/example/demo/src/assets/styles/common.scss:
--------------------------------------------------------------------------------
1 | #app{
2 | height: 100%;
3 | width: 100%;
4 | section{
5 | height: 100%;
6 | width: 100%;
7 | }
8 | }
9 | #components-layout-demo-custom-trigger .trigger {
10 | font-size: 18px;
11 | line-height: 64px;
12 | padding: 0 24px;
13 | cursor: pointer;
14 | transition: color 0.3s;
15 | }
16 |
17 | #components-layout-demo-custom-trigger .trigger:hover {
18 | color: #1890ff;
19 | }
20 |
21 | #components-layout-demo-custom-trigger .logo {
22 | height: 32px;
23 | background: rgba(255, 255, 255, 0.2);
24 | margin: 16px;
25 | }
26 |
27 | .site-layout .site-layout-background {
28 | background: #fff;
29 | }
--------------------------------------------------------------------------------
/example/demo/src/app.scss:
--------------------------------------------------------------------------------
1 | .page{
2 | position: fixed;
3 | left:0;
4 | right: 0;
5 | top:0;
6 | bottom: 0;
7 | background-color: #ffff;
8 |
9 | }
10 | .content{
11 | position: absolute;
12 | left:50%;
13 | top: 50%;
14 | transform: translate(-50%,-50%);
15 | }
16 | .title{
17 | text-align: center;
18 | font-size: 10px;
19 | color: #ccc;
20 | }
21 | .btns{
22 | padding-left: 30px;
23 | button{
24 | margin-right: 30px;
25 | }
26 | }
27 | .routerLink{
28 | margin-left: 15px;
29 | color: #0366d6;
30 | font-size: 14px;
31 | }
32 | .theStyle{
33 | height: 50px;
34 | background-color: #fff;
35 | position: fixed;
36 | left:0;
37 | right:0;
38 | top:0;
39 | z-index: 10000;
40 | }
--------------------------------------------------------------------------------
/example/demo/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "modules":"commonjs",
7 | "targets": {
8 | "chrome": "67"
9 | },
10 | "useBuiltIns": "usage",
11 | "corejs": 2
12 | }
13 | ],
14 | "@babel/preset-react"
15 | ],
16 | "plugins": [
17 | "@babel/plugin-proposal-class-properties",
18 | [
19 | "@babel/plugin-transform-runtime",
20 | {
21 | "absoluteRuntime": false,
22 | "helpers": true,
23 | "regenerator": true,
24 | "useESModules": false
25 | }
26 | ],
27 | "@babel/plugin-syntax-class-properties",
28 | ["import", {
29 | "libraryName":
30 | "antd",
31 | "libraryDirectory": "es",
32 | "style": true
33 | }]
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/example/demo/src/page/home/index.tsx:
--------------------------------------------------------------------------------
1 | //todo
2 | import React from 'react'
3 | import './index.scss'
4 | import { useCacheDispatch } from 'react-keepalive-router'
5 |
6 | class Index extends React.Component{
7 |
8 | handerClick=(payload)=>{
9 | const dispatch = useCacheDispatch()
10 | dispatch({ type:'reset' , payload })
11 | }
12 |
13 | render(){
14 | console.log(this.props)
15 | return
16 |
this.handerClick('/list')}
18 | >清除 生命周期
19 |
this.handerClick('/list2')}
21 | >清除 缓存列表
22 |
this.handerClick('/detail')}
24 | >清除 缓存表单
25 |
26 | }
27 | }
28 |
29 |
30 |
31 |
32 |
33 | export default Index
--------------------------------------------------------------------------------
/example/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/example/demo/src/page/input/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 |
4 |
5 |
6 | class Index extends React.Component{
7 | constructor(prop){
8 | super(prop)
9 | this.state = {
10 | list: [ { id:1 , name: 'xixi' } ,{ id:2 , name: 'haha' },{ id:3 , name: 'heihei' } ],
11 | number:1
12 | }
13 | }
14 | render(){
15 | const { number }:any = this.state
16 | return
17 |
18 |
19 |
20 |
21 | {new Array(number).fill(0).map(()=> {'🌟'})}
22 |
23 |
24 |
25 |
26 | }
27 | }
28 |
29 |
30 |
31 | export default Index
--------------------------------------------------------------------------------
/example/demo/src/page/lifecycle/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { keepaliveLifeCycle } from 'react-keepalive-router'
4 | console.log(keepaliveLifeCycle)
5 | import './style.scss'
6 |
7 | @keepaliveLifeCycle
8 | class index extends React.Component{
9 |
10 | state={
11 | activedNumber:0,
12 | unActivedNumber:0
13 | }
14 | actived(){
15 | this.setState({
16 | activedNumber:this.state.activedNumber + 1
17 | })
18 | }
19 | unActived(){
20 | this.setState({
21 | unActivedNumber:this.state.unActivedNumber + 1
22 | })
23 | }
24 | render(){
25 | const { activedNumber , unActivedNumber } = this.state
26 | return
27 |
页面 actived 次数: {activedNumber}
28 |
页面 unActived 次数:{unActivedNumber}
29 |
30 |
31 | }
32 | }
33 |
34 | export default index
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 数据类型校验
3 | * @param type 待校验类型
4 | */
5 | export function isType(type) {
6 | return Object.prototype.toString.call(type).slice(8, -1)
7 | }
8 |
9 | /* 字符串类型 */
10 | export function isString(type) {
11 | return isType(type) === 'String'
12 | }
13 |
14 | /* 数字类型 */
15 | export function isNumber(type) {
16 | return isType(type) === 'Number'
17 | }
18 |
19 | /* 函数类型 */
20 | export function isFuntion(type) {
21 | return isType(type) === 'Function'
22 | }
23 |
24 | /* 数组类型 */
25 | export function isArray(type) {
26 | return isType(type) === 'Array'
27 | }
28 |
29 | /* 对象类型 */
30 | export function isObject(type) {
31 | return isType(type) === 'Object'
32 | }
33 |
34 | /* 访问属性 */
35 | export function saveTryAttr(obj, attr) {
36 | const aAttr = isString(attr) ? attr.split('.') : null
37 | return aAttr
38 | }
39 |
40 | export function funCur (callback){
41 | return function (...arg){
42 | const cb = callback()
43 | cb(...arg)
44 | }
45 | }
--------------------------------------------------------------------------------
/example/demo/src/model/index.ts:
--------------------------------------------------------------------------------
1 |
2 | function getData(){
3 | return new Promise((resolve)=>setTimeout(()=>{ resolve(1) },100)).then(res=>res)
4 | }
5 |
6 | export default {
7 | state: {
8 | number: 1
9 | },
10 | reducer: {
11 | numberAdd(state) {
12 | return {
13 | ...state,
14 | number: state.number + 1
15 | }
16 | },
17 | numberDel(state) {
18 | return {
19 | ...state,
20 | number: state.number - 1
21 | }
22 | },
23 | numberReset(state,{ payload }){
24 | return {
25 | ...state,
26 | number: payload
27 | }
28 | }
29 | },
30 | effect: {
31 | asyncnumberAdd(dispatch) {
32 | setTimeout(() => {
33 | dispatch({
34 | type: 'numberAdd'
35 | })
36 | }, 3000)
37 | },
38 | async resetNumber(dispatch){
39 | const res = await getData()
40 | dispatch({
41 | type:'numberReset',
42 | payload:res
43 | })
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import resolve from 'rollup-plugin-node-resolve'
2 | import babel from 'rollup-plugin-babel'
3 | import { uglify } from 'rollup-plugin-uglify'
4 |
5 | export default [
6 | {
7 | input: 'src/index.js',
8 | output: {
9 | name: 'keepaliveRouter',
10 | file: 'lib/index.js',
11 | format: 'cjs',
12 | sourcemap: true
13 | },
14 | external: [
15 | 'react',
16 | 'react-router-dom',
17 | 'invariant',
18 | 'hoist-non-react-statics'
19 | ],
20 | plugins: [
21 | resolve(),
22 | babel({
23 | exclude: 'node_modules/**'
24 | })
25 | ]
26 | },
27 | /* 压缩` */
28 | {
29 | input: 'src/index.js',
30 | output: {
31 | name: 'keepaliveRouter',
32 | file: 'lib/index.min.js',
33 | format: 'umd'
34 | },
35 | external: [
36 | 'react',
37 | 'react-router-dom',
38 | 'invariant',
39 | 'hoist-non-react-statics'
40 | ],
41 | plugins: [
42 | resolve(),
43 | babel({
44 | exclude: 'node_modules/**'
45 | }),
46 | uglify()
47 | ]
48 | }
49 | ]
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@didi/react-keepalive-router",
3 | "version": "1.1.5",
4 | "description": "基于`react 16.8+` ,`react-router 4+` 开发的`react`缓存组件,可以用于缓存页面组件,类似`vue`的`keepalive`包裹`vue-router`的效果功能。",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "build": "rollup --config"
9 | },
10 | "keywords": [
11 | "keep alive",
12 | "react",
13 | "react router",
14 | "react keep alive route",
15 | "react hooks"
16 | ],
17 | "homepage": "https://github.com/AlienZhaolin/react-keepalive-router",
18 | "peerDependencies": {
19 | "react": ">=16.8",
20 | "react-router-dom": ">=4",
21 | "invariant": ">=2"
22 | },
23 | "author": "rachelxuxin",
24 | "maintainers": ["rachelxuxin"],
25 | "license": "ISC",
26 | "devDependencies": {
27 | "@babel/core": "^7.12.3",
28 | "@babel/preset-react": "^7.12.5",
29 | "@babel/preset-env": "^7.12.1",
30 | "@babel/plugin-proposal-class-properties": "^7.12.1",
31 | "rollup": "1.32.1",
32 | "rollup-plugin-node-resolve": "^5.2.0",
33 | "rollup-plugin-babel": "^4.4.0",
34 | "rollup-plugin-uglify": "6.0.4"
35 | },
36 | "dependencies": {
37 | "hoist-non-react-statics": "^3.3.2",
38 | "invariant": "^2.2.4"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/example/demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-keepalive-router-demo",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "rux start",
8 | "build": "rux build"
9 | },
10 | "author": "zhaolin",
11 | "license": "ISC",
12 | "dependencies": {
13 | "@babel/plugin-syntax-class-properties": "^7.12.1",
14 | "@types/react": "^16.9.25",
15 | "antd": "^4.9.3",
16 | "react": "^16.8.1",
17 | "react-dom": "^16.8.1",
18 | "react-keepalive-router": "^1.1.2",
19 | "react-router": "^5.1.2",
20 | "react-router-dom": "^5.2.0",
21 | "react-tiny-virtual-list": "^2.2.0",
22 | "ruxjs": "0.0.2"
23 | },
24 | "devDependencies": {
25 | "@babel/core": "^7.5.4",
26 | "@babel/plugin-transform-runtime": "^7.5.0",
27 | "@babel/preset-env": "^7.5.4",
28 | "@babel/preset-react": "^7.0.0",
29 | "@types/webpack-env": "^1.15.1",
30 | "babel-eslint": "^10.1.0",
31 | "babel-loader": "^8.0.6",
32 | "babel-plugin-import": "^1.13.0",
33 | "css-hot-loader": "^1.4.4",
34 | "css-loader": "^3.0.0",
35 | "eslint": "^6.8.0",
36 | "eslint-plugin-react": "^7.19.0",
37 | "file-loader": "^6.0.0",
38 | "happypack": "^5.0.1",
39 | "node-sass": "^4.13.1",
40 | "postcss-loader": "^3.0.0",
41 | "rux-react-webpack-plugin": "^1.0.3",
42 | "sass-loader": "^7.3.1",
43 | "style-loader": "^0.23.1",
44 | "ts-loader": "^4.5.0",
45 | "typescript": "^3.9.3",
46 | "url-loader": "^4.0.0"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/example/renderRoutesDemo.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './app.scss'
3 | import { BrowserRouter as Router ,useHistory } from 'react-router-dom'
4 |
5 | import { renderRoutes } from 'react-router-config'
6 | import { KeepaliveRouterSwitch ,KeepaliveRoute} from 'react-keepalive-router'
7 |
8 | import Detail from './page/input'
9 | import List from './page/lifecycle'
10 | import TheIndex from '../src/page/home/index'
11 | import List2 from './page/goodsList'
12 | import List3 from './page/list2/index'
13 |
14 | const menusList = [
15 | {
16 | name: '首页',
17 | path: '/home',
18 | component:TheIndex
19 | },
20 | {
21 | name: '生命周期demo',
22 | path: '/list',
23 | component:List
24 | },
25 | {
26 | name: '列表demo',
27 | path: '/list2',
28 | component:List2
29 | },
30 | {
31 | name: '表单demo',
32 | path: '/detail',
33 | component:()=>
36 | },
37 | {
38 | name:'列表demo2',
39 | path:'/list3',
40 | component:List3
41 | }
42 |
43 | ]
44 |
45 |
46 | function Meuns(){
47 | const history = useHistory()
48 | return
49 | {menusList.map(item=> { history.push(item.path) }}
52 | >{item.name})}
53 |
54 | }
55 |
56 | const index = () => {
57 |
58 | return
59 |
60 |
61 |
62 |
63 | {renderRoutes(menusList)}
64 |
65 |
66 |
67 |
68 | }
69 |
70 |
71 |
72 | export default index
--------------------------------------------------------------------------------
/src/hoc/lifecycle.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import hoistNonReactStatic from 'hoist-non-react-statics'
3 |
4 | import ExtendsSelfHoc from './extendsSelf'
5 | import {
6 | lifeCycles
7 | } from '../core/keeper'
8 | import {
9 | funCur,
10 | isFuntion
11 | } from '../utils/index'
12 |
13 |
14 | function keepaliveLifeCycle(Component) {
15 | const isClassComponent = Component.prototype.setState && Component.prototype.isReactComponent
16 | if (!isClassComponent) return Component
17 |
18 | let callback = null
19 | const SelfComponent = ExtendsSelfHoc(Component,funCur(() => callback))
20 |
21 | class WrapComponent extends React.Component {
22 | constructor(props) {
23 | super(props)
24 | this.cur = null
25 | callback = (cur) => (this.cur=cur)
26 | }
27 |
28 | componentDidMount() {
29 | const {
30 | cacheId
31 | } = this.props
32 | cacheId && (lifeCycles[cacheId] = this.handerLifeCycle)
33 | }
34 | componentWillUnmount() {
35 | const {
36 | cacheId
37 | } = this.props
38 | delete lifeCycles[cacheId]
39 | }
40 | handerLifeCycle = type => {
41 | if (!this.cur) return
42 | const lifeCycleFunc = this.cur[type]
43 | isFuntion(lifeCycleFunc) && lifeCycleFunc.call(this.cur)
44 | }
45 | render = () => {
46 | const {
47 | forwardedRef,
48 | ...otherProp
49 | } = this.props
50 | return
53 | }
54 | }
55 |
56 | const forWardRefComponent = React.forwardRef((props, ref) => (
57 |
60 | ))
61 |
62 | return hoistNonReactStatic(forWardRefComponent, SelfComponent)
63 | }
64 |
65 | export default keepaliveLifeCycle
--------------------------------------------------------------------------------
/example/demo/src/asyncRouter.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const routerObserveQueue = []
4 |
5 | /* 懒加载路由卫士钩子 */
6 | export const RouterHooks = {
7 | /* 路由组件加载之前 */
8 | beforeRouterComponentLoad: function (callback) {
9 | routerObserveQueue.push({
10 | type: 'before',
11 | callback
12 | })
13 | },
14 | /* 路由组件加载之后 */
15 | afterRouterComponentDidLoaded(callback) {
16 | routerObserveQueue.push({
17 | type: 'after',
18 | callback
19 | })
20 | }
21 | }
22 |
23 | /* 路由懒加载HOC */
24 | export default function AsyncRouter(loadRouter) {
25 | return class Content extends React.Component {
26 | constructor(props) {
27 | super(props)
28 | this.dispatchRouterQueue('before')
29 | }
30 | state = {
31 | Component: null
32 | }
33 | dispatchRouterQueue(type) {
34 | const {
35 | history
36 | } = this.props
37 | routerObserveQueue.forEach(item => {
38 | if (item.type === type) item.callback(history)
39 | })
40 | }
41 | componentDidMount() {
42 | if (this.state.Component) return
43 | loadRouter()
44 | .then(module => module.default)
45 | .then(Component => this.setState({
46 | Component
47 | },
48 | () => {
49 | this.dispatchRouterQueue('after')
50 | }))
51 | }
52 | render() {
53 | const {
54 | Component
55 | } = this.state
56 | return Component ? < Component {
57 | ...this.props
58 | }
59 | /> : null
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/example/demo/src/app.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './app.scss'
3 | import { BrowserRouter as Router, Route ,useHistory , Redirect } from 'react-router-dom'
4 | // import { KeepaliveRouterSwitch ,KeepaliveRoute} from './keep-src/index'
5 |
6 | import { KeepaliveRouterSwitch ,KeepaliveRoute} from 'react-keepalive-router'
7 | import Detail from './page/input'
8 | import List from './page/lifecycle'
9 | import TheIndex from '../src/page/home/index'
10 | import List2 from './page/goodsList'
11 |
12 | const menusList = [
13 | {
14 | name: '首页',
15 | path: '/home'
16 | },
17 | {
18 | name: '生命周期demo',
19 | path: '/list'
20 | },
21 | {
22 | name: '缓存列表demo',
23 | path: '/list2'
24 | },
25 | {
26 | name: '表单demo',
27 | path: '/detail'
28 | },
29 | ]
30 |
31 |
32 | function Meuns(){
33 | const history = useHistory()
34 | return
35 | { menusList.map(item=> { history.push(item.path) } } key={item.path} >{ item.name }) }
36 |
37 | }
38 |
39 | const RouteWithSubRoutes = (item)=>
40 |
41 | const index = () => {
42 | return
43 |
44 |
45 |
46 |
47 | {
48 | [{ path:'/detail' ,component:Detail }].map(item=> )
49 | }
50 |
51 |
52 |
53 | {/* 路由不匹配,重定向到/index */}
54 |
55 |
56 |
57 |
58 |
59 | }
60 |
61 | export default index
--------------------------------------------------------------------------------
/example/demo/src/page/goodsList/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { listData } from '../../mock'
3 |
4 | import './index.scss'
5 |
6 |
7 | class Index extends React.Component{
8 | num = 0
9 | state = {
10 | list:[],
11 | renderList: [] /* 渲染列表 */
12 | }
13 |
14 | componentDidMount() {
15 | this.setState({
16 | list : listData.data
17 | })
18 | }
19 | /* 处理滚动效果 */
20 | render() {
21 | const { list } = this.state
22 | return
23 |
26 | {/* 显然区 */}
27 |
28 | {
29 | list.map((item:any, index) => (
30 |
33 |

36 |
37 |
38 | {item.giftName}
39 |
40 |
41 |
42 |
43 |
44 | ¥ {item.price}
45 |
46 |
47 |
48 |
![]()
49 |
50 |
51 | ))
52 | }
53 |
54 |
55 |
56 |
57 | }
58 | }
59 |
60 |
61 | export default Index
--------------------------------------------------------------------------------
/example/demo/src/mock.js:
--------------------------------------------------------------------------------
1 | export const listData = {
2 | "code" : 200,
3 | "message" : "success",
4 | "data" : [ {
5 | "skuId" : "1",
6 | "giftName" : "约斯夫家庭校园多功能创可贴卡通女少女可爱超弹防水透气弹力小面积开放性创伤创口贴 超弹防水透气型 100贴/盒",
7 | "giftImage" : "https://img14.360buyimg.com/n1/jfs/t1/117043/23/16493/438028/5f50a682E96819e0d/a3678e5c4fb5a3cf.jpg",
8 | "price" : "19.90",
9 | }, {
10 | "skuId" : "2",
11 | "giftName" : "【MaincareBio】医用外科口罩一次性无菌三层透气成人挂耳式防细菌病毒飞沫防护医用口罩 儿童医用外科口罩50只【10只/包*5包】",
12 | "giftImage" : "https://img14.360buyimg.com/n1/jfs/t1/133614/39/16312/128620/5fb3a1b8E02fec0c6/0b7d82a132932f35.jpg",
13 | "price" : "39.90",
14 | }, {
15 | "skuId" : "3",
16 | "giftName" : "乐樊一次性医用外科口罩医生专用成人通用三层医疗口罩透气单片防护 医用外科口罩100只蓝色【非独立包装/2包】",
17 | "giftImage" : "https://img14.360buyimg.com/n1/jfs/t1/151889/33/15018/129441/6008e066Ee813ef0d/1f1a8218fa30a05f.jpg",
18 | "price" : "31.90",
19 | }, {
20 | "skuId" : "4",
21 | "giftName" : "俏东方 一次性医用口罩白色 轻薄透气 三层防护含熔喷过滤成人男女适用冬季防护面罩 50只医用口罩白色整包(工厂特惠)",
22 | "giftImage" : "https://img14.360buyimg.com/n1/jfs/t1/164271/11/7365/212791/6032be25E162107e3/df794675c5095edf.jpg",
23 | "price" : "9.90",
24 | }, {
25 | "skuId" : "5",
26 | "giftName" : "【7仓隔日达】咔贝爱(KABEIAI)一次性医用防护口罩防尘防雾霾防颗粒物 三层防护透气医用口罩 医用口罩50只(1包)",
27 | "giftImage" : "https://img14.360buyimg.com/n1/jfs/t1/156216/4/9112/168310/601e5d2aE4ad9ee3b/65a25f358d136a20.jpg",
28 | "price" : "19.90",
29 | }, {
30 | "skuId" : "6",
31 | "giftName" : "康诺嘉口罩KN95一次性口罩5层防护日常防雾霾灰尘通用型男女口罩 50只",
32 | "giftImage" : "https://img14.360buyimg.com/n1/jfs/t1/161195/25/1659/131874/5ff7d289E597c8999/700182369a7bed58.jpg",
33 | "price" : "26.90",
34 | }, {
35 | "skuId" : "7",
36 | "giftName" : "拓家中药泡脚包艾草红花草益母草老姜当归泡脚包缓解疲劳泡脚粉 30小包/袋 随机发货 3袋(90包)",
37 | "giftImage" : "https://img14.360buyimg.com/n1/jfs/t1/168773/15/1476/92527/5ff6d15fE46e9b990/98acc32416799ab9.jpg",
38 | "price" : "29.90",
39 | }, {
40 | "skuId" : "8",
41 | "giftName" : "多美忆 2021新年装饰品窗贴春节装饰窗花牛年福字贴纸贴画家用室内商场店铺场景布置 春节窗贴(新年快乐)",
42 | "giftImage" : "https://img14.360buyimg.com/n1/jfs/t1/165342/17/1238/201103/5ff58107E7b42ee87/aab515ec5cb209b3.jpg",
43 | "price" : "7.90",
44 | }, {
45 | "skuId" : "9",
46 | "giftName" : "南极人【5双装】保暖袜子男加厚款秋冬季毛圈袜纯色长袜中筒毛巾袜棉袜男 加厚毛圈袜5色5双",
47 | "giftImage" : "https://img14.360buyimg.com/n1/jfs/t1/100815/34/3889/179336/5de36360E458679a3/af803962c81a6939.jpg",
48 | "price" : "12.90",
49 | } ],
50 | "totalCount" : 338,
51 | "pageCount" : 34,
52 | "currentPage" : 1
53 | }
--------------------------------------------------------------------------------
/src/components/keepliveRouteSwitch.js:
--------------------------------------------------------------------------------
1 |
2 | import React, {useMemo, useEffect} from 'react'
3 | import {Switch, matchPath, useHistory, __RouterContext, withRouter} from 'react-router-dom'
4 | import invariant from 'invariant'
5 |
6 | import Cache , { beforeSwitchDestory } from './keepCache'
7 | import {KEEPLIVE_ROUTE_SWITCH, KEEPLIVE_ROUTE_COMPONENT } from '../utils/const'
8 | import {isFuntion, isObject} from '../utils/index'
9 |
10 |
11 | const {isValidElement, cloneElement} = React
12 | const {forEach} = React.Children
13 |
14 | const isKeepliveRouter = child => child.type.__componentType === KEEPLIVE_ROUTE_COMPONENT
15 |
16 | class KeepliveRouterSwitch extends Switch {
17 |
18 | constructor(props, ...arg) {
19 | super(props, ...arg)
20 | const {ishasRouterSwitch, children, cacheDispatch } = props
21 | const __render = this.render
22 |
23 | this.render = () => {
24 | if (ishasRouterSwitch) {
25 | let element, match, history, location
26 | if (this.context.router) {
27 | history = this.context.router.history
28 | location = history.location
29 | } else {
30 | history = this.props.history
31 | location = this.props.location
32 | }
33 | forEach(children, child => {
34 | if (match == null && isValidElement(child)) {
35 | element = child
36 | const path = child.props.path || child.props.from
37 | match = path
38 | ? matchPath(location.pathname, {...child.props, path})
39 | : this.context.match
40 | }
41 | })
42 | /* 防止路由渲染过程中,切换路由,页面不刷新情况,我们这里加入key, 提高渲染,防止渲染异常 */
43 | const key = element.props.cacheId || element.props.path
44 | const scroll = element.props.scroll
45 |
46 | return match
47 | ? isKeepliveRouter(element)
48 | ? cloneElement(element, { key, location, history, computedMatch: match, cacheDispatch, match, iskeep: true , scroll})
49 | : cloneElement(element, { key, location, history, computedMatch: match, cacheDispatch, match})
50 | : null
51 | }
52 | return __render.call(this)
53 | }
54 | }
55 | }
56 |
57 | KeepliveRouterSwitch.__componentType = KEEPLIVE_ROUTE_SWITCH
58 |
59 | const KeepSwitch = ({children, withoutRoute = false, deep = true,...props}) => {
60 | const ishasRouterSwitch = useMemo(() => {
61 | let ishas = false
62 | forEach(children, child => {
63 | if (isObject(child) && isFuntion(child.type) && isKeepliveRouter(child)) {
64 | invariant(
65 | child.props.cacheId || child.props.path,
66 | 'keepliveRouter should be a cacheid or paths attribute'
67 | )
68 | return (ishas = true)
69 | }
70 | })
71 | return ishas
72 | }, [])
73 |
74 | useEffect(()=>{
75 | /* 防止当 KeepSwitch 突然销毁造成 react 找不到即将销毁的真实dom节点引发的报错 */
76 | return function (){
77 | try{
78 | for(let key in beforeSwitchDestory){
79 | beforeSwitchDestory[key] && beforeSwitchDestory[key]()
80 | }
81 | }catch(e){}
82 | }
83 | },[])
84 |
85 | if (ishasRouterSwitch || deep) {
86 | return
87 | {
88 | cacheProps => {
89 | return withoutRoute
90 | ?
91 | children
92 | :
93 |
98 | {children}
99 |
100 | }
101 | }
102 |
103 | }
104 | return
105 | {children}
106 |
107 | }
108 |
109 | export default (useHistory || __RouterContext) ? withRouter(KeepSwitch) : KeepSwitch
--------------------------------------------------------------------------------
/example/demo/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "env": {
3 | "browser": true,
4 | "commonjs": true,
5 | "es6": true
6 | },
7 | "extends": "eslint:recommended",
8 | "globals": {
9 | "$": true,
10 | "process": true,
11 | "__dirname": true
12 | },
13 | "parser": "babel-eslint",
14 | "parserOptions": {
15 | "ecmaFeatures": {
16 | "experimentalObjectRestSpread": true,
17 | "jsx": true
18 | },
19 | "sourceType": "module",
20 | "ecmaVersion": 7
21 | },
22 | "plugins": [
23 | "react"
24 | ],
25 | "rules": {
26 | "quotes": [2, "single"], //单引号
27 | "no-console": 0, //不禁用console
28 | "no-debugger": 2, //禁用debugger
29 | "no-var": 0, //对var警告
30 | "semi": 0, //不强制使用分号
31 | "no-irregular-whitespace": 0, //不规则的空白不允许
32 | "no-trailing-spaces": 1, //一行结束后面有空格就发出警告
33 | "eol-last": 0, //文件以单一的换行符结束
34 | "no-unused-vars": [2, {"vars": "all", "args": "after-used"}], //不能有声明后未被使用的变量或参数
35 | "no-underscore-dangle": 0, //标识符不能以_开头或结尾
36 | "no-alert": 2, //禁止使用alert confirm prompt
37 | "no-lone-blocks": 0, //禁止不必要的嵌套块
38 | "no-class-assign": 2, //禁止给类赋值
39 | "no-cond-assign": 2, //禁止在条件表达式中使用赋值语句
40 | "no-const-assign": 2, //禁止修改const声明的变量
41 | "no-delete-var": 2, //不能对var声明的变量使用delete操作符
42 | "no-dupe-keys": 2, //在创建对象字面量时不允许键重复
43 | "no-duplicate-case": 2, //switch中的case标签不能重复
44 | "no-dupe-args": 2, //函数参数不能重复
45 | "no-empty": 2, //块语句中的内容不能为空
46 | "no-func-assign": 2, //禁止重复的函数声明
47 | "no-invalid-this": 0, //禁止无效的this,只能用在构造器,类,对象字面量
48 | "no-redeclare": 2, //禁止重复声明变量
49 | "no-spaced-func": 2, //函数调用时 函数名与()之间不能有空格
50 | "no-this-before-super": 0, //在调用super()之前不能使用this或super
51 | "no-undef": 2, //不能有未定义的变量
52 | "no-use-before-define": 2, //未定义前不能使用
53 | "camelcase": 0, //强制驼峰法命名
54 | "jsx-quotes": [2, "prefer-double"], //强制在JSX属性(jsx-quotes)中一致使用双引号
55 | "react/display-name": 0, //防止在React组件定义中丢失displayName
56 | "react/forbid-prop-types": [2, {"forbid": ["any"]}], //禁止某些propTypes
57 | "react/jsx-boolean-value": 2, //在JSX中强制布尔属性符号
58 | "react/jsx-closing-bracket-location": 1, //在JSX中验证右括号位置
59 | "react/jsx-curly-spacing": [2, {"when": "never", "children": true}], //在JSX属性和表达式中加强或禁止大括号内的空格。
60 | "react/jsx-indent-props": [2, 4], //验证JSX中的props缩进
61 | "react/jsx-key": 2, //在数组或迭代器中验证JSX具有key属性
62 | "react/jsx-max-props-per-line": [1, {"maximum": 1}], // 限制JSX中单行上的props的最大数量
63 | "react/jsx-no-bind": 0, //JSX中不允许使用箭头函数和bind
64 | "react/jsx-no-duplicate-props": 2, //防止在JSX中重复的props
65 | "react/jsx-no-literals": 0, //防止使用未包装的JSX字符串
66 | "react/jsx-no-undef": 1, //在JSX中禁止未声明的变量
67 | "react/jsx-pascal-case": 0, //为用户定义的JSX组件强制使用PascalCase
68 | "react/jsx-sort-props": 2, //强化props按字母排序
69 | "react/jsx-uses-react": 1, //防止反应被错误地标记为未使用
70 | "react/jsx-uses-vars": 2, //防止在JSX中使用的变量被错误地标记为未使用
71 | "react/no-danger": 0, //防止使用危险的JSX属性
72 | "react/no-did-mount-set-state": 0, //防止在componentDidMount中使用setState
73 | "react/no-did-update-set-state": 1, //防止在componentDidUpdate中使用setState
74 | "react/no-direct-mutation-state": 2, //防止this.state的直接变异
75 | "react/no-multi-comp": 2, //防止每个文件有多个组件定义
76 | "react/no-set-state": 0, //防止使用setState
77 | "react/no-unknown-property": 2, //防止使用未知的DOM属性
78 | "react/prefer-es6-class": 2, //为React组件强制执行ES5或ES6类
79 | "react/prop-types": 0, //防止在React组件定义中丢失props验证
80 | "react/react-in-jsx-scope": 2, //使用JSX时防止丢失React
81 | "react/self-closing-comp": 0, //防止没有children的组件的额外结束标签
82 | "react/sort-comp": 2, //强制组件方法顺序
83 | "no-extra-boolean-cast": 0, //禁止不必要的bool转换
84 | "react/no-array-index-key": 0, //防止在数组中遍历中使用数组key做索引
85 | "react/no-deprecated": 1, //不使用弃用的方法
86 | "react/jsx-equals-spacing": 2, //在JSX属性中强制或禁止等号周围的空格
87 | "no-unreachable": 1, //不能有无法执行的代码
88 | "comma-dangle": 2, //对象字面量项尾不能有逗号
89 | "no-mixed-spaces-and-tabs": 0, //禁止混用tab和空格
90 | "prefer-arrow-callback": 0, //比较喜欢箭头回调
91 | "arrow-parens": 0, //箭头函数用小括号括起来
92 | "arrow-spacing": 0 //=>的前/后括号
93 | },
94 | "settings": {
95 | "import/ignore": [
96 | "node_modules"
97 | ]
98 | }
99 | }
--------------------------------------------------------------------------------
/example/demo/src/page/goodsList/index.scss:
--------------------------------------------------------------------------------
1 | .list{
2 | list-style: none;
3 | background-color: pink;
4 | padding: 10px 20px;
5 | color: #fff;
6 | height: 50px;
7 | line-height: 50px;
8 | box-sizing: border-box;
9 | margin-bottom: 10px;
10 | margin-left: 24px;
11 | margin-right: 24px;
12 | font-weight: bold;
13 | border-radius:10px ;
14 | }
15 |
16 |
17 | .list_box{
18 | position: fixed;
19 | left:0;
20 | top:60px;
21 | overflow: scroll;
22 | bottom:0;
23 | right: 0;
24 | }
25 |
26 | .goods_item {
27 | line-height: 1!important;
28 | height: 134px;
29 | box-sizing: border-box;
30 | padding-bottom: 16px;
31 | display: flex;
32 | margin-bottom: 50px;
33 | position: relative;
34 |
35 | .newPerson {
36 | height: 34px;
37 | transform: translateY(7px);
38 | // top: -50px;
39 | width: 122px;
40 | }
41 |
42 |
43 |
44 |
45 | .item_image {
46 | height: 168px;
47 | width: 168px;
48 | transition: opacity 0.7s;
49 | border-radius: 12px;
50 | transform: translateY(-2px);
51 | }
52 |
53 | .item_content {
54 | flex: 1;
55 | box-sizing: border-box;
56 | position: relative;
57 | padding-left: 15px;
58 |
59 | .goods_name {
60 | font-family: PingFangSC-Regular;
61 | font-size: 14px;
62 | color: #303133;
63 | letter-spacing: 0;
64 | line-height: 18px;
65 | letter-spacing: 0;
66 | line-height: 18px;
67 | text-overflow: -o-ellipsis-lastline;
68 | overflow: hidden;
69 | text-overflow: ellipsis;
70 | word-break: break-all;
71 | display: -webkit-box;
72 | -webkit-line-clamp: 2;
73 | line-clamp: 2;
74 | min-height: 37px;
75 | -webkit-box-orient: vertical;
76 | margin-bottom: 7px;
77 | }
78 |
79 | // .go_share {
80 | // border-radius: 29px;
81 | // height: 58px;
82 | // width: 150px;
83 | // position: absolute;
84 | // bottom: 0;
85 | // right: 0;
86 | // line-height: 58px;
87 | // text-align: center;
88 | // font-family: HYYakuHei-GEW;
89 | // font-size: 26px;
90 | // color: #FFFFFF;
91 | // letter-spacing: 0;
92 | // }
93 |
94 |
95 | .new_price {
96 | position: relative;
97 | height: 25px;
98 | margin-bottom: 2px;
99 | display: inline-block;
100 | margin-left: -1px;
101 | transform: translateY(5px);
102 | .view {
103 | display: inline-block;
104 | font-family: JDZhengHT-Regular;
105 | color: #f12e40;
106 | letter-spacing: 0;
107 |
108 | }
109 |
110 | .one {
111 | transform: translateX(7px);
112 | }
113 |
114 | .two {
115 | font-size: 42px;
116 | line-height: 36px;
117 | transform: translateX(-1px);
118 | }
119 |
120 | .three {
121 | font-size: 26px;
122 | transform: translateX(-7px);
123 | }
124 | }
125 |
126 | .hold_price {
127 | height: 12px;
128 | }
129 |
130 | .old_price {
131 | display: inline-block;
132 | margin-left: 2px;
133 | font-family: JDZhengHT-Regular;
134 | font-size: 26px !important;
135 | color: #C0C4CC !important;
136 | letter-spacing: 0;
137 | transform: translate(-5px,4px);
138 | text-decoration: line-through;
139 | }
140 |
141 | .goods_tag {
142 | height: 30px;
143 | margin-bottom: 33px;
144 | transform: translateY(-2px);
145 | .ziying {
146 | width: 52px;
147 | height: 30px;
148 | margin-right: 5px;
149 | }
150 |
151 | .peisong {
152 | width: 96px;
153 | height: 30px;
154 | }
155 | }
156 | }
157 | }
--------------------------------------------------------------------------------
/src/components/keepCache.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/jsx-indent-props */
2 | /* eslint-disable react/no-multi-comp */
3 | import React, {useRef, useEffect, useMemo, memo, useContext} from 'react'
4 |
5 | import CacheContext from '../core/cacheContext'
6 | import useKeeper , { scrolls } from '../core/keeper'
7 | import {ACTION_ACTIVE, ACTION_ACTIVED, ACITON_UNACTIVE, ACTION_UNACTIVED, ACTION_DESTORYED} from '../utils/const'
8 | import {isFuntion} from '../utils/index'
9 |
10 |
11 | export const beforeSwitchDestory = {}
12 | const keepChange = (pre, next) => pre.state === next.state
13 | let cacheDispatchCurrent = null
14 |
15 | export const handerReactComponent = (children, prop) =>
16 | React.isValidElement(children)
17 | ? React.createElement(children, {...prop})
18 | : isFuntion(children)
19 | ? children(prop)
20 | : null
21 | const UpdateComponent = memo(({children}) => children, () => true)
22 |
23 | const CacheKeepItem = memo(({cacheId, children, state, dispatch, className='', lastState,load = () => {}, router = {}}) => {
24 | const parentCurDom = useRef(null)
25 | const curDom = useRef(null)
26 | const curComponent = useRef(null)
27 | useEffect(()=>{
28 | beforeSwitchDestory[cacheId] = function (){
29 | if(parentCurDom.current && curDom.current ) parentCurDom.current.appendChild(curDom.current)
30 | }
31 | return function(){
32 | if(scrolls[cacheId]) delete scrolls[cacheId]
33 | delete beforeSwitchDestory[cacheId]
34 | }
35 | },[])
36 | useEffect(() => {
37 | if (state === ACTION_ACTIVE) { /* 激活状态 */
38 | const {current} = curDom
39 | const parentNode = current.parentNode
40 | parentCurDom.current = parentNode
41 | load(current)
42 | } else if (state === ACITON_UNACTIVE) {
43 | parentCurDom.current.appendChild(curDom.current)
44 | /* 改变状态为休眠完成状态 */
45 | if (lastState === 'destory') {
46 | dispatch({
47 | type: ACTION_DESTORYED,
48 | payload: cacheId
49 | })
50 | } else {
51 | dispatch({
52 | type: ACTION_UNACTIVED,
53 | payload: cacheId
54 | })
55 | }
56 | }
57 | return () => {
58 | if (state === 'destory') { /* 如果是销毁阶段 */
59 | curComponent.current = null
60 | }
61 | }
62 | }, [state])
63 | return
69 | {(state === ACTION_ACTIVE || state === ACTION_ACTIVED || state === ACITON_UNACTIVE || state === ACTION_UNACTIVED) ? {children()} : null}
70 |
71 | }, keepChange)
72 |
73 | function Cache({children, className, ...prop}) {
74 | const [cacheState, cacheDispatch] = useKeeper()
75 | return
76 | {
77 | Object.keys(cacheState).map(cacheId =>{
78 | return
84 | })
85 | }
86 | {!cacheDispatchCurrent && (cacheDispatchCurrent = c)} />}
87 | {/* 提供对外的cacheDispatch方法 */}
88 | {useMemo(() => children({cacheDispatch}), [prop.location])}
89 |
90 | }
91 |
92 | export default Cache
93 |
94 | /* 对于层层嵌套的组件结构 ,我们需要一个容器来提供 cacheContext */
95 | export const GetCacheContext = ({children, cacheDispatch}) => {
96 | const cacheContext = useContext(CacheContext) || {}
97 | useEffect(() => {
98 | cacheDispatch && isFuntion(cacheDispatch) && cacheDispatch(cacheContext.cacheDispatch)
99 | }, [])
100 | return
101 | {context => {
102 | return handerReactComponent(children, context)
103 | }}
104 |
105 | }
106 |
107 | const nextTick = (cb)=> Promise.resolve().then(cb)
108 | let timer = null
109 |
110 | export const resolveCacheDispatch = ( dispatch ) => () => (action) => {
111 | const { type } = action
112 | const cacheDispatch = dispatch || cacheDispatchCurrent
113 | if( !cacheDispatch ) return
114 | if(type === 'reset'){
115 | if(timer) clearTimeout(timer)
116 | timer = setTimeout(()=>{
117 | cacheDispatch(action)
118 | nextTick(()=> {
119 | cacheDispatch({ type :'clear' })
120 | })
121 | },50)
122 | }else{
123 | cacheDispatch(action)
124 | }
125 | }
126 |
127 | export const useCacheDispatch = resolveCacheDispatch( cacheDispatchCurrent )
--------------------------------------------------------------------------------
/src/core/keeper.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-case-declarations */
2 | import {
3 | useReducer
4 | } from 'react'
5 | import {
6 | ACITON_CREATED,
7 | ACTION_ACTIVE,
8 | ACTION_ACTIVED,
9 | ACITON_UNACTIVE,
10 | ACTION_UNACTIVED,
11 | ACTION_RESERT,
12 | ACTION_DESTORYED,
13 | ACTION_CLEAR
14 | } from '../utils/const'
15 | import {
16 | isString,
17 | isArray,
18 | isFuntion
19 | } from '../utils/index'
20 |
21 | export const keeperCallbackQuene = []
22 | export const lifeCycles = {}
23 | export const scrolls = {}
24 |
25 | export const addKeeperListener = (cb) => {
26 | keeperCallbackQuene.push(cb)
27 | return () => {
28 | const keepIndex = keeperCallbackQuene.findIndex(callback => cb === callback)
29 | if (keepIndex) keeperCallbackQuene.splice(keepIndex, 1)
30 | }
31 | }
32 |
33 | function destoryState(state,keep){
34 | if (state[keep].state !== ACTION_ACTIVED) {
35 | state[keep].lastState = state[keep].state
36 | state[keep].state = 'destory'
37 | state[keep].load = null
38 | }
39 | }
40 |
41 | /**
42 | * keeplive keeplive状态 active(激活)
43 | * cacheId (缓存id) :{ state:状态 , children:激活组件实例 , load :加载函数 } state状态有 active actived unActive unActived created destoryed
44 | */
45 | const useKeeper = () => useReducer((state, action) => {
46 | const {
47 | type,
48 | payload
49 | } = action
50 | switch (type) {
51 | /* 添加keeplive状态 */
52 | case ACITON_CREATED:
53 | const {
54 | cacheId, children, load
55 | } = payload
56 | const isDestory = state[cacheId] && (state[cacheId].state === 'destory')
57 | if (!state[cacheId] || isDestory) {
58 | state[cacheId] = {
59 | children,
60 | state: ACITON_CREATED,
61 | lastState: isDestory ? 'destory' : '',
62 | load
63 | }
64 | }
65 | return state
66 | /* 开始激活状态 */
67 | case ACTION_ACTIVE:
68 | if (state[payload.cacheId]) {
69 | let other = {}
70 | if (payload.load) other.load = payload.load
71 | state[payload.cacheId] = {
72 | ...state[payload.cacheId],
73 | lastState: state[payload.cacheId].state,
74 | state: ACTION_ACTIVE,
75 | ...other
76 | }
77 | }
78 | return {
79 | ...state
80 | }
81 | /* 激活完成状态 */
82 | case ACTION_ACTIVED:
83 | if (state[payload]) {
84 | state[payload] = {
85 | ...state[payload],
86 | lastState: state[payload].state,
87 | state: ACTION_ACTIVED
88 | }
89 | const lifeCycleFunc = lifeCycles[payload]
90 | lifeCycleFunc && isFuntion(lifeCycleFunc) && lifeCycleFunc(ACTION_ACTIVED)
91 | }
92 | return {
93 | ...state
94 | }
95 | /* 休眠状态 */
96 | case ACITON_UNACTIVE:
97 | if (state[payload]) {
98 | state[payload] = {
99 | ...state[payload],
100 | lastState: state[payload].state,
101 | state: ACITON_UNACTIVE
102 | }
103 | }
104 | return {
105 | ...state
106 | }
107 | /* 休眠完成状态 */
108 | case ACTION_UNACTIVED:
109 | if (state[payload]) {
110 | state[payload] = {
111 | ...state[payload],
112 | lastState: state[payload].state,
113 | state: ACTION_UNACTIVED
114 | }
115 | const lifeCycleFunc = lifeCycles[payload]
116 | lifeCycleFunc && isFuntion(lifeCycleFunc) && lifeCycleFunc(ACTION_UNACTIVED)
117 | }
118 | return {
119 | ...state
120 | }
121 | /* 销毁状态 */
122 | case ACTION_RESERT:
123 | if (isString(payload) && state[payload]) {
124 | destoryState(state,payload)
125 | } else if (isArray(payload)) {
126 | payload.forEach(item=>{
127 | state[item] && destoryState(state,item)
128 | })
129 | } else if(!payload){
130 | Object.keys(state).forEach(keep => {
131 | destoryState(state,keep)
132 | })
133 | }
134 | return state
135 | /* 销毁单个state */
136 | case ACTION_DESTORYED:
137 | delete state[payload]
138 | return {
139 | ...state
140 | }
141 | case ACTION_CLEAR:
142 | Object.keys(state).forEach(keep => {
143 | if( state[keep].state !== ACTION_ACTIVED && ( state[keep].state === 'destory' || state[keep].lastState === 'destory' )){
144 | delete state[keep]
145 | }
146 | })
147 | return {
148 | ...state
149 | }
150 | default:
151 | return state
152 | }
153 | }, {})
154 |
155 | export default useKeeper
--------------------------------------------------------------------------------
/src/components/keepliveRoute.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/no-multi-comp */
2 | import React , { useContext } from 'react'
3 | import {Route , withRouter ,matchPath } from 'react-router-dom'
4 | import invariant from 'invariant'
5 |
6 | import CacheContext from '../core/cacheContext'
7 | import {isFuntion} from '../utils/index'
8 | import { resolveCacheDispatch } from './keepCache'
9 | import {keeperCallbackQuene ,scrolls } from '../core/keeper'
10 | import {
11 | KEEPLIVE_ROUTE_COMPONENT,
12 | ACITON_CREATED,
13 | ACTION_ACTIVE,
14 | ACTION_ACTIVED,
15 | ACITON_UNACTIVE,
16 | ACTION_UNACTIVED
17 | } from '../utils/const'
18 |
19 | class CacheRoute extends Route {
20 |
21 | cacheMatch=null
22 |
23 | constructor(prop, ...arg) {
24 | super(prop, ...arg)
25 | this.parentNode = null
26 | this.keepliveState = ''
27 | this.componentCur = null
28 | const {children, component,iskeep, render, cacheState, ...otherProps } = prop
29 | const { cacheDispatch, history, location } = otherProps
30 | /* 记录路由是否匹配 */
31 | const match = this.computerMatchRouter(prop)
32 | this.cacheMatch={...match}
33 | if (iskeep && cacheDispatch && cacheState && match ) {
34 | /* 如果当前 KeepliveRoute 没有被 KeepliveRouterSwitch 包裹 ,那么 KeepliveRoute 就会失去缓存作用, 就会按照正常route处理 */
35 | const cacheId = this.getAndcheckCacheKey()
36 | /* 执行监听函数 */
37 | Promise.resolve().then(() => {
38 | keeperCallbackQuene.forEach(cb => {
39 | isFuntion(cb) && cb({...otherProps}, this.getAndcheckCacheKey())
40 | })
41 | })
42 | if (!cacheState[cacheId] || (cacheState[cacheId] && cacheState[cacheId].state === 'destory')) {
43 | let WithRouterComponent = history && location ? component : withRouter(component)
44 |
45 | /* 对 cacheDispatch 处理 */
46 | const useCacheDispatch = resolveCacheDispatch(cacheDispatch)
47 | otherProps.cacheDispatch = useCacheDispatch()
48 | const childrenProps = { ...otherProps,cacheId }
49 |
50 | cacheDispatch({
51 | type: ACITON_CREATED,
52 | payload: {
53 | cacheId,
54 | load: this.injectDom.bind(this),
55 | children: () => children
56 | ? isFuntion(children)
57 | ? children(childrenProps)
58 | : children
59 | : component
60 | ? React.createElement(WithRouterComponent, childrenProps)
61 | : render
62 | ? render(childrenProps)
63 | : null
64 | }
65 | })
66 | this.keepliveState = ACITON_CREATED
67 | } else if (cacheState[cacheId]) {
68 | this.keepliveState = cacheState[cacheId].state
69 | }
70 | this.render = ()=>{
71 | return (this.parentNode = node)} />
72 | }
73 |
74 | }
75 | }
76 | /* 路由是否匹配 */
77 | computerMatchRouter=(props)=>{
78 | const { computedMatch ,location ,path } = props
79 | return computedMatch
80 | ? computedMatch
81 | : path
82 | ? matchPath(location.pathname, props)
83 | : null
84 | }
85 |
86 |
87 | UNSAFE_componentWillReceiveProps(curProps) {
88 | const { cacheState ,iskeep } = curProps
89 | if(!cacheState || !iskeep ) return
90 | this.keepliveState = cacheState[this.getAndcheckCacheKey()].state
91 | const newMatch = (this.computerMatchRouter(curProps) || {})
92 | if(this.keepliveState === 'actived' && newMatch && this.cacheMatch && newMatch.path !== this.cacheMatch.path && newMatch.url !== this.cacheMatch.url ){
93 | //TODO: 路由不一致情况
94 | }
95 | }
96 |
97 | getAndcheckCacheKey = () => {
98 | const {cacheId, path} = this.props
99 | const cacheKey = cacheId || path
100 | invariant(
101 | cacheKey,
102 | 'KeepliveRoute must have a cacheId'
103 | )
104 | return cacheKey
105 | }
106 |
107 | componentDidMount() {
108 | /* 如果第一次创建keepliveRouter,那么激活keepliveRouter */
109 | const {cacheDispatch, iskeep, scroll } = this.props
110 | if (!iskeep) return
111 | if (this.keepliveState === ACITON_CREATED) {
112 | cacheDispatch({
113 | type: ACTION_ACTIVE,
114 | payload: {cacheId: this.getAndcheckCacheKey()}
115 | })
116 | /* 如果keeplive是休眠状态,那么复用节点再次激活 */
117 | } else if (this.keepliveState === ACTION_UNACTIVED) {
118 | cacheDispatch({
119 | type: ACTION_ACTIVE,
120 | payload: {cacheId: this.getAndcheckCacheKey(), load: this.injectDom.bind(this)}
121 | })
122 | }
123 | if(scroll){
124 | this.parentNode.addEventListener('scroll', this.handerKeepScoll ,true)
125 | }
126 | const cacheId = this.getAndcheckCacheKey()
127 | if(scrolls[cacheId]){
128 | const { scrollTarget , scrollTop } = scrolls[cacheId]
129 | this.scrollTimer = setTimeout(()=>{
130 | if(scrollTarget) scrollTarget.scrollTop = scrollTop
131 | },0)
132 | }
133 |
134 | }
135 |
136 | handerKeepScoll= (e) => {
137 | if(!this.scrollTarget ) this.scrollTarget = e.target
138 | }
139 |
140 | injectDom = currentNode => {
141 | const {cacheDispatch} = this.props
142 | this.parentNode && this.parentNode.appendChild(currentNode)
143 | /* 改变状态actived 激活完成状态 */
144 | cacheDispatch({
145 | type: ACTION_ACTIVED,
146 | payload: this.getAndcheckCacheKey()
147 | })
148 | }
149 | exportDom = () => {
150 | const {cacheDispatch} = this.props
151 | const cacheId = this.getAndcheckCacheKey()
152 | if(this.scrollTarget){
153 | scrolls[cacheId] = {
154 | scrollTarget:this.scrollTarget,
155 | scrollTop:this.scrollTarget.scrollTop
156 | }
157 | }
158 | try {
159 | /* 切换keepalive缓存状态 */
160 | cacheDispatch({
161 | type: ACITON_UNACTIVE,
162 | payload:cacheId
163 | })
164 | } catch (e) {
165 |
166 | }
167 | }
168 | componentWillUnmount() {
169 | const {iskeep} = this.props
170 | if (!iskeep) return
171 | this.exportDom()
172 | this.parentNode.removeEventListener('scroll',this.handerKeepScoll,true)
173 | if(this.scrollTimer) clearTimeout(this.scrollTimer)
174 | }
175 |
176 | }
177 |
178 |
179 | const KeepliveRoute = (props)=>{
180 | const { path } = props
181 | const value = useContext(CacheContext) || {}
182 | const { cacheState } = value
183 | return (
184 | /* 对于外层没有 switch 包裹的结构,我们需要一个 `控制器` 来控制 组件的 挂载与销毁 , 这里用 Route 刚好 */
185 |
{
187 | return (
188 | )
194 | }}
195 | />
196 | )
197 | }
198 |
199 | KeepliveRoute.__componentType = KEEPLIVE_ROUTE_COMPONENT
200 |
201 | export default KeepliveRoute
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | [](https://www.npmjs.org/package/react-keepalive-router)
4 | [](https://npmjs.org/package/react-keepalive-router)
5 | 
6 |
7 | # react-keepalive-router
8 |
9 |
10 | ## 一 介绍
11 |
12 | 基于`react 16.8+` ,`react-router 4+` 开发的`react`缓存组件,可以用于缓存页面组件,类似`vue`的`keepalive`包裹`vue-router`的效果功能。
13 |
14 | 采用`react hooks`全新`api`,支持缓存路由,手动解除缓存,增加了**缓存的状态周期**,监听函数等。
15 |
16 | 后续版本会完善其他功能。
17 |
18 |
19 |
20 | ### demo
21 |
22 | #### 缓存组件 + 监听
23 |
24 |
25 | ## 二 快速上手
26 |
27 |
28 | ### 下载
29 |
30 | ```bash
31 | npm install react-keepalive-router --save
32 | # or
33 | yarn add react-keepalive-router
34 | ```
35 |
36 |
37 | ### 使用
38 |
39 | ### 1 基本用法
40 |
41 |
42 | #### KeepaliveRouterSwitch
43 |
44 |
45 | `KeepaliveRouterSwitch`可以理解为常规的Switch,也可以理解为 `keepaliveScope`,我们**确保整个缓存作用域,只有一个 `KeepaliveRouterSwitch` 就可以了**。
46 |
47 | #### 常规用法
48 |
49 | ````jsx
50 | import { BrowserRouter as Router, Route, Redirect ,useHistory } from 'react-router-dom'
51 | import { KeepaliveRouterSwitch ,KeepaliveRoute ,addKeeperListener } from 'react-keepalive-router'
52 |
53 | const index = () => {
54 | useEffect(()=>{
55 | /* 增加缓存监听器 */
56 | addKeeperListener((history,cacheKey)=>{
57 | if(history)console.log('当前激活状态缓存组件:'+ cacheKey )
58 | })
59 | },[])
60 | return
61 |
62 |
63 |
64 |
65 |
66 |
67 | { /* 我们将详情页加入缓存 */ }
68 |
69 |
70 |
71 |
72 |
73 |
74 | }
75 | ````
76 |
77 |
78 | 这里应该注意⚠️的是对于复杂的路由结构。或者KeepaliveRouterSwitch 包裹的子组件不是Route ,我们要给 `KeepaliveRouterSwitch` 增加特有的属性 `withoutRoute` 就可以了。如下例子🌰🌰🌰:
79 |
80 | **例子一**
81 |
82 | ````jsx
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | ````
92 |
93 | **例子二**
94 |
95 | 或者我们可以使用 `renderRoutes` 等`api`配合 `KeepliveRouterSwitch` 使用 。
96 |
97 | ````jsx
98 | import {renderRoutes} from "react-router-config"
99 | { renderRoutes(routes) }
100 | ````
101 |
102 |
103 | #### KeepaliveRoute
104 |
105 | `KeepaliveRoute` 基本使用和 `Route`没有任何区别。
106 |
107 |
108 | **在当前版本中⚠️⚠️⚠️如果 `KeepaliveRoute` 如果没有被 `KeepaliveRouterSwitch`包裹就会失去缓存作用。**
109 |
110 | **效果**
111 |
112 | 
113 |
114 |
115 | 
116 |
117 | ### 2 其他功能
118 |
119 |
120 |
121 | #### 1 缓存组件激活监听器
122 |
123 | 如果我们希望对当前激活的组件,有一些额外的操作,我们可以添加监听器,用来监听缓存组件的激活状态。
124 |
125 | ````js
126 | addKeeperListener((history,cacheKey)=>{
127 | if(history)console.log('当前激活状态缓存组件:'+ cacheKey )
128 | })
129 | ````
130 | 第一个参数未history对象,第二个参数为当前缓存路由的唯一标识cacheKey
131 |
132 | #### 2 清除缓存
133 |
134 | 缓存的组件,或是被`route`包裹的组件,会在`props`增加额外的方法`cacheDispatch`用来清除缓存。
135 |
136 | 如果props没有`cacheDispatch`方法,可以通过
137 |
138 |
139 | ````js
140 |
141 |
142 | import React from 'react'
143 | import { useCacheDispatch } from 'react-keepalive-router'
144 |
145 | function index(){
146 | const cacheDispatch = useCacheDispatch()
147 | return 我是首页
148 |
149 |
150 | }
151 |
152 | export default index
153 | ````
154 |
155 | **1 清除所有缓存**
156 |
157 | ````js
158 | cacheDispatch({ type:'reset' })
159 | ````
160 |
161 | **2 清除单个缓存**
162 |
163 | ````js
164 | cacheDispatch({ type:'reset',payload:'cacheId' })
165 | ````
166 |
167 | **3 清除多个缓存**
168 |
169 | ````js
170 | cacheDispatch({ type:'reset',payload:['cacheId1','cacheId2'] })
171 | ````
172 |
173 | #### 3 缓存scroll ,增加缓存滚动条功能
174 |
175 | 如果我们想要缓存列表 `scrollTop` 的位置 ,我们可以在 `KeepaliveRoute` 动态添加 `scroll` 属性 ( 目前仅支持y轴 )。 为什么加入`scroll`,我们这里考虑到,只有在想要缓存`scroll`的y值的时候,才进行缓存,避免不必要的事件监听和内存开销。
176 |
177 | ````js
178 |
179 | ````
180 |
181 | **效果**
182 |
183 | 
184 |
185 |
186 |
187 | #### 4 生命周期
188 |
189 | `react-keepalive-router`加入了全新的页面组件生命周期 `actived` 和 `unActived`, `actived` 作为缓存路由组件激活时候用,初始化的时候会默认执行一次 , `unActived`作为路由组件缓存完成后调用。但是生命周期需要用一个`HOC`组件`keepaliveLifeCycle`包裹。
190 |
191 | 使用:
192 |
193 |
194 |
195 |
196 | ````js
197 | import React from 'react'
198 |
199 | import { keepaliveLifeCycle } from 'react-keepalive-router'
200 | import './style.scss'
201 |
202 | @keepaliveLifeCycle
203 | class index extends React.Component{
204 |
205 | state={
206 | activedNumber:0,
207 | unActivedNumber:0
208 | }
209 | actived(){
210 | this.setState({
211 | activedNumber:this.state.activedNumber + 1
212 | })
213 | }
214 | unActived(){
215 | this.setState({
216 | unActivedNumber:this.state.unActivedNumber + 1
217 | })
218 | }
219 | render(){
220 | const { activedNumber , unActivedNumber } = this.state
221 | return
222 |
页面 actived 次数: { activedNumber }
223 |
页面 unActived 次数:{ unActivedNumber }
224 |
225 | }
226 | }
227 |
228 | export default index
229 | ````
230 |
231 | 效果:
232 |
233 |
234 | 
235 |
236 | 这里注意的是 `keepaliveLifeCycle` 要是组件最近的 `Hoc`。
237 |
238 | 比如
239 |
240 | 装饰器模式下:
241 | **🙅错误做法**
242 | ````js
243 | @keepaliveLifeCycle
244 | @withStyles(styles)
245 | @withRouter
246 | class Index extends React.Componen{
247 |
248 | }
249 | ````
250 |
251 | **🙆正确做法**
252 | ````js
253 | @withStyles(styles)
254 | @withRouter
255 | @keepaliveLifeCycle
256 | class Index extends React.Componen{
257 |
258 | }
259 | ````
260 |
261 | 非装饰器模式下:
262 | **🙅错误做法**
263 | ````js
264 | class Index extends React.Componen{
265 |
266 | }
267 |
268 | export default keepaliveLifeCycle( withRouter(Index) )
269 | ````
270 |
271 | **🙆正确做法**
272 | ````js
273 | class Index extends React.Componen{
274 |
275 | }
276 |
277 | export default withRouter( keepaliveLifeCycle(Index) )
278 | ````
--------------------------------------------------------------------------------