├── .babelrc ├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.MIT ├── ONLINE_EDITOR.md ├── README.md ├── README.zh-CN.md ├── assets ├── basic-currently.gif ├── basic-original.gif ├── lifecycle.png └── react-logo.png ├── demo ├── index.html └── src │ ├── KeepAlive.backup.js │ ├── KeepAlive.js │ ├── index.js │ ├── utils.js │ └── views │ ├── A.js │ ├── B.js │ └── C.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── components │ ├── AsyncComponent.tsx │ ├── Comment.tsx │ ├── Consumer.tsx │ ├── KeepAlive.tsx │ └── Provider.tsx ├── contexts │ ├── IdentificationContext.ts │ └── KeepAliveContext.ts ├── index.ts └── utils │ ├── bindLifecycle.tsx │ ├── changePositionByComment.ts │ ├── createEventEmitter.ts │ ├── createStoreElement.ts │ ├── createUniqueIdentification.ts │ ├── debug.ts │ ├── getDisplayName.ts │ ├── getKeepAlive.ts │ ├── getKeyByFiberNode.ts │ ├── isRegExp.ts │ ├── keepAliveDecorator.tsx │ ├── md5.ts │ ├── noop.ts │ ├── shallowEqual.ts │ ├── useKeepAliveEffect.tsx │ ├── withIdentificationContextConsumer.tsx │ └── withKeepAliveContextConsumer.tsx ├── test ├── Comment.test.js ├── KeepAlive.test.js └── setup.js ├── tsconfig.json ├── tslint.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/env", 4 | "@babel/react" 5 | ], 6 | "plugins": [ 7 | [ 8 | "@babel/plugin-proposal-decorators", 9 | { 10 | "legacy": true 11 | } 12 | ], 13 | "@babel/plugin-proposal-class-properties" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 2 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | es/** 4 | cjs/** 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .travis.yml 3 | .babelrc 4 | .editorconfig 5 | test 6 | demo 7 | node_modules 8 | coverage 9 | src 10 | tsconfig.json 11 | webpack.config.js 12 | tslint.json 13 | jest.config.js 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language : node_js 2 | 3 | node_js: 4 | - "11" 5 | 6 | install: 7 | - npm install 8 | 9 | script: 10 | - npm test 11 | 12 | after_success: 13 | - npm run codecov 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | All notable changes are described on the [Releases](https://github.com/Sam618/react-keep-alive/releases) page. 2 | -------------------------------------------------------------------------------- /LICENSE.MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Shen Chang 2019-2020 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ONLINE_EDITOR.md: -------------------------------------------------------------------------------- 1 | # Online Editor 2 | 3 | - Basic([original](https://codesandbox.io/s/6wy4mlkkzn) | [currently](https://codesandbox.io/s/z657w5l4zl)) 4 | 5 |
6 | Basic original 7 | Basic currently 8 |
9 | 10 | - [With React Router](https://codesandbox.io/s/yjn7k230z) 11 | - [With React Loadable](https://codesandbox.io/s/3r5331vjl1) 12 | - [Lifecycle and events](https://codesandbox.io/s/q1xprn1qq) 13 | - [Control cache](https://codesandbox.io/s/llp50vxnq7) 14 | - [Using animation](https://codesandbox.io/s/zljyprv8op) 15 | - [useKeepAliveEffect](https://codesandbox.io/s/yp6qjk1vw1) 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 | 7 |

React Keep Alive

8 |
9 | 10 | [![npm](https://img.shields.io/npm/v/react-keep-alive.svg?style=for-the-badge)](https://www.npmjs.com/package/react-keep-alive) [![Travis (.org)](https://img.shields.io/travis/Sam618/react-keep-alive.svg?style=for-the-badge)](https://travis-ci.org/Sam618/react-keep-alive.svg?branch=master) [![LICENSE](https://img.shields.io/npm/l/react-keep-alive.svg?style=for-the-badge)](https://github.com/Sam618/react-keep-alive/blob/master/LICENSE.MIT) [![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/react-keep-alive.svg?style=for-the-badge)](https://www.npmjs.com/package/react-keep-alive) [![downloads](https://img.shields.io/npm/dm/react-keep-alive.svg?style=for-the-badge)](https://www.npmjs.com/package/react-keep-alive) [![typescript](https://img.shields.io/badge/language-typescript-blue.svg?style=for-the-badge)](https://www.typescriptlang.org/) 11 | 12 |

13 | 14 |

A component that maintains component state and avoids repeated re-rendering.

15 | 16 |
17 |
English | 中文
18 |
Online Editor
19 |
20 |
21 | 22 | 23 | ## ✨ Features 24 | - Not based on React Router, so you can use it wherever you need to cache it. 25 | - You can easily use to wrap your components to keep them alive. 26 | - Because it is not controlled by `display: none | block`, you can use animation. 27 | - You will be able to use the latest React Hooks. 28 | - Ability to manually control whether your components need to stay active. 29 | 30 | ## 📦 Installation 31 | React Keep Alive requires React 16.3 or later, but if you use React Hooks, you must be React 16.8 or higher. 32 | 33 | To use React Keep Alive with your React app: 34 | 35 | ```bash 36 | npm install --save react-keep-alive 37 | ``` 38 | 39 | 40 | ## 🔨 Usage 41 | React Keep Alive provides ``, you must use `` to wrap the `` cache to take effect. 42 | 43 | ```JavaScript 44 | import React from 'react'; 45 | import ReactDOM from 'react-dom'; 46 | import { 47 | Provider, 48 | KeepAlive, 49 | } from 'react-keep-alive'; 50 | import Test from './views/Test'; 51 | 52 | ReactDOM.render( 53 | 54 | 55 | 56 | 57 | , 58 | document.getElementById('root'), 59 | ); 60 | ``` 61 | 62 | 63 | ## 💡 Why do you need this component? 64 | If you've used [Vue](https://vuejs.org/), you know that it has a very good component ([keep-alive](https://vuejs.org/v2/guide/components-dynamic-async.html)) that keeps the state of the component to avoid repeated re-rendering. 65 | 66 | Sometimes, we want the list page to cache the page state after the list page enters the detail page. When the detail page returns to the list page, the list page is still the same as before the switch. 67 | 68 | Oh, this is actually quite difficult to achieve, because the components in React cannot be reused once they are uninstalled. Two solutions are proposed in [issue #12039](https://github.com/facebook/react/issues/12039). By using the style switch component display (`display: none | block;`), this can cause problems, such as when you switch components, you can't use animations; or use data flow management tools like Mobx and Redux, but this is too much trouble. 69 | 70 | In the end, I implemented this effect through the [React.createPortal API](https://reactjs.org/docs/portals.html). `react-keep-alive` has two main components `` and ``. The `` is responsible for saving the component's cache and rendering the cached component outside of the application via the React.createPortal API before processing. The cached components must be placed in ``, and `` will mount the components that are cached outside the application to the location that really needs to be displayed. 71 | 72 | 73 | ## 📝 API Reference 74 | 75 | ### `Provider` 76 | Since the cached components need to be stored, the `` must be rendered at the top of the application for the program to run properly. 77 | 78 | #### Props 79 | `include`: Only components that match key will be cached. It can be a string, an array of strings, or a regular expression, eg: 80 | ```JavaScript 81 | ... 82 | // or 83 | ... 84 | // or 85 | ... 86 | ``` 87 | 88 | `exclude`: Any component that matches key will not be cached. It can be a string, an array of strings, or a regular expression. 89 | 90 | `max`(`v2.5.2+`): If the maximum value is set, the value in the cache is deleted after it goes out. 91 | 92 | #### Example 93 | In the example below, the component is our root-level component. This means it’s at the very top of our component hierarchy. 94 | 95 | ```javascript 96 | import React from 'react'; 97 | import ReactDOM from 'react-dom'; 98 | import { Provider } from 'react-keep-alive'; 99 | import App from './App'; 100 | 101 | ReactDOM.render( 102 | 103 | 104 | , 105 | document.getElementById('root'), 106 | ); 107 | ``` 108 | 109 | ##### Usage with React Router and Mobx React 110 | 111 | ```JavaScript 112 | import React from 'react'; 113 | import ReactDOM from 'react-dom'; 114 | import { 115 | BrowserRouter as Router, 116 | } from 'react-router-dom'; 117 | import { 118 | Provider as MobxProvider, 119 | } from 'mobx-react'; 120 | import { 121 | Provider as KeepAliveProvider, 122 | } from 'react-keep-alive'; 123 | 124 | ReactDOM.render( 125 | 126 | 127 | 128 | 129 | 130 | 131 | , 132 | document.getElementById('root'), 133 | ); 134 | ``` 135 | 136 | **Note**: You must put in and the React Router must be sure to be the **latest version**. Because React Keep Alive uses the **new Context**, you must ensure that the Router does the same. Please use the following command to install the latest version. 137 | 138 | ```bash 139 | npm install react-router@next react-router-dom@next 140 | ``` 141 | 142 | 143 | ### `KeepAlive` 144 | Children of `` will be cached, but we have to make sure that `` is inside ``. 145 | 146 | #### Props 147 | `name`: Name must exist and need to ensure that all `` names under the current `` are unique(1.2.0 added, Replace key). 148 | 149 | `disabled`: When we don't need components for caching, we can disable it; the disabled configuration will only takes effect when the component's status changes from unactive to active. 150 | 151 | `extra`(`v2.0.1+`): Additional data can be obtained through `bindLifecycle`. 152 | 153 | **Note**: `` The innermost outer layer of the packaged component must have a real DOM tag. 154 | 155 | #### Example 156 | ```JavaScript 157 | import React from 'react'; 158 | import ReactDOM from 'react-dom'; 159 | import { 160 | BrowserRouter as Router, 161 | Switch, 162 | Route, 163 | Link, 164 | } from 'react-router-dom'; 165 | import { 166 | Provider, 167 | KeepAlive, 168 | } from 'react-keep-alive'; 169 | 170 | class One extends React.Component { 171 | render() { 172 | return ( 173 | // a real DOM tag 174 |
This is One.
175 | ); 176 | } 177 | } 178 | 179 | class App extends React.Component { 180 | render() { 181 | return ( 182 |
183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 |
191 | ); 192 | } 193 | } 194 | 195 | ReactDOM.render( 196 | 197 | 198 | 199 | 200 | , 201 | document.getElementById('root'), 202 | ); 203 | ``` 204 | 205 | ##### Usage with `include` props of `` 206 | ```JavaScript 207 | import React from 'react'; 208 | import ReactDOM from 'react-dom'; 209 | import { 210 | BrowserRouter as Router, 211 | Switch, 212 | Route, 213 | Link, 214 | } from 'react-router-dom'; 215 | import { 216 | Provider, 217 | KeepAlive, 218 | } from 'react-keep-alive'; 219 | 220 | class One extends React.Component { 221 | render() { 222 | return ( 223 |
This is One.
224 | ); 225 | } 226 | } 227 | 228 | class App extends React.Component { 229 | render() { 230 | return ( 231 |
232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 |
240 | ); 241 | } 242 | } 243 | 244 | ReactDOM.render( 245 | 246 | 247 | 248 | 249 | , 250 | document.getElementById('root'), 251 | ); 252 | ``` 253 | 254 | **Note**: If you want to use the **lifecycle**, wrap the components in a `bindLifecycle` high-level component. 255 | 256 | ### `bindLifecycle` 257 | Components that pass this high-level component wrap will have the **correct** lifecycle, and we have added two additional lifecycles, `componentDidActivate` and `componentWillUnactivate`. 258 | 259 | Lifecycle after adding: 260 | ![Lifecycle after adding](https://github.com/Sam618/react-keep-alive/raw/master/assets/lifecycle.png) 261 | 262 | `componentDidActivate` will be executed once after the initial mount or from the unactivated state to the active state. although we see `componentDidActivate` after `componentDidUpdate` in the `Updating` phase, this does not mean `componentDidActivate` Always triggered. 263 | 264 | At the same time, only one of the lifecycles of `componentWillUnactivate` and `componentWillUnmount` is triggered. `componentWillUnactivate` is executed when caching is required; `componentWillUnmount` is executed without caching. 265 | 266 | #### Example 267 | ```JavaScript 268 | import React from 'react'; 269 | import {bindLifecycle} from 'react-keep-alive'; 270 | 271 | @bindLifecycle 272 | class Test extends React.Component { 273 | render() { 274 | return ( 275 |
276 | This is Test. 277 |
278 | ); 279 | } 280 | } 281 | ``` 282 | 283 | 284 | ### `useKeepAliveEffect` 285 | `useKeepAliveEffect` will fire when the component enters and leaves; because the component will not be unmounted while it is still active, so if you use `useEffect`, that will not achieve the real purpose. 286 | 287 | **Note**: `useKeepAliveEffect` uses the latest React Hooks, so you must make sure React is the latest version. 288 | 289 | #### Example 290 | ```JavaScript 291 | import React from 'react'; 292 | import {useKeepAliveEffect} from 'react-keep-alive'; 293 | 294 | function Test() { 295 | useKeepAliveEffect(() => { 296 | console.log("mounted"); 297 | return () => { 298 | console.log("unmounted"); 299 | }; 300 | }); 301 | return ( 302 |
303 | This is Test. 304 |
305 | ); 306 | } 307 | ``` 308 | 309 | 310 | ## 🐛 Issues 311 | If you find a bug, please file an issue on [our issue tracker on GitHub](https://github.com/Sam618/react-keep-alive/issues). 312 | 313 | 314 | ## 🏁 Changelog 315 | Changes are tracked in the [CHANGELOG.md](https://github.com/Sam618/react-keep-alive/blob/master/CHANGELOG.md). 316 | 317 | 318 | ## 📄 License 319 | React Keep Alive is available under the [MIT](https://github.com/Sam618/react-keep-alive/blob/master/LICENSE) License. 320 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 | 7 |

React Keep Alive

8 |
9 | 10 | [![npm](https://img.shields.io/npm/v/react-keep-alive.svg?style=for-the-badge)](https://www.npmjs.com/package/react-keep-alive) [![Travis (.org)](https://img.shields.io/travis/Sam618/react-keep-alive.svg?style=for-the-badge)](https://travis-ci.org/Sam618/react-keep-alive.svg?branch=master) [![LICENSE](https://img.shields.io/npm/l/react-keep-alive.svg?style=for-the-badge)](https://github.com/Sam618/react-keep-alive/blob/master/LICENSE.MIT) [![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/react-keep-alive.svg?style=for-the-badge)](https://www.npmjs.com/package/react-keep-alive) [![downloads](https://img.shields.io/npm/dm/react-keep-alive.svg?style=for-the-badge)](https://www.npmjs.com/package/react-keep-alive) [![typescript](https://img.shields.io/badge/language-typescript-blue.svg?style=for-the-badge)](https://www.typescriptlang.org/) 11 | 12 | 13 |

14 | 15 |

一个保持组件状态并避免重复重渲染的组件。

16 | 17 |
18 |
English | 中文
19 | 20 |
21 |
22 | 23 | 24 | ## ✨ 特征 25 | - 不基于 React Router,因此可以在任何需要缓存的地方使用它。 26 | - 你可以轻松地使用 `` 包装你组件来使它们保持活力。 27 | - 因为并不是使用 `display: none | block` 来控制的,所以可以使用动画。 28 | - 你将能够使用最新的 React Hooks。 29 | - 能够手动控制你的组件是否需要保持活力。 30 | 31 | 32 | ## 📦 安装 33 | React Keep Alive 最低支持 React 16.3 版本,但是如果你使用了 React Hooks,那么必须是 React 16.8 或更高版本。 34 | 35 | 在你的应用中安装 React Keep Alive: 36 | 37 | ```bash 38 | npm install --save react-keep-alive 39 | ``` 40 | 41 | 42 | ## 🔨 使用 43 | React Keep Alive 提供了 ``, 你必须把 `` 放在 `Provider` 里面。 44 | 45 | ```JavaScript 46 | import React from 'react'; 47 | import ReactDOM from 'react-dom'; 48 | import { 49 | Provider, 50 | KeepAlive, 51 | } from 'react-keep-alive'; 52 | import Test from './views/Test'; 53 | 54 | ReactDOM.render( 55 | 56 | 57 | 58 | 59 | , 60 | document.getElementById('root'), 61 | ); 62 | ``` 63 | 64 | 65 | ## 💡 为什么使用这个组件? 66 | 如果你用过 [Vue](https://vuejs.org/),那肯定知道它有一个非常好用的组件([keep-alive](https://vuejs.org/v2/guide/components-dynamic-async.html))能够保持组件的状态来避免重复重渲染。 67 | 68 | 有时,我们希望在列表页面进入详情页面后,缓存列表页面的状态;当从详情页面返回列表页面时,列表页面还是和切换前一样。 69 | 70 | 这实际上挺难实现的,因为 React 中的组件一旦卸载就无法重用。在 [issue #12039](https://github.com/facebook/react/issues/12039) 中提出了两种解决方案;通过样式来控制组件的显示(`display:none | block;`),但是这可能会导致问题,例如切换组件时,无法使用动画;或者使用像 Mobx 和 Redux 这样的数据流管理工具,但这太麻烦了。 71 | 72 | 最后,我通过 [React.createPortal API](https://reactjs.org/docs/portals.html) 实现了这个效果。`react-keep-alive` 有两个主要的组件 `` 和 ``;`` 负责保存组件的缓存,并在处理之前通过 `React.createPortal` API 将缓存的组件渲染在应用程序的外面。缓存的组件必须放在 `` 中,`` 会把在应用程序外面渲染的组件挂载到真正需要显示的位置。 73 | 74 | 75 | ## 📝 API 参考 76 | 77 | ### `Provider` 78 | 因为需要存储缓存的组件,所以必须把 `` 放在应用程序的最外面以使程序能够正常运行。 79 | 80 | #### Props 81 | `include`:只会缓存匹配 `key` 的组件。它可以是字符串,字符串数组或正则表达式,例如: 82 | ```JavaScript 83 | ... 84 | // or 85 | ... 86 | // or 87 | ... 88 | ``` 89 | 90 | `exclude`:任何匹配 `key` 的组件都不会被缓存。它可以是字符串,字符串数组或正则表达式。 91 | 92 | `max`(`v2.5.2+`):如果设置了最大值,那么在超出最大值之后就会删除缓存中的值。 93 | 94 | #### 例子 95 | 在下面的示例中,`` 是我们的根组件,这意味着它位于组件层次结构的最顶层。 96 | 97 | ```javascript 98 | import React from 'react'; 99 | import ReactDOM from 'react-dom'; 100 | import { Provider } from 'react-keep-alive'; 101 | import App from './App'; 102 | 103 | ReactDOM.render( 104 | 105 | 106 | , 107 | document.getElementById('root'), 108 | ); 109 | ``` 110 | 111 | ##### 结合 React Router 和 Mobx React 使用 112 | 113 | ```JavaScript 114 | import React from 'react'; 115 | import ReactDOM from 'react-dom'; 116 | import { 117 | BrowserRouter as Router, 118 | } from 'react-router-dom'; 119 | import { 120 | Provider as MobxProvider, 121 | } from 'mobx-react'; 122 | import { 123 | Provider as KeepAliveProvider, 124 | } from 'react-keep-alive'; 125 | 126 | ReactDOM.render( 127 | 128 | 129 | 130 | 131 | 132 | 133 | , 134 | document.getElementById('root'), 135 | ); 136 | ``` 137 | 138 | **注意**:React Router 必须确保是 **最新版本**。因为 React Keep Alive 使用了 **new Context**,所以必须确保 `` 使用相同的 API。请使用以下命令安装 React Router 的最新版本: 139 | 140 | ```bash 141 | npm install react-router@next react-router-dom@next 142 | ``` 143 | 144 | 145 | ### `KeepAlive` 146 | 我们必须确保 `` 在 `` 里面,这样 `` 的子组件才能被缓存。 147 | 148 | #### Props 149 | `name`:`name` 必须存在并且需要确保当前 `` 下的所有 `` 的 `name` 都是唯一的(1.2.0 新增,替换 `key`)。 150 | 151 | `disabled`:当我们不需要缓存组件时,我们可以禁用它;禁用仅在组件从未激活状态变为激活状态时生效。 152 | 153 | `extra`(`v2.0.1+`): 额外的数据可以通过 `bindLifecycle` 获取。 154 | 155 | **注意**:`` 包裹的组件内部最外层必须有一个真实的 DOM 标签。 156 | 157 | 158 | #### 例子 159 | ```JavaScript 160 | import React from 'react'; 161 | import ReactDOM from 'react-dom'; 162 | import { 163 | BrowserRouter as Router, 164 | Switch, 165 | Route, 166 | Link, 167 | } from 'react-router-dom'; 168 | import { 169 | Provider, 170 | KeepAlive, 171 | } from 'react-keep-alive'; 172 | 173 | class One extends React.Component { 174 | render() { 175 | return ( 176 |
This is One.
177 | ); 178 | } 179 | } 180 | 181 | class App extends React.Component { 182 | render() { 183 | return ( 184 |
185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 |
193 | ); 194 | } 195 | } 196 | 197 | ReactDOM.render( 198 | 199 | 200 | 201 | 202 | , 203 | document.getElementById('root'), 204 | ); 205 | ``` 206 | 207 | ##### 使用 `` 的 `include` 属性 208 | ```JavaScript 209 | import React from 'react'; 210 | import ReactDOM from 'react-dom'; 211 | import { 212 | BrowserRouter as Router, 213 | Switch, 214 | Route, 215 | Link, 216 | } from 'react-router-dom'; 217 | import { 218 | Provider, 219 | KeepAlive, 220 | } from 'react-keep-alive'; 221 | 222 | class One extends React.Component { 223 | render() { 224 | return ( 225 |
This is One.
226 | ); 227 | } 228 | } 229 | 230 | class App extends React.Component { 231 | render() { 232 | return ( 233 |
234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 |
242 | ); 243 | } 244 | } 245 | 246 | ReactDOM.render( 247 | 248 | 249 | 250 | 251 | , 252 | document.getElementById('root'), 253 | ); 254 | ``` 255 | 256 | **注意**:如果要使用 **生命周期**,请将组件包装在 `bindLifecycle` 高阶组件中。 257 | 258 | ### `bindLifecycle` 259 | 这个高阶组件包装的组件将具有 **正确的** 的生命周期,并且我们添加了两个额外的生命周期 `componentDidActivate` 和 `componentWillUnactivate`。 260 | 261 | 添加新的生命周期之后: 262 | ![Lifecycle after adding](https://github.com/Sam618/react-keep-alive/raw/master/assets/lifecycle.png) 263 | 264 | `componentDidActivate` 将在组件刚挂载或从未激活状态变为激活状态时执行。虽然我们在 `Updating` 阶段的 `componentDidUpdate` 之后能够看到 `componentDidActivate`,但这并不意味着 `componentDidActivate` 总是被触发。 265 | 266 | 同时只能触发 `componentWillUnactivate` 和 `componentWillUnmount` 生命周期的其中之一。当需要缓存时执行 `componentWillUnactivate`,而 `componentWillUnmount` 在禁用缓存的情况下执行。 267 | 268 | #### 例子 269 | ```JavaScript 270 | import React from 'react'; 271 | import {bindLifecycle} from 'react-keep-alive'; 272 | 273 | @bindLifecycle 274 | class Test extends React.Component { 275 | render() { 276 | return ( 277 |
278 | This is Test. 279 |
280 | ); 281 | } 282 | } 283 | ``` 284 | 285 | 286 | ### `useKeepAliveEffect` 287 | `useKeepAliveEffect` 会在组件进入和离开时触发;因为在保持活力时组件不会被卸载,所以如果使用的是 `useEffect`,那将不会达到真正的目的。 288 | 289 | **注意**:`useKeepAliveEffect` 使用了最新的 React Hooks,所以必须确保 React 是最新版本。 290 | 291 | #### 例子 292 | ```JavaScript 293 | import React from 'react'; 294 | import {useKeepAliveEffect} from 'react-keep-alive'; 295 | 296 | function Test() { 297 | useKeepAliveEffect(() => { 298 | console.log("mounted"); 299 | return () => { 300 | console.log("unmounted"); 301 | }; 302 | }); 303 | return ( 304 |
305 | This is Test. 306 |
307 | ); 308 | } 309 | ``` 310 | 311 | 312 | ## 🐛 Issues 313 | 如果你发现了错误,请在 [我们 GitHub 的 Issues](https://github.com/Sam618/react-keep-alive/issues) 上提出问题。 314 | 315 | 316 | ## 🏁 Changelog 317 | 在 [CHANGELOG.md](https://github.com/Sam618/react-keep-alive/blob/master/CHANGELOG.md) 中能够查看到所有的更新. 318 | 319 | 320 | ## 📄 License 321 | React Keep Alive 使用了 [MIT](https://github.com/Sam618/react-keep-alive/blob/master/LICENSE) 许可. 322 | -------------------------------------------------------------------------------- /assets/basic-currently.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StructureBuilder/react-keep-alive/d8a3031c8d799f2cb9e8155a703ff20be8eb80c8/assets/basic-currently.gif -------------------------------------------------------------------------------- /assets/basic-original.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StructureBuilder/react-keep-alive/d8a3031c8d799f2cb9e8155a703ff20be8eb80c8/assets/basic-original.gif -------------------------------------------------------------------------------- /assets/lifecycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StructureBuilder/react-keep-alive/d8a3031c8d799f2cb9e8155a703ff20be8eb80c8/assets/lifecycle.png -------------------------------------------------------------------------------- /assets/react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StructureBuilder/react-keep-alive/d8a3031c8d799f2cb9e8155a703ff20be8eb80c8/assets/react-logo.png -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | react-keep-alive 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /demo/src/KeepAlive.backup.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {cloneDeep} from 'lodash'; 3 | import {getStateBackup, deepForceUpdateByBackup} from './utils'; 4 | 5 | let backup = null; 6 | 7 | const excludes = ['props', 'context', 'refs', 'updater', '_reactInternalFiber', '_reactInternalInstance']; 8 | 9 | function createBackupElement(node) { 10 | const ExtendsCtor = node.constructor.name === '__Ctor__' ? node.constructor.__Ctor__ : node.constructor; 11 | class __Ctor__ extends ExtendsCtor { 12 | static __Ctor__ = node.constructor; 13 | 14 | constructor(...args) { 15 | super(...args); 16 | Object.keys(node.instance).forEach(key => { 17 | if (~excludes.indexOf(key)) { 18 | return; 19 | } 20 | if (typeof node.instance[key] === 'function') { 21 | return; 22 | } 23 | this[key] = node.instance[key]; 24 | }); 25 | const oldRender = this.render; 26 | this.render = (...args) => { 27 | const element = oldRender.call(this, ...args); 28 | const node = this.props.__render__.child; 29 | function deep(element, node) { 30 | if (!element) { 31 | if (node.current) { 32 | node.current = null; 33 | node.restored = true; 34 | } 35 | return element; 36 | } 37 | 38 | if (typeof element.type === 'function') { 39 | if (node.current === null && node.restored) { 40 | return element; 41 | } 42 | if (node.current) { 43 | return node.current; 44 | } 45 | node.current = createBackupElement(node); 46 | return node.current; 47 | } 48 | 49 | if (element.props && Array.isArray(element.props.children)) { 50 | if (!node) { 51 | return element; 52 | } 53 | let nodeChild = node.child; 54 | return React.createElement(element.type, { 55 | children: React.Children.map(element.props.children, inner => { 56 | const result = deep(inner, nodeChild); 57 | if (result) { 58 | nodeChild = nodeChild.sibling; 59 | } 60 | return result; 61 | }) 62 | }); 63 | } 64 | 65 | return element; 66 | } 67 | return deep(element, node); 68 | }; 69 | } 70 | } 71 | return React.createElement(__Ctor__, { 72 | __render__: node, 73 | }); 74 | } 75 | 76 | export default class KeepAlive extends React.Component { 77 | __children__ = null; 78 | 79 | constructor(...args) { 80 | super(...args); 81 | this.__children__ = this.props.children; 82 | if (backup) { 83 | this.__children__ = createBackupElement(backup.child); 84 | } 85 | } 86 | 87 | componentWillUnmount() { 88 | backup = getStateBackup(cloneDeep(this._reactInternalFiber || this._reactInternalInstance)); 89 | } 90 | 91 | render() { 92 | return this.__children__; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /demo/src/KeepAlive.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {getStateBackup, deepForceUpdateByBackup} from './utils'; 3 | 4 | let backup = null; 5 | 6 | export default class KeepAlive extends React.Component { 7 | componentDidMount() { 8 | if (backup) { 9 | deepForceUpdateByBackup(this, backup); 10 | } 11 | } 12 | 13 | componentWillUnmount() { 14 | backup = getStateBackup(this); 15 | } 16 | 17 | render() { 18 | return this.props.children; 19 | } 20 | } 21 | 22 | 23 | -------------------------------------------------------------------------------- /demo/src/index.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { 4 | Switch, 5 | Route, 6 | Link, 7 | BrowserRouter as Router, 8 | } from 'react-router-dom'; 9 | import {Provider, KeepAlive} from '../../es'; 10 | import A from './views/A'; 11 | import B from './views/B'; 12 | import C from './views/C'; 13 | 14 | function App() { 15 | const [toggle, setToggle] = useState(true); 16 | return ( 17 |
18 |
    19 |
  • 20 | a 21 |
  • 22 |
  • setToggle(true)}> 23 | b 24 |
  • 25 |
  • setToggle(false)}> 26 | c 27 |
  • 28 |
29 | 30 |
31 | 32 |
33 | 34 | 35 | ( 38 | 39 | 40 | 41 | )} 42 | /> 43 | ( 46 | 47 | 48 | )} 49 | /> 50 | ( 53 | 54 | 55 | 56 | )} 57 | /> 58 | 59 |
60 | ); 61 | } 62 | 63 | ReactDOM.render( 64 | ( 65 | 66 | 67 | 68 | 69 | 70 | ), 71 | document.getElementById('root') 72 | ); 73 | -------------------------------------------------------------------------------- /demo/src/utils.js: -------------------------------------------------------------------------------- 1 | export function getStateBackup(instance) { 2 | const root = instance; 3 | let node = root 4 | const backupRoot = {}; 5 | let backupNode = backupRoot; 6 | while (true) { 7 | if (node.stateNode !== null && typeof node.type === 'function') { 8 | backupNode.state = node.stateNode.state 9 | backupNode.constructor = node.stateNode.constructor 10 | backupNode.instance = node.stateNode 11 | } 12 | if (node.child) { 13 | node.child.return = node 14 | node = node.child 15 | backupNode.child = { 16 | return: backupNode, 17 | }; 18 | backupNode = backupNode.child; 19 | continue 20 | } 21 | if (node === root) { 22 | return backupRoot 23 | } 24 | while (!node.sibling) { 25 | if (!node.return || node.return === root) { 26 | return backupRoot 27 | } 28 | node = node.return 29 | backupNode = backupNode.return; 30 | } 31 | node.sibling.return = node.return 32 | node = node.sibling 33 | backupNode.sibling = { 34 | return: backupNode.return, 35 | }; 36 | backupNode = backupNode.sibling; 37 | } 38 | } 39 | 40 | const excludes = ['props', 'state', 'context', 'refs', 'updater', '_reactInternalFiber', '_reactInternalInstance']; 41 | 42 | export function deepForceUpdateByBackup(instance, backup) { 43 | const root = instance._reactInternalFiber || instance._reactInternalInstance 44 | let node = root 45 | while (true) { 46 | if (node.stateNode !== null && typeof node.type === 'function') { 47 | const publicInstance = node.stateNode 48 | const { updater } = publicInstance 49 | // TODO: 50 | // 无法修改箭头函数的 this 指向 51 | Object.keys(backup.instance).forEach(key => { 52 | if (~excludes.indexOf(key)) { 53 | return 54 | } 55 | if (typeof backup.instance[key] === 'function') { 56 | return 57 | } 58 | publicInstance[key] = backup.instance[key] 59 | }); 60 | if (typeof publicInstance.setState === 'function') { 61 | publicInstance.setState(backup.state, () => { 62 | console.log(); 63 | }) 64 | } else if (updater && typeof updater.enqueueSetState === 'function') { 65 | updater.enqueueSetState(publicInstance, backup.state) 66 | } 67 | } 68 | if (node.child) { 69 | node.child.return = node 70 | node = node.child 71 | if (backup.child) { 72 | backup.child.return = backup 73 | backup = backup.child 74 | } 75 | continue 76 | } 77 | if (node === root) { 78 | return undefined 79 | } 80 | while (!node.sibling) { 81 | if (!node.return || node.return === root) { 82 | return undefined 83 | } 84 | node = node.return 85 | backup = backup.return 86 | } 87 | node.sibling.return = node.return 88 | node = node.sibling 89 | if (backup.sibling) { 90 | backup.sibling.return = backup.return 91 | backup = backup.sibling 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /demo/src/views/A.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect, useRef} from 'react'; 2 | import {useKeepAliveEffect} from '../../../es'; 3 | import B from './B'; 4 | 5 | function Test() { 6 | const [index, setIndex] = useState(0); 7 | const divRef = useRef(); 8 | useKeepAliveEffect(() => { 9 | console.log('activated', index); 10 | console.log(divRef.current.offsetWidth); 11 | const i = 0; 12 | 13 | return () => { 14 | console.log('unactivated', index, i); 15 | }; 16 | }); 17 | return ( 18 |
19 |
This is a.
20 | 21 |
22 | ); 23 | } 24 | 25 | export default Test; 26 | -------------------------------------------------------------------------------- /demo/src/views/B.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from "react-redux"; 3 | import {bindLifecycle} from '../../../es'; 4 | 5 | @bindLifecycle 6 | class B extends React.Component { 7 | componentWillMount() { 8 | console.log('B componentWillMount'); 9 | } 10 | 11 | componentDidMount() { 12 | console.log(this.ref.offsetWidth); 13 | console.log('B componentDidMount'); 14 | } 15 | 16 | componentDidActivate() { 17 | console.log('B componentDidActivate'); 18 | } 19 | 20 | componentWillUpdate() { 21 | console.log('B componentWillUpdate'); 22 | } 23 | 24 | componentDidUpdate() { 25 | console.log(this.ref.offsetWidth); 26 | console.log('B componentDidUpdate'); 27 | } 28 | 29 | componentWillUnactivate() { 30 | console.log('B componentWillUnactivate'); 31 | } 32 | 33 | componentWillUnmount() { 34 | console.log('B componentWillUnmount'); 35 | } 36 | 37 | render() { 38 | console.log(this); 39 | console.log('B render'); 40 | return ( 41 |
this.ref = ref}>This is b.
42 | ); 43 | } 44 | } 45 | 46 | export default B; 47 | -------------------------------------------------------------------------------- /demo/src/views/C.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {bindLifecycle} from '../../../es'; 3 | 4 | @bindLifecycle 5 | class C extends React.Component { 6 | state = { 7 | value: false, 8 | }; 9 | 10 | componentWillMount() { 11 | console.log('C componentWillMount'); 12 | } 13 | 14 | componentDidMount() { 15 | setTimeout(() => { 16 | this.setState({ 17 | value: true, 18 | }); 19 | }, 1000); 20 | console.log('C componentDidMount'); 21 | } 22 | 23 | componentDidActivate() { 24 | console.log('C componentDidActivate'); 25 | } 26 | 27 | componentWillUpdate() { 28 | console.log('C componentWillUpdate'); 29 | } 30 | 31 | componentDidUpdate() { 32 | console.log('C componentDidUpdate'); 33 | } 34 | 35 | componentWillUnactivate() { 36 | console.log('C componentWillUnactivate'); 37 | } 38 | 39 | componentWillUnmount() { 40 | console.log('C componentWillUnmount'); 41 | } 42 | 43 | render() { 44 | console.log('C render'); 45 | return ( 46 |
47 | {this.state.value ?
This is c.
: null} 48 |
49 | ); 50 | } 51 | } 52 | 53 | export default C; 54 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | verbose: true, 4 | testPathIgnorePatterns: [ 5 | '/node/modules', 6 | ], 7 | setupFiles: [ 8 | '/test/setup.js', 9 | ], 10 | transform: { 11 | "^.+\\.js$": "babel-jest", 12 | ".(ts|tsx)": "ts-jest", 13 | }, 14 | testURL: 'http://localhost:3002/', 15 | collectCoverage: true, 16 | coverageDirectory: './coverage', 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-keep-alive", 3 | "version": "2.5.2", 4 | "description": "Package will allow components to maintain their status, to avoid repeated re-rendering.", 5 | "author": "Shen Chang", 6 | "homepage": "https://github.com/StructureBuilder/react-keep-alive", 7 | "keywords": [ 8 | "react", 9 | "react-router", 10 | "keep-alive", 11 | "cache" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/StructureBuilder/react-keep-alive.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/StructureBuilder/react-keep-alive/issues" 19 | }, 20 | "main": "cjs/index.js", 21 | "module": "es/index.js", 22 | "scripts": { 23 | "clean:es": "rimraf es", 24 | "clean:cjs": "rimraf cjs", 25 | "test": "jest", 26 | "codecov": "codecov", 27 | "build:demo": "webpack", 28 | "build:es": "npm run clean:es && tsc", 29 | "build:cjs": "npm run clean:cjs && tsc --outDir cjs --module commonjs", 30 | "build:dist": "npm run build:es && npm run build:cjs", 31 | "publish:patch": "npm version patch && npm publish", 32 | "publish:minor": "npm version minor && npm publish", 33 | "publish:major": "npm version major && npm publish", 34 | "start:demo": "webpack-dev-server --hot --historyApiFallback", 35 | "start:es": "npm run clean:es && tsc -w -sourcemap --outDir es" 36 | }, 37 | "husky": { 38 | "hooks": { 39 | "pre-commit": "npm run test" 40 | } 41 | }, 42 | "license": "MIT", 43 | "sideEffects": false, 44 | "dependencies": { 45 | "@types/js-md5": "^0.4.2", 46 | "hoist-non-react-statics": "^3.3.0", 47 | "js-md5": "^0.7.3" 48 | }, 49 | "devDependencies": { 50 | "@babel/cli": "^7.2.3", 51 | "@babel/core": "^7.3.4", 52 | "@babel/plugin-proposal-class-properties": "^7.3.0", 53 | "@babel/plugin-proposal-decorators": "^7.3.0", 54 | "@babel/preset-env": "^7.3.4", 55 | "@babel/preset-react": "^7.0.0", 56 | "@types/hoist-non-react-statics": "^3.0.1", 57 | "@types/node": "^10.12.21", 58 | "@types/react": "^16.8.1", 59 | "@types/react-dom": "^16.8.2", 60 | "animate.css": "^3.7.0", 61 | "babel": "^6.23.0", 62 | "babel-loader": "^8.0.5", 63 | "codecov": "^3.2.0", 64 | "css-loader": "^2.1.1", 65 | "cz-conventional-changelog": "^2.1.0", 66 | "enzyme": "^3.8.0", 67 | "enzyme-adapter-react-16": "^1.9.1", 68 | "html-webpack-plugin": "^3.2.0", 69 | "husky": "^1.3.1", 70 | "jest": "^24.1.0", 71 | "react": "^16.8.5", 72 | "react-dom": "^16.8.5", 73 | "react-redux": "^7.0.3", 74 | "react-router-dom": "^5.0.0", 75 | "react-transition-group": "^4.0.1", 76 | "redux": "^4.0.1", 77 | "rimraf": "^2.6.3", 78 | "style-loader": "^0.23.1", 79 | "ts-jest": "^23.10.5", 80 | "typescript": "^3.3.1", 81 | "webpack": "^4.29.3", 82 | "webpack-cli": "^3.2.3", 83 | "webpack-dev-server": "^3.1.14" 84 | }, 85 | "peerDependencies": { 86 | "react": ">=16.3.0", 87 | "react-dom": ">=16.3.0", 88 | "react-router-dom": ">=5.0.0" 89 | }, 90 | "config": { 91 | "commitizen": { 92 | "path": "./node_modules/cz-conventional-changelog" 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/components/AsyncComponent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {bindLifecycleTypeName} from '../utils/bindLifecycle'; 3 | 4 | interface IProps { 5 | setMounted: (value: boolean) => void; 6 | getMounted: () => boolean; 7 | onUpdate: () => void; 8 | } 9 | 10 | interface IState { 11 | component: any; 12 | } 13 | 14 | export default class AsyncComponent extends React.Component { 15 | public state = { 16 | component: null, 17 | }; 18 | 19 | /** 20 | * Force update child nodes 21 | * 22 | * @private 23 | * @returns 24 | * @memberof AsyncComponent 25 | */ 26 | private forceUpdateChildren() { 27 | if (!this.props.children) { 28 | return; 29 | } 30 | const root: any = (this as any)._reactInternalFiber || (this as any)._reactInternalInstance; 31 | let node = root.child; 32 | let sibling = node; 33 | while (sibling) { 34 | while (true) { 35 | if (node.type && node.type.displayName && node.type.displayName.indexOf(bindLifecycleTypeName) !== -1) { 36 | return; 37 | } 38 | if (node.stateNode) { 39 | break; 40 | } 41 | node = node.child; 42 | } 43 | if (typeof node.type === 'function') { 44 | node.stateNode.forceUpdate(); 45 | } 46 | sibling = sibling.sibling; 47 | } 48 | } 49 | 50 | public componentDidMount() { 51 | const {children} = this.props; 52 | Promise.resolve().then(() => this.setState({component: children})); 53 | } 54 | 55 | public componentDidUpdate() { 56 | this.props.onUpdate(); 57 | } 58 | 59 | // Delayed update 60 | // In order to be able to get real DOM data 61 | public shouldComponentUpdate() { 62 | if (!this.state.component) { 63 | // If it is already mounted asynchronously, you don't need to do it again when you update it. 64 | this.props.setMounted(false); 65 | return true; 66 | } 67 | Promise.resolve().then(() => { 68 | if (this.props.getMounted()) { 69 | this.props.setMounted(false); 70 | this.forceUpdateChildren(); 71 | this.props.onUpdate(); 72 | } 73 | }); 74 | return false; 75 | } 76 | 77 | public render() { 78 | return this.state.component; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/components/Comment.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import noop from '../utils/noop'; 4 | 5 | interface IReactCommentProps { 6 | onLoaded: () => void; 7 | } 8 | 9 | class ReactComment extends React.PureComponent { 10 | public static defaultProps = { 11 | onLoaded: noop, 12 | }; 13 | 14 | private parentNode: Node; 15 | 16 | private currentNode: Element; 17 | 18 | private commentNode: Comment; 19 | 20 | private content: string; 21 | 22 | public componentDidMount() { 23 | const node = ReactDOM.findDOMNode(this) as Element; 24 | const commentNode = this.createComment(); 25 | this.commentNode = commentNode; 26 | this.currentNode = node; 27 | this.parentNode = node.parentNode as Node; 28 | this.parentNode.replaceChild(commentNode, node); 29 | ReactDOM.unmountComponentAtNode(node); 30 | this.props.onLoaded(); 31 | } 32 | 33 | public componentWillUnmount() { 34 | this.parentNode.replaceChild(this.currentNode, this.commentNode); 35 | } 36 | 37 | private createComment() { 38 | let content = this.props.children; 39 | if (typeof content !== 'string') { 40 | content = ''; 41 | } 42 | this.content = (content as string).trim(); 43 | return document.createComment(this.content); 44 | } 45 | 46 | public render() { 47 | return
; 48 | } 49 | } 50 | 51 | export default ReactComment; 52 | -------------------------------------------------------------------------------- /src/components/Consumer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Comment from './Comment'; 3 | import {LIFECYCLE, ICache, ICacheItem} from './Provider'; 4 | 5 | interface IConsumerProps { 6 | children: React.ReactNode; 7 | identification: string; 8 | keepAlive: boolean; 9 | cache: ICache; 10 | setCache: (identification: string, value: ICacheItem) => void; 11 | unactivate: (identification: string) => void; 12 | } 13 | 14 | class Consumer extends React.PureComponent { 15 | private renderElement: HTMLElement; 16 | 17 | private commentRef: any; 18 | 19 | private identification: string = this.props.identification; 20 | 21 | public componentDidMount() { 22 | const { 23 | setCache, 24 | children, 25 | keepAlive, 26 | } = this.props; 27 | this.renderElement = this.commentRef.parentNode; 28 | setCache(this.identification, { 29 | children, 30 | keepAlive, 31 | lifecycle: LIFECYCLE.MOUNTED, 32 | renderElement: this.renderElement, 33 | activated: true, 34 | }); 35 | } 36 | 37 | public componentDidUpdate() { 38 | const { 39 | setCache, 40 | children, 41 | keepAlive, 42 | } = this.props; 43 | setCache(this.identification, { 44 | children, 45 | keepAlive, 46 | lifecycle: LIFECYCLE.UPDATING, 47 | }); 48 | } 49 | 50 | public componentWillUnmount() { 51 | const {unactivate} = this.props; 52 | unactivate(this.identification); 53 | } 54 | 55 | public render() { 56 | const {identification} = this; 57 | return this.commentRef = ref}>{identification}; 58 | } 59 | } 60 | 61 | export default Consumer; 62 | -------------------------------------------------------------------------------- /src/components/KeepAlive.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import AsyncComponent from './AsyncComponent'; 3 | import {START_MOUNTING_DOM, LIFECYCLE} from './Provider'; 4 | import keepAlive, {COMMAND} from '../utils/keepAliveDecorator'; 5 | import changePositionByComment from '../utils/changePositionByComment'; 6 | 7 | interface IKeepAliveProps { 8 | key?: string; 9 | name?: string; 10 | disabled?: boolean; 11 | extra?: any; 12 | } 13 | 14 | interface IKeepAliveInnerProps extends IKeepAliveProps { 15 | _container: any; 16 | } 17 | 18 | class KeepAlive extends React.PureComponent { 19 | private bindUnmount: (() => void) | null = null; 20 | 21 | private bindUnactivate: (() => void) | null = null; 22 | 23 | private unmounted = false; 24 | 25 | private mounted = false; 26 | 27 | private ref: null | Element = null; 28 | 29 | private refNextSibling: null | Node = null; 30 | 31 | private childNodes: Node[] = []; 32 | 33 | public componentDidMount() { 34 | const { 35 | _container, 36 | } = this.props; 37 | const { 38 | notNeedActivate, 39 | identification, 40 | eventEmitter, 41 | keepAlive, 42 | } = _container; 43 | notNeedActivate(); 44 | const cb = () => { 45 | this.mount(); 46 | this.listen(); 47 | eventEmitter.off([identification, START_MOUNTING_DOM], cb); 48 | }; 49 | eventEmitter.on([identification, START_MOUNTING_DOM], cb); 50 | if (keepAlive) { 51 | this.componentDidActivate(); 52 | } 53 | } 54 | 55 | public componentDidActivate() { 56 | // tslint-disable 57 | } 58 | 59 | public componentDidUpdate() { 60 | const { 61 | _container, 62 | } = this.props; 63 | const { 64 | notNeedActivate, 65 | isNeedActivate, 66 | } = _container; 67 | if (isNeedActivate()) { 68 | notNeedActivate(); 69 | this.mount(); 70 | this.listen(); 71 | this.unmounted = false; 72 | this.componentDidActivate(); 73 | } 74 | } 75 | 76 | public componentWillUnactivate() { 77 | this.unmount(); 78 | this.unlisten(); 79 | } 80 | 81 | public componentWillUnmount() { 82 | if (!this.unmounted) { 83 | this.unmounted = true; 84 | this.unmount(); 85 | this.unlisten(); 86 | } 87 | } 88 | 89 | private mount() { 90 | const { 91 | _container: { 92 | cache, 93 | identification, 94 | storeElement, 95 | setLifecycle, 96 | }, 97 | } = this.props; 98 | this.setMounted(true); 99 | const {renderElement} = cache[identification]; 100 | setLifecycle(LIFECYCLE.UPDATING); 101 | changePositionByComment(identification, renderElement, storeElement); 102 | } 103 | 104 | private correctionPosition = () => { 105 | if (this.ref && this.ref.parentNode && this.ref.nextSibling) { 106 | const childNodes = this.ref.childNodes as any; 107 | this.refNextSibling = this.ref.nextSibling; 108 | this.childNodes = []; 109 | while (childNodes.length) { 110 | const child = childNodes[0]; 111 | this.childNodes.push(child); 112 | this.ref.parentNode.insertBefore(child, this.ref.nextSibling); 113 | } 114 | this.ref.parentNode.removeChild(this.ref); 115 | } 116 | } 117 | 118 | private retreatPosition = () => { 119 | if (this.ref && this.refNextSibling && this.refNextSibling.parentNode) { 120 | for (const child of this.childNodes) { 121 | this.ref.appendChild(child); 122 | } 123 | this.refNextSibling.parentNode.insertBefore(this.ref, this.refNextSibling); 124 | } 125 | } 126 | 127 | private unmount() { 128 | const { 129 | _container: { 130 | identification, 131 | storeElement, 132 | cache, 133 | setLifecycle, 134 | }, 135 | } = this.props; 136 | const {renderElement, ifStillActivate, reactivate} = cache[identification]; 137 | setLifecycle(LIFECYCLE.UNMOUNTED); 138 | this.retreatPosition(); 139 | changePositionByComment(identification, storeElement, renderElement); 140 | if (ifStillActivate) { 141 | reactivate(); 142 | } 143 | } 144 | 145 | private listen() { 146 | const { 147 | _container: { 148 | identification, 149 | eventEmitter, 150 | }, 151 | } = this.props; 152 | eventEmitter.on( 153 | [identification, COMMAND.CURRENT_UNMOUNT], 154 | this.bindUnmount = this.componentWillUnmount.bind(this), 155 | ); 156 | eventEmitter.on( 157 | [identification, COMMAND.CURRENT_UNACTIVATE], 158 | this.bindUnactivate = this.componentWillUnactivate.bind(this), 159 | ); 160 | } 161 | 162 | private unlisten() { 163 | const { 164 | _container: { 165 | identification, 166 | eventEmitter, 167 | }, 168 | } = this.props; 169 | eventEmitter.off([identification, COMMAND.CURRENT_UNMOUNT], this.bindUnmount); 170 | eventEmitter.off([identification, COMMAND.CURRENT_UNACTIVATE], this.bindUnactivate); 171 | } 172 | 173 | private setMounted = (value: boolean) => { 174 | this.mounted = value; 175 | } 176 | 177 | private getMounted = () => { 178 | return this.mounted; 179 | } 180 | 181 | public render() { 182 | // The purpose of this div is to not report an error when moving the DOM, 183 | // so you need to remove this div later. 184 | return ( 185 |
this.ref = ref}> 186 | 191 | {this.props.children} 192 | 193 |
194 | ); 195 | } 196 | } 197 | 198 | export default keepAlive(KeepAlive); 199 | -------------------------------------------------------------------------------- /src/components/Provider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import Comment from './Comment'; 4 | import KeepAliveContext from '../contexts/KeepAliveContext'; 5 | import createEventEmitter from '../utils/createEventEmitter'; 6 | import createUniqueIdentification from '../utils/createUniqueIdentification'; 7 | import createStoreElement from '../utils/createStoreElement'; 8 | 9 | export const keepAliveProviderTypeName = '$$KeepAliveProvider'; 10 | export const START_MOUNTING_DOM = 'startMountingDOM'; 11 | 12 | export enum LIFECYCLE { 13 | MOUNTED, 14 | UPDATING, 15 | UNMOUNTED, 16 | } 17 | 18 | export interface ICacheItem { 19 | children: React.ReactNode; 20 | keepAlive: boolean; 21 | lifecycle: LIFECYCLE; 22 | renderElement?: HTMLElement; 23 | activated?: boolean; 24 | ifStillActivate?: boolean; 25 | reactivate?: () => void; 26 | } 27 | 28 | export interface ICache { 29 | [key: string]: ICacheItem; 30 | } 31 | 32 | export interface IKeepAliveProviderImpl { 33 | storeElement: HTMLElement; 34 | cache: ICache; 35 | keys: string[]; 36 | eventEmitter: any; 37 | existed: boolean; 38 | providerIdentification: string; 39 | setCache: (identification: string, value: ICacheItem) => void; 40 | unactivate: (identification: string) => void; 41 | isExisted: () => boolean; 42 | } 43 | 44 | export interface IKeepAliveProviderProps { 45 | include?: string | string[] | RegExp; 46 | exclude?: string | string[] | RegExp; 47 | max?: number; 48 | } 49 | 50 | export default class KeepAliveProvider extends React.PureComponent implements IKeepAliveProviderImpl { 51 | public static displayName = keepAliveProviderTypeName; 52 | 53 | public static defaultProps = { 54 | max: 10, 55 | }; 56 | 57 | public storeElement: HTMLElement; 58 | 59 | // Sometimes data that changes with setState cannot be synchronized, so force refresh 60 | public cache: ICache = Object.create(null); 61 | 62 | public keys: string[] = []; 63 | 64 | public eventEmitter = createEventEmitter(); 65 | 66 | public existed: boolean = true; 67 | 68 | private needRerender: boolean = false; 69 | 70 | public providerIdentification: string = createUniqueIdentification(); 71 | 72 | public componentDidMount() { 73 | this.storeElement = createStoreElement(); 74 | this.forceUpdate(); 75 | } 76 | 77 | public componentDidUpdate() { 78 | if (this.needRerender) { 79 | this.needRerender = false; 80 | this.forceUpdate(); 81 | } 82 | } 83 | 84 | public componentWillUnmount() { 85 | this.existed = false; 86 | document.body.removeChild(this.storeElement); 87 | } 88 | 89 | public isExisted = () => { 90 | return this.existed; 91 | } 92 | 93 | public setCache = (identification: string, value: ICacheItem) => { 94 | const {cache, keys} = this; 95 | const {max} = this.props; 96 | const currentCache = cache[identification]; 97 | if (!currentCache) { 98 | keys.push(identification); 99 | } 100 | this.cache[identification] = { 101 | ...currentCache, 102 | ...value, 103 | }; 104 | this.forceUpdate(() => { 105 | // If the maximum value is set, the value in the cache is deleted after it goes out. 106 | if (currentCache) { 107 | return; 108 | } 109 | if (!max) { 110 | return; 111 | } 112 | const difference = keys.length - (max as number); 113 | if (difference <= 0) { 114 | return; 115 | } 116 | const spliceKeys = keys.splice(0, difference); 117 | this.forceUpdate(() => { 118 | spliceKeys.forEach(key => { 119 | delete cache[key as string]; 120 | }); 121 | }); 122 | }); 123 | } 124 | 125 | public unactivate = (identification: string) => { 126 | const {cache} = this; 127 | this.cache[identification] = { 128 | ...cache[identification], 129 | activated: false, 130 | lifecycle: LIFECYCLE.UNMOUNTED, 131 | }; 132 | this.forceUpdate(); 133 | } 134 | 135 | private startMountingDOM = (identification: string) => { 136 | this.eventEmitter.emit([identification, START_MOUNTING_DOM]); 137 | } 138 | 139 | public render() { 140 | const { 141 | cache, 142 | keys, 143 | providerIdentification, 144 | isExisted, 145 | setCache, 146 | existed, 147 | unactivate, 148 | storeElement, 149 | eventEmitter, 150 | } = this; 151 | const { 152 | children: innerChildren, 153 | include, 154 | exclude, 155 | } = this.props; 156 | if (!storeElement) { 157 | return null; 158 | } 159 | return ( 160 | 175 | 176 | {innerChildren} 177 | {ReactDOM.createPortal( 178 | keys.map(identification => { 179 | const currentCache = cache[identification]; 180 | const { 181 | keepAlive, 182 | children, 183 | lifecycle, 184 | } = currentCache; 185 | let cacheChildren = children; 186 | if (lifecycle === LIFECYCLE.MOUNTED && !keepAlive) { 187 | // If the cache was last enabled, then the components of this keepAlive package are used, 188 | // and the cache is not enabled, the UI needs to be reset. 189 | cacheChildren = null; 190 | this.needRerender = true; 191 | currentCache.lifecycle = LIFECYCLE.UPDATING; 192 | } 193 | // current true, previous true | undefined, keepAlive false, not cache 194 | // current true, previous true | undefined, keepAlive true, cache 195 | 196 | // current true, previous false, keepAlive true, cache 197 | // current true, previous false, keepAlive false, not cache 198 | return ( 199 | cacheChildren 200 | ? ( 201 | 202 | {identification} 203 | {cacheChildren} 204 | this.startMountingDOM(identification)} 206 | >{identification} 207 | 208 | ) 209 | : null 210 | ); 211 | }), 212 | storeElement 213 | )} 214 | 215 | 216 | ); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/contexts/IdentificationContext.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export interface IIdentificationContextProps { 4 | identification: string; 5 | eventEmitter: any; 6 | keepAlive: boolean; 7 | getLifecycle: () => number; 8 | isExisted: () => boolean; 9 | activated: boolean; 10 | extra: any; 11 | } 12 | 13 | const WithKeepAliveContext = React.createContext({} as any); 14 | 15 | export default WithKeepAliveContext; 16 | -------------------------------------------------------------------------------- /src/contexts/KeepAliveContext.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {IKeepAliveProviderImpl, IKeepAliveProviderProps} from '../components/Provider'; 3 | 4 | export type IKeepAliveContextProps = IKeepAliveProviderImpl & IKeepAliveProviderProps; 5 | 6 | const KeepAliveContext = React.createContext({} as any); 7 | 8 | export default KeepAliveContext; 9 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Provider from './components/Provider'; 2 | import KeepAlive from './components/KeepAlive'; 3 | import bindLifecycle from './utils/bindLifecycle'; 4 | import useKeepAliveEffect from './utils/useKeepAliveEffect'; 5 | 6 | export { 7 | Provider, 8 | KeepAlive, 9 | bindLifecycle, 10 | useKeepAliveEffect, 11 | }; 12 | -------------------------------------------------------------------------------- /src/utils/bindLifecycle.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import hoistNonReactStatics from 'hoist-non-react-statics'; 3 | import noop from './noop'; 4 | import {warn} from './debug'; 5 | import {COMMAND} from './keepAliveDecorator'; 6 | import withIdentificationContextConsumer from './withIdentificationContextConsumer'; 7 | import getDisplayName from './getDisplayName'; 8 | 9 | export const bindLifecycleTypeName = '$$bindLifecycle'; 10 | 11 | export default function bindLifecycle

(Component: React.ComponentClass

) { 12 | const WrappedComponent = (Component as any).WrappedComponent || (Component as any).wrappedComponent || Component; 13 | 14 | const { 15 | componentDidMount = noop, 16 | componentDidUpdate = noop, 17 | componentDidActivate = noop, 18 | componentWillUnactivate = noop, 19 | componentWillUnmount = noop, 20 | shouldComponentUpdate = noop, 21 | } = WrappedComponent.prototype; 22 | 23 | WrappedComponent.prototype.componentDidMount = function () { 24 | componentDidMount.call(this); 25 | this._needActivate = false; 26 | const { 27 | _container: { 28 | identification, 29 | eventEmitter, 30 | activated, 31 | }, 32 | keepAlive, 33 | } = this.props; 34 | // Determine whether to execute the componentDidActivate life cycle of the current component based on the activation state of the KeepAlive components 35 | if (!activated && keepAlive !== false) { 36 | componentDidActivate.call(this); 37 | } 38 | eventEmitter.on( 39 | [identification, COMMAND.ACTIVATE], 40 | this._bindActivate = () => this._needActivate = true, 41 | true, 42 | ); 43 | eventEmitter.on( 44 | [identification, COMMAND.UNACTIVATE], 45 | this._bindUnactivate = () => { 46 | componentWillUnactivate.call(this); 47 | this._unmounted = false; 48 | }, 49 | true, 50 | ); 51 | eventEmitter.on( 52 | [identification, COMMAND.UNMOUNT], 53 | this._bindUnmount = () => { 54 | componentWillUnmount.call(this); 55 | this._unmounted = true; 56 | }, 57 | true, 58 | ); 59 | }; 60 | 61 | // In order to be able to re-update after transferring the DOM, we need to block the first update. 62 | WrappedComponent.prototype.shouldComponentUpdate = function (...args: any) { 63 | if (this._needActivate) { 64 | this.forceUpdate(); 65 | return false; 66 | } 67 | return shouldComponentUpdate.call(this, ...args) || true; 68 | }; 69 | 70 | WrappedComponent.prototype.componentDidUpdate = function (...args: any) { 71 | componentDidUpdate.call(this, ...args); 72 | if (this._needActivate) { 73 | this._needActivate = false; 74 | componentDidActivate.call(this); 75 | } 76 | }; 77 | WrappedComponent.prototype.componentWillUnmount = function () { 78 | if (!this._unmounted) { 79 | componentWillUnmount.call(this); 80 | } 81 | const { 82 | _container: { 83 | identification, 84 | eventEmitter, 85 | }, 86 | } = this.props; 87 | eventEmitter.off( 88 | [identification, COMMAND.ACTIVATE], 89 | this._bindActivate, 90 | ); 91 | eventEmitter.off( 92 | [identification, COMMAND.UNACTIVATE], 93 | this._bindUnactivate, 94 | ); 95 | eventEmitter.off( 96 | [identification, COMMAND.UNMOUNT], 97 | this._bindUnmount, 98 | ); 99 | }; 100 | 101 | const BindLifecycleHOC = withIdentificationContextConsumer( 102 | ({ 103 | forwardRef, 104 | _identificationContextProps: { 105 | identification, 106 | eventEmitter, 107 | activated, 108 | keepAlive, 109 | extra, 110 | }, 111 | ...wrapperProps 112 | }) => { 113 | if (!identification) { 114 | warn('[React Keep Alive] You should not use bindLifecycle outside a .'); 115 | return null; 116 | } 117 | return ( 118 | 129 | ); 130 | }, 131 | ); 132 | const BindLifecycle = React.forwardRef((props: P, ref) => ( 133 | 134 | )); 135 | 136 | (BindLifecycle as any).WrappedComponent = WrappedComponent; 137 | BindLifecycle.displayName = `${bindLifecycleTypeName}(${getDisplayName(Component)})`; 138 | return hoistNonReactStatics( 139 | BindLifecycle, 140 | Component, 141 | ) as any; 142 | } 143 | -------------------------------------------------------------------------------- /src/utils/changePositionByComment.ts: -------------------------------------------------------------------------------- 1 | enum NODE_TYPES { 2 | ELEMENT = 1, 3 | COMMENT = 8, 4 | } 5 | 6 | function findElementsBetweenComments(node: Node, identification: string): Node[] { 7 | const elements = []; 8 | const childNodes = node.childNodes as any; 9 | let startCommentExist = false; 10 | for (const child of childNodes) { 11 | if ( 12 | child.nodeType === NODE_TYPES.COMMENT && 13 | child.nodeValue.trim() === identification && 14 | !startCommentExist 15 | ) { 16 | startCommentExist = true; 17 | } else if (startCommentExist && child.nodeType === NODE_TYPES.ELEMENT) { 18 | elements.push(child); 19 | } else if (child.nodeType === NODE_TYPES.COMMENT && startCommentExist) { 20 | return elements; 21 | } 22 | } 23 | return elements; 24 | } 25 | 26 | function findComment(node: Node, identification: string): Node | undefined { 27 | const childNodes = node.childNodes as any; 28 | for (const child of childNodes) { 29 | if ( 30 | child.nodeType === NODE_TYPES.COMMENT && 31 | child.nodeValue.trim() === identification 32 | ) { 33 | return child; 34 | } 35 | } 36 | } 37 | 38 | export default function changePositionByComment(identification: string, presentParentNode: Node, originalParentNode: Node) { 39 | if (!presentParentNode || !originalParentNode) { 40 | return; 41 | } 42 | const elementNodes = findElementsBetweenComments(originalParentNode, identification); 43 | const commentNode = findComment(presentParentNode, identification); 44 | if (!elementNodes.length || !commentNode) { 45 | return; 46 | } 47 | elementNodes.push(elementNodes[elementNodes.length - 1].nextSibling as Node); 48 | elementNodes.unshift(elementNodes[0].previousSibling as Node); 49 | // Deleting comment elements when using commet components will result in component uninstallation errors 50 | for (let i = elementNodes.length - 1; i >= 0; i--) { 51 | presentParentNode.insertBefore(elementNodes[i], commentNode); 52 | } 53 | originalParentNode.appendChild(commentNode); 54 | } 55 | -------------------------------------------------------------------------------- /src/utils/createEventEmitter.ts: -------------------------------------------------------------------------------- 1 | import {warn} from './debug'; 2 | 3 | type EventNames = string | string[]; 4 | 5 | type Listener = (...args: any) => void; 6 | 7 | export default function createEventEmitter() { 8 | let events = Object.create(null); 9 | 10 | function on(eventNames: EventNames, listener: Listener, direction = false) { 11 | eventNames = getEventNames(eventNames); 12 | let current = events; 13 | const maxIndex = eventNames.length - 1; 14 | for (let i = 0; i < eventNames.length; i++) { 15 | const key = eventNames[i]; 16 | if (!current[key]) { 17 | current[key] = i === maxIndex ? [] : {}; 18 | } 19 | current = current[key]; 20 | } 21 | if (!Array.isArray(current)) { 22 | warn('[React Keep Alive] Access path error.'); 23 | } 24 | if (direction) { 25 | current.unshift(listener); 26 | } else { 27 | current.push(listener); 28 | } 29 | } 30 | 31 | function off(eventNames: EventNames, listener: Listener) { 32 | const listeners = getListeners(eventNames); 33 | if (!listeners) { 34 | return; 35 | } 36 | const matchIndex = listeners.findIndex((v: Listener) => v === listener); 37 | if (matchIndex !== -1) { 38 | listeners.splice(matchIndex, 1); 39 | } 40 | } 41 | 42 | function removeAllListeners(eventNames: EventNames) { 43 | const listeners = getListeners(eventNames); 44 | if (!listeners) { 45 | return; 46 | } 47 | eventNames = getEventNames(eventNames); 48 | const lastEventName = eventNames.pop(); 49 | if (lastEventName) { 50 | const event = eventNames.reduce((obj, key) => obj[key], events); 51 | event[lastEventName] = []; 52 | } 53 | } 54 | 55 | function emit(eventNames: EventNames, ...args: any) { 56 | const listeners = getListeners(eventNames); 57 | if (!listeners) { 58 | return; 59 | } 60 | for (const listener of listeners) { 61 | if (listener) { 62 | listener(...args); 63 | } 64 | } 65 | } 66 | 67 | function listenerCount(eventNames: EventNames) { 68 | const listeners = getListeners(eventNames); 69 | return listeners ? listeners.length : 0; 70 | } 71 | 72 | function clear() { 73 | events = Object.create(null); 74 | } 75 | 76 | function getListeners(eventNames: EventNames): Listener[] | undefined { 77 | eventNames = getEventNames(eventNames); 78 | try { 79 | return eventNames.reduce((obj, key) => obj[key], events); 80 | } catch (e) { 81 | return; 82 | } 83 | } 84 | 85 | function getEventNames(eventNames: EventNames): string[] { 86 | if (!eventNames) { 87 | warn('[React Keep Alive] Must exist event name.'); 88 | } 89 | if (typeof eventNames === 'string') { 90 | eventNames = [eventNames]; 91 | } 92 | return eventNames; 93 | } 94 | 95 | return { 96 | on, 97 | off, 98 | emit, 99 | clear, 100 | listenerCount, 101 | removeAllListeners, 102 | }; 103 | } 104 | -------------------------------------------------------------------------------- /src/utils/createStoreElement.ts: -------------------------------------------------------------------------------- 1 | import {prefix} from './createUniqueIdentification'; 2 | 3 | export default function createStoreElement(): HTMLElement { 4 | const keepAliveDOM = document.createElement('div'); 5 | keepAliveDOM.dataset.type = prefix; 6 | keepAliveDOM.style.display = 'none'; 7 | document.body.appendChild(keepAliveDOM); 8 | return keepAliveDOM; 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/createUniqueIdentification.ts: -------------------------------------------------------------------------------- 1 | const hexDigits = '0123456789abcdef'; 2 | 3 | export const prefix = 'keep-alive'; 4 | 5 | /** 6 | * Create UUID 7 | * Reference: https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript 8 | * @export 9 | * @returns 10 | */ 11 | export default function createUniqueIdentification(length = 6) { 12 | const strings = []; 13 | for (let i = 0; i < length; i++) { 14 | strings[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1); 15 | } 16 | return `${prefix}-${strings.join('')}`; 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/debug.ts: -------------------------------------------------------------------------------- 1 | type Warn = (message?: string) => void; 2 | 3 | export let warn: Warn = () => undefined; 4 | 5 | if (process.env.NODE_ENV !== 'production') { 6 | /** 7 | * Prints a warning in the console if it exists. 8 | * 9 | * @param {*} message 10 | */ 11 | warn = message => { 12 | if (typeof console !== undefined && typeof console.error === 'function') { 13 | console.error(message); 14 | } else { 15 | throw new Error(message); 16 | } 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/getDisplayName.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default function getDisplayName(Component: React.ComponentType) { 4 | return Component.displayName || Component.name || 'Component'; 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/getKeepAlive.ts: -------------------------------------------------------------------------------- 1 | import isRegExp from './isRegExp'; 2 | 3 | type Pattern = string | string[] | RegExp; 4 | 5 | function matches (pattern: Pattern, name: string) { 6 | if (Array.isArray(pattern)) { 7 | return pattern.indexOf(name) > -1; 8 | } else if (typeof pattern === 'string') { 9 | return pattern.split(',').indexOf(name) > -1; 10 | } else if (isRegExp(pattern)) { 11 | return pattern.test(name); 12 | } 13 | return false; 14 | } 15 | 16 | export default function getKeepAlive( 17 | name: string, 18 | include?: Pattern, 19 | exclude?: Pattern, 20 | disabled?: boolean 21 | ) { 22 | if (disabled !== undefined) { 23 | return !disabled; 24 | } 25 | if ( 26 | (include && (!name || !matches(include, name))) || 27 | (exclude && name && matches(exclude, name)) 28 | ) { 29 | return false; 30 | } 31 | return true; 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/getKeyByFiberNode.ts: -------------------------------------------------------------------------------- 1 | import {WithKeepAliveContextConsumerDisplayName} from './withKeepAliveContextConsumer'; 2 | 3 | export default function getKeyByFiberNode(fiberNode: any): string | null { 4 | if (!fiberNode) { 5 | return null; 6 | } 7 | const { 8 | key, 9 | type, 10 | } = fiberNode; 11 | if (type.displayName && type.displayName.indexOf(WithKeepAliveContextConsumerDisplayName) !== -1) { 12 | return key; 13 | } 14 | return getKeyByFiberNode(fiberNode.return); 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/isRegExp.ts: -------------------------------------------------------------------------------- 1 | export default function isRegExp(value: RegExp) { 2 | return value && Object.prototype.toString.call(value) === '[object RegExp]'; 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/keepAliveDecorator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import hoistNonReactStatics from 'hoist-non-react-statics'; 3 | import IdentificationContext from '../contexts/IdentificationContext'; 4 | import Consumer from '../components/Consumer'; 5 | import {LIFECYCLE} from '../components/Provider'; 6 | import md5 from './md5'; 7 | import {warn} from './debug'; 8 | import getKeyByFiberNode from './getKeyByFiberNode'; 9 | import withIdentificationContextConsumer, {IIdentificationContextConsumerComponentProps} from './withIdentificationContextConsumer'; 10 | import withKeepAliveContextConsumer, {IKeepAliveContextConsumerComponentProps} from './withKeepAliveContextConsumer'; 11 | import shallowEqual from './shallowEqual'; 12 | import getKeepAlive from './getKeepAlive'; 13 | 14 | export enum COMMAND { 15 | UNACTIVATE = 'unactivate', 16 | UNMOUNT = 'unmount', 17 | ACTIVATE = 'activate', 18 | CURRENT_UNMOUNT = 'current_unmount', 19 | CURRENT_UNACTIVATE = 'current_unactivate', 20 | } 21 | 22 | interface IListenUpperKeepAliveContainerProps extends IIdentificationContextConsumerComponentProps, IKeepAliveContextConsumerComponentProps { 23 | disabled?: boolean; 24 | name?: string; 25 | } 26 | 27 | interface IListenUpperKeepAliveContainerState { 28 | activated: boolean; 29 | } 30 | 31 | interface ITriggerLifecycleContainerProps extends IKeepAliveContextConsumerComponentProps { 32 | propKey: string; 33 | extra?: any; 34 | keepAlive: boolean; 35 | getCombinedKeepAlive: () => boolean; 36 | } 37 | 38 | /** 39 | * Decorating the component, the main function is to listen to events emitted by the upper component, triggering events of the current component. 40 | * 41 | * @export 42 | * @template P 43 | * @param {React.ComponentType} Component 44 | * @returns {React.ComponentType

} 45 | */ 46 | export default function keepAliveDecorator

(Component: React.ComponentType): React.ComponentType

{ 47 | class TriggerLifecycleContainer extends React.PureComponent { 48 | private identification: string; 49 | 50 | private activated = false; 51 | 52 | private ifStillActivate = false; 53 | 54 | // Let the lifecycle of the cached component be called normally. 55 | private needActivate = true; 56 | 57 | private lifecycle = LIFECYCLE.MOUNTED; 58 | 59 | constructor(props: ITriggerLifecycleContainerProps, ...args: any) { 60 | super(props, ...args); 61 | const { 62 | _keepAliveContextProps: { 63 | cache, 64 | }, 65 | } = props; 66 | if (!cache) { 67 | warn('[React Keep Alive] You should not use outside a .'); 68 | } 69 | } 70 | 71 | public componentDidMount() { 72 | if (!this.ifStillActivate) { 73 | this.activate(); 74 | } 75 | const { 76 | keepAlive, 77 | _keepAliveContextProps: { 78 | eventEmitter, 79 | }, 80 | } = this.props; 81 | if (keepAlive) { 82 | this.needActivate = true; 83 | eventEmitter.emit([this.identification, COMMAND.ACTIVATE]); 84 | } 85 | } 86 | 87 | public componentDidCatch() { 88 | if (!this.activated) { 89 | this.activate(); 90 | } 91 | } 92 | 93 | public componentWillUnmount() { 94 | const { 95 | getCombinedKeepAlive, 96 | _keepAliveContextProps: { 97 | eventEmitter, 98 | isExisted, 99 | }, 100 | } = this.props; 101 | const keepAlive = getCombinedKeepAlive(); 102 | if (!keepAlive || !isExisted()) { 103 | eventEmitter.emit([this.identification, COMMAND.CURRENT_UNMOUNT]); 104 | eventEmitter.emit([this.identification, COMMAND.UNMOUNT]); 105 | } 106 | // When the Provider components are unmounted, the cache is not needed, 107 | // so you don't have to execute the componentWillUnactivate lifecycle. 108 | if (keepAlive && isExisted()) { 109 | eventEmitter.emit([this.identification, COMMAND.CURRENT_UNACTIVATE]); 110 | eventEmitter.emit([this.identification, COMMAND.UNACTIVATE]); 111 | } 112 | } 113 | 114 | private activate = () => { 115 | this.activated = true; 116 | } 117 | 118 | private reactivate = () => { 119 | this.ifStillActivate = false; 120 | this.forceUpdate(); 121 | } 122 | 123 | private isNeedActivate = () => { 124 | return this.needActivate; 125 | } 126 | 127 | private notNeedActivate = () => { 128 | this.needActivate = false; 129 | } 130 | 131 | private getLifecycle = () => { 132 | return this.lifecycle; 133 | } 134 | 135 | private setLifecycle = (lifecycle: LIFECYCLE) => { 136 | this.lifecycle = lifecycle; 137 | } 138 | 139 | public render() { 140 | const { 141 | propKey, 142 | keepAlive, 143 | extra, 144 | getCombinedKeepAlive, 145 | _keepAliveContextProps: { 146 | isExisted, 147 | storeElement, 148 | cache, 149 | eventEmitter, 150 | setCache, 151 | unactivate, 152 | providerIdentification, 153 | }, 154 | ...wrapperProps 155 | } = this.props; 156 | if (!this.identification) { 157 | // We need to generate a corresponding unique identifier based on the information of the component. 158 | this.identification = md5( 159 | `${providerIdentification}${propKey}`, 160 | ); 161 | // The last activated component must be unactivated before it can be activated again. 162 | const currentCache = cache[this.identification]; 163 | if (currentCache) { 164 | this.ifStillActivate = currentCache.activated as boolean; 165 | currentCache.ifStillActivate = this.ifStillActivate; 166 | currentCache.reactivate = this.reactivate; 167 | } 168 | } 169 | const { 170 | isNeedActivate, 171 | notNeedActivate, 172 | activated, 173 | getLifecycle, 174 | setLifecycle, 175 | identification, 176 | ifStillActivate, 177 | } = this; 178 | return !ifStillActivate 179 | ? ( 180 | 187 | 198 | 211 | 212 | 213 | ) 214 | : null; 215 | } 216 | } 217 | 218 | class ListenUpperKeepAliveContainer extends React.Component { 219 | private combinedKeepAlive: boolean; 220 | 221 | public state = { 222 | activated: true, 223 | }; 224 | 225 | private activate: () => void; 226 | 227 | private unactivate: () => void; 228 | 229 | private unmount: () => void; 230 | 231 | public shouldComponentUpdate(nextProps: IListenUpperKeepAliveContainerProps, nextState: IListenUpperKeepAliveContainerState) { 232 | if (this.state.activated !== nextState.activated) { 233 | return true; 234 | } 235 | const { 236 | _keepAliveContextProps, 237 | _identificationContextProps, 238 | ...rest 239 | } = this.props; 240 | const { 241 | _keepAliveContextProps: nextKeepAliveContextProps, 242 | _identificationContextProps: nextIdentificationContextProps, 243 | ...nextRest 244 | } = nextProps; 245 | if (!shallowEqual(rest, nextRest)) { 246 | return true; 247 | } 248 | if ( 249 | !shallowEqual(_keepAliveContextProps, nextKeepAliveContextProps) || 250 | !shallowEqual(_identificationContextProps, nextIdentificationContextProps) 251 | ) { 252 | return true; 253 | } 254 | return false; 255 | } 256 | 257 | public componentDidMount() { 258 | this.listenUpperKeepAlive(); 259 | } 260 | 261 | public componentWillUnmount() { 262 | this.unlistenUpperKeepAlive(); 263 | } 264 | 265 | private listenUpperKeepAlive() { 266 | const {identification, eventEmitter} = this.props._identificationContextProps; 267 | if (!identification) { 268 | return; 269 | } 270 | eventEmitter.on( 271 | [identification, COMMAND.ACTIVATE], 272 | this.activate = () => this.setState({activated: true}), 273 | true, 274 | ); 275 | eventEmitter.on( 276 | [identification, COMMAND.UNACTIVATE], 277 | this.unactivate = () => this.setState({activated: false}), 278 | true, 279 | ); 280 | eventEmitter.on( 281 | [identification, COMMAND.UNMOUNT], 282 | this.unmount = () => this.setState({activated: false}), 283 | true, 284 | ); 285 | } 286 | 287 | private unlistenUpperKeepAlive() { 288 | const {identification, eventEmitter} = this.props._identificationContextProps; 289 | if (!identification) { 290 | return; 291 | } 292 | eventEmitter.off([identification, COMMAND.ACTIVATE], this.activate); 293 | eventEmitter.off([identification, COMMAND.UNACTIVATE], this.unactivate); 294 | eventEmitter.off([identification, COMMAND.UNMOUNT], this.unmount); 295 | } 296 | 297 | private getCombinedKeepAlive = () => { 298 | return this.combinedKeepAlive; 299 | } 300 | 301 | public render() { 302 | const { 303 | _identificationContextProps: { 304 | identification, 305 | keepAlive: upperKeepAlive, 306 | getLifecycle, 307 | }, 308 | disabled, 309 | name, 310 | ...wrapperProps 311 | } = this.props; 312 | const {activated} = this.state; 313 | const { 314 | _keepAliveContextProps: { 315 | include, 316 | exclude, 317 | }, 318 | } = wrapperProps; 319 | // When the parent KeepAlive component is mounted or unmounted, 320 | // use the keepAlive prop of the parent KeepAlive component. 321 | const propKey = name || getKeyByFiberNode((this as any)._reactInternalFiber); 322 | if (!propKey) { 323 | warn('[React Keep Alive] components must have key or name.'); 324 | return null; 325 | } 326 | const newKeepAlive = getKeepAlive(propKey, include, exclude, disabled); 327 | this.combinedKeepAlive = getLifecycle === undefined || getLifecycle() === LIFECYCLE.UPDATING 328 | ? newKeepAlive 329 | : identification 330 | ? upperKeepAlive && newKeepAlive 331 | : newKeepAlive; 332 | return activated 333 | ? ( 334 | 341 | ) 342 | : null; 343 | } 344 | } 345 | 346 | const KeepAlive = withKeepAliveContextConsumer( 347 | withIdentificationContextConsumer(ListenUpperKeepAliveContainer) 348 | ) as any; 349 | 350 | return hoistNonReactStatics(KeepAlive, Component); 351 | } 352 | -------------------------------------------------------------------------------- /src/utils/md5.ts: -------------------------------------------------------------------------------- 1 | import md5 from 'js-md5'; 2 | import {prefix} from './createUniqueIdentification'; 3 | 4 | export default function createMD5(value: string = '', length = 6) { 5 | return `${prefix}-${md5(value).substr(0, length)}`; 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/noop.ts: -------------------------------------------------------------------------------- 1 | const noop = () => undefined; 2 | 3 | export default noop; 4 | -------------------------------------------------------------------------------- /src/utils/shallowEqual.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * From react 3 | */ 4 | function is(x: any, y: any) { 5 | return ( 6 | (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare 7 | ); 8 | } 9 | 10 | const hasOwnProperty = Object.prototype.hasOwnProperty; 11 | 12 | function shallowEqual(objA: object, objB: object) { 13 | if (is(objA, objB)) { 14 | return true; 15 | } 16 | 17 | if ( 18 | typeof objA !== 'object' || 19 | objA === null || 20 | typeof objB !== 'object' || 21 | objB === null 22 | ) { 23 | return false; 24 | } 25 | 26 | const keysA = Object.keys(objA); 27 | const keysB = Object.keys(objB); 28 | 29 | if (keysA.length !== keysB.length) { 30 | return false; 31 | } 32 | 33 | // Test for A's keys different from B. 34 | for (const key of keysA) { 35 | if ( 36 | !hasOwnProperty.call(objB, key) || 37 | !is(objA[key], objB[key]) 38 | ) { 39 | return false; 40 | } 41 | } 42 | 43 | return true; 44 | } 45 | 46 | export default shallowEqual; 47 | -------------------------------------------------------------------------------- /src/utils/useKeepAliveEffect.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useContext, useRef} from 'react'; 2 | import {warn} from './debug'; 3 | import {COMMAND} from './keepAliveDecorator'; 4 | import IdentificationContext, {IIdentificationContextProps} from '../contexts/IdentificationContext'; 5 | 6 | export default function useKeepAliveEffect(effect: React.EffectCallback) { 7 | if (!useEffect) { 8 | warn('[React Keep Alive] useKeepAliveEffect API requires react 16.8 or later.'); 9 | } 10 | const { 11 | eventEmitter, 12 | identification, 13 | } = useContext(IdentificationContext); 14 | const effectRef: React.MutableRefObject = useRef(effect); 15 | effectRef.current = effect; 16 | useEffect(() => { 17 | let bindActivate: (() => void) | null = null; 18 | let bindUnactivate: (() => void) | null = null; 19 | let bindUnmount: (() => void) | null = null; 20 | let effectResult = effectRef.current(); 21 | let unmounted = false; 22 | eventEmitter.on( 23 | [identification, COMMAND.ACTIVATE], 24 | bindActivate = () => { 25 | // Delayed update 26 | Promise.resolve().then(() => { 27 | effectResult = effectRef.current(); 28 | }); 29 | unmounted = false; 30 | }, 31 | true, 32 | ); 33 | eventEmitter.on( 34 | [identification, COMMAND.UNACTIVATE], 35 | bindUnactivate = () => { 36 | if (effectResult) { 37 | effectResult(); 38 | unmounted = true; 39 | } 40 | }, 41 | true, 42 | ); 43 | eventEmitter.on( 44 | [identification, COMMAND.UNMOUNT], 45 | bindUnmount = () => { 46 | if (effectResult) { 47 | effectResult(); 48 | unmounted = true; 49 | } 50 | }, 51 | true, 52 | ); 53 | return () => { 54 | if (effectResult && !unmounted) { 55 | effectResult(); 56 | } 57 | eventEmitter.off( 58 | [identification, COMMAND.ACTIVATE], 59 | bindActivate, 60 | ); 61 | eventEmitter.off( 62 | [identification, COMMAND.UNACTIVATE], 63 | bindUnactivate, 64 | ); 65 | eventEmitter.off( 66 | [identification, COMMAND.UNMOUNT], 67 | bindUnmount, 68 | ); 69 | }; 70 | }, []); 71 | } 72 | -------------------------------------------------------------------------------- /src/utils/withIdentificationContextConsumer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import IdentificationContext, {IIdentificationContextProps} from '../contexts/IdentificationContext'; 3 | import getDisplayName from './getDisplayName'; 4 | 5 | export interface IIdentificationContextConsumerComponentProps { 6 | _identificationContextProps: IIdentificationContextProps; 7 | } 8 | 9 | export const withIdentificationContextConsumerDisplayName = 'withIdentificationContextConsumer'; 10 | 11 | export default function withIdentificationContextConsumer

(Component: React.ComponentType) { 12 | const WithIdentificationContextConsumer = (props: P) => ( 13 | 14 | {(contextProps: IIdentificationContextProps) => } 15 | 16 | ); 17 | 18 | WithIdentificationContextConsumer.displayName = `${withIdentificationContextConsumerDisplayName}(${getDisplayName(Component)})`; 19 | return WithIdentificationContextConsumer; 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/withKeepAliveContextConsumer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import KeepAliveContext, {IKeepAliveContextProps} from '../contexts/KeepAliveContext'; 3 | import getDisplayName from './getDisplayName'; 4 | 5 | export interface IKeepAliveContextConsumerComponentProps { 6 | _keepAliveContextProps: IKeepAliveContextProps; 7 | } 8 | 9 | export const WithKeepAliveContextConsumerDisplayName = 'withKeepAliveContextConsumer'; 10 | 11 | export default function withKeepAliveContextConsumer

(Component: React.ComponentType) { 12 | const WithKeepAliveContextConsumer = (props: P) => ( 13 | 14 | {(contextProps: IKeepAliveContextProps) => } 15 | 16 | ); 17 | 18 | WithKeepAliveContextConsumer.displayName = `${WithKeepAliveContextConsumerDisplayName}(${getDisplayName(Component)})`; 19 | return WithKeepAliveContextConsumer; 20 | } 21 | -------------------------------------------------------------------------------- /test/Comment.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {mount} from 'enzyme'; 3 | import Comment from '../src/components/Comment'; 4 | 5 | describe('', () => { 6 | it('the render function will render a div element', () => { 7 | const wrapper = mount(test); 8 | expect(wrapper.html()).toEqual('

'); 9 | }); 10 | 11 | 12 | it('rendered
will be replaced with comment nodes', () => { 13 | const wrapper = mount( 14 |
15 | test 16 |
17 | ); 18 | expect(wrapper.html()).toContain(''); 19 | }); 20 | 21 | it('the comment node will be restored to
when uninstalling', () => { 22 | const componentWillUnmount = Comment.prototype.componentWillUnmount; 23 | const wrapper = mount( 24 |
25 | test 26 |
27 | ); 28 | Comment.prototype.componentWillUnmount = function () { 29 | componentWillUnmount.call(this); 30 | expect(wrapper.html()).toContain('
'); 31 | } 32 | wrapper.unmount(); 33 | Comment.prototype.componentWillUnmount = componentWillUnmount; 34 | }); 35 | 36 | it('children of will become empty strings if they are not of type string', () => { 37 | const wrapper = mount( 38 |
39 |
40 |
41 | ); 42 | expect(wrapper.html()).toContain(''); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/KeepAlive.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {mount} from 'enzyme'; 3 | import {KeepAlive} from '../src'; 4 | 5 | class Test extends React.Component { 6 | state = { 7 | index: 0, 8 | }; 9 | 10 | handleAdd = () => { 11 | this.setState(({index}) => ({ 12 | index: index + 1, 13 | })); 14 | } 15 | 16 | render() { 17 | return this.state.index; 18 | } 19 | } 20 | 21 | describe('', () => { 22 | it(' not will report an error', () => { 23 | expect(() => { 24 | mount( 25 | 26 | 27 | , 28 | ); 29 | }).toThrow('[React Keep Alive]'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | import {configure} from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | console.error = jest.fn(error => { 5 | throw new Error(error); 6 | }); 7 | 8 | configure({ 9 | adapter: new Adapter(), 10 | }); 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "outDir": "es", 5 | "module": "esnext", 6 | "esModuleInterop": true, 7 | "target": "es5", 8 | "lib": ["es6", "dom"], 9 | "declaration": true, 10 | "sourceMap": false, 11 | "jsx": "react", 12 | "moduleResolution": "node", 13 | "rootDir": "src", 14 | "forceConsistentCasingInFileNames": true, 15 | "noImplicitThis": true, 16 | "noImplicitAny": true, 17 | "strictNullChecks": true, 18 | "suppressImplicitAnyIndexErrors": true, 19 | "experimentalDecorators": true, 20 | "noUnusedLocals": true, 21 | "typeRoots": [ 22 | "node_modules/@types", 23 | "typings", 24 | ], 25 | }, 26 | "exclude": [ 27 | "node_modules", 28 | "jest", 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "rules": { 7 | "max-line-length": false, 8 | "no-console": false, 9 | "no-debugger": false, 10 | "quotemark": [true, "single", "jsx-double"], 11 | "trailing-comma": [true, {"multiline": "ignore", "singleline": "never"}], 12 | "ordered-imports": false, 13 | "member-ordering": false, 14 | "max-classes-per-file": false, 15 | "no-unsafe-finally": false, 16 | "object-literal-sort-keys": false, 17 | "space-before-function-paren": false, 18 | "no-shadowed-variable": false, 19 | "arrow-parens": [true, "ban-single-arg-parens"] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | 5 | const ROOT = path.join(__dirname, 'demo'); 6 | const SRC = path.join(ROOT, 'src'); 7 | 8 | module.exports = { 9 | mode: 'development', 10 | devtool: 'source-map', 11 | entry: { 12 | index: path.join(SRC, 'index.js'), 13 | }, 14 | output: { 15 | path: path.join(ROOT, 'build'), 16 | filename: 'static/[name].js', 17 | publicPath: '/', 18 | }, 19 | plugins: [ 20 | new HtmlWebpackPlugin({ 21 | filename: 'index.html', 22 | inject: true, 23 | template: path.join(ROOT, 'index.html'), 24 | }), 25 | ], 26 | module: { 27 | rules: [ 28 | { 29 | test: /\.js$/, 30 | use: ['babel-loader'], 31 | include: SRC, 32 | }, 33 | { 34 | test: /\.css$/, 35 | use: ['style-loader', 'css-loader'], 36 | }, 37 | ], 38 | }, 39 | }; 40 | --------------------------------------------------------------------------------