对于目前广泛的“单页应用”,其中的好处是,前端能够从容的处理较复杂的数据模型,同时基于数据模型能够进行变换,实现更为良好的交互操做。前端
良好的交互操做背后,实际上是基于一个对应到页面组件状态的模型,随便称其为UI模型。react
数据模型对应的是后端数据库中的业务数据,UI模型对应的是用户在浏览器一系列操做后组件所呈现的状态。git
这两个模型不是对等的!github
好比下图中这个管控台(不存在所谓的子页面,来进行单页路由的切换,而是一个相似portal的各块组件的切换):数据库
咱们构建的这个单页应用,后端的数据库和提供的接口,是存储和管理数据模型的状态。redux
可是用户操做管控台中,左侧面板的打开/关闭、列表选中的项目、编辑面板的打开等,这些UI模型的状态均不会被后端记录。后端
当用户强制进行页面刷新,或者关闭页面后又再次打开时,单页应用虽然能从后端拉取数据记录,可是页面组件的状态已经没法恢复了。数组
目前,多数的单页应用的处理,就是在页面刷新或从新打开后,抛弃以前用户操做后的状态,进到一个初始状态。(固然,若是涉及较多内容编辑的,会提示用户先保存等等)浏览器
但这样,显然是 对交互的一种妥协。缓存
咱们的单页应用是基于Redux+React构建。
组件的 大部分状态 (一些非受控组件内部维护的state,确实比较难去记录了)都记录在Redux的store维护的state中。
正是由于Redux这种基于全局的状态管理,才让“UI模型”能够清晰浮现出来。
因此,只要在浏览器的本地存储(localStorage)中,将state进行缓存,就能够(基本)还原用户最后的交互界面了。
先说什么时候取,由于这块好说。
假设咱们已经存下了state,localStorage中就会存在一个序列化后的state对象。
在界面中还原state,只须要在应用初始化的时候,Redux建立store的时候取一次就能够。
... const loadState = () => { try { // 也能够容错一下不支持localStorage的状况下,用其余本地存储 const serializedState = localStorage.getItem('state'); if (serializedState === null) { return undefined; } else { return JSON.parse(serializedState); } } catch (err) { // ... 错误处理 return undefined; } } let store = createStore(todoApp, loadState()) ...
保存state的方式很简单:
const saveState = (state) => { try { const serializedState = JSON.stringify(state); localStorage.setItem('state', serializedState); } catch (err) { // ...错误处理 } };
至于什么时候触发保存,一种简(愚)单(蠢)的方式是,在每次state发生更新的时候,都去持久化一下。这样就能让本地存储的state时刻保持最新状态。
基于Redux,这也很容易作到。在建立了store后,调用subscribe方法能够去监听state的变化。
// createStore以后 store.subscribe(() => { const state = store.getState(); saveState(state); })
可是,显然,从性能角度这很不合理(不过也许在某些场景下有这个必要)。因此机智的既望同窗,提议只在onbeforeunload事件上就能够。
window.onbeforeunload = (e) => { const state = store.getState(); saveState(state); };
因此,只要用户刷新或者关闭页面时,都会默默记下当前的state状态。
一存一取作到后,特性就已实现。版本上线,用户使用,本地缓存了state,当前的应用毫无问题。
可是当再次发布新版本代码后,问题就来了。
新代码维护的state和以前的结构不同,用户用新的代码,读取本身本地缓存的旧的state,不免会出错。
然而用户此时不管怎么操做,都不会清楚掉本身本地缓存的state(不详细说了,主要就是由于上面loadState和saveState的逻辑,致使。。。错误的state会一直被反复保存,即便在developer tools中手动清除localStorage也不会有效果)
解决就是,state须要有个版本管理,当和代码的版本不一致时,至少进行个清空操做。
目前项目中,采用的如下方案:
直接利用state,在其中增长一个节点,来记录version。即增长对应的action、reducer,只是为了维护version的值。
... // Actions export function versionUpdate(version = 0.1) { return { type : VERSION_UPDATE, payload : version }; } ...
保存state的逻辑改动较小,就是在每次保存的时候,要把当前代码的version更新到state。
... window.onbeforeunload = (e) => { store.dispatch({ type: 'VERSION_UPDATE', payload: __VERSION__ // 代码全局变量,随工程配置一块儿处理便可。每次涉及须要更新state的时候,必须更新此版本号。 }) const state = store.getState(); saveState(state); } ...
读取state的时候,则要比较代码的版本和state的版本,不匹配则进行相应处理(清空则是传给createStore的初始state为undefined便可)
export const loadState = () => { try { const serializedState = localStorage.getItem('state'); if (serializedState === null) { return undefined; } else { let state = JSON.parse(serializedState); // 判断本地存储的state版本,若是落后于代码的版本,则清空state if (state.version < __VERSION__) { return undefined; } else { return state; } } } catch (err) { // ...错误处理 return undefined; } };
如下不是转的,是本身写的。
解读 redux 源码之 createStore,代码目录在 redux/src/createStore。
import isPlainObject from 'lodash/isPlainObject' import $$observable from 'symbol-observable' /** * 这是 redux 保留的私有的 action types。 * 对于任何未知的 actions,你必需要返回当前的状态。 * 若是当前的状态是没有定义的,你都要返回一个初始的状态。 * 不要在你的代码中直接引用这些 action types。 */ export const ActionTypes = { INIT: '@@redux/INIT' } /** * 建立一个持有状态树的 redux store。 * 调用dispatch() 是惟一的一种方式去修改 store中的的值。 * 应用中应该只有一个 store。为了将程序状态中不一样部分的变动逻辑 * 组合在一块儿,你须要使用 combineReducers 将一些 * reducers 合并成一个reducer * * @param {Function} reducer 一个返回下一个状态树的方法,须要提供当 * 前的状态树和要发送的 action。 * * @param {any} [preloadedState] 初始的状态。 * 您能够选择指定它来保存通用应用程序中服务器的状态,或者恢复 * 之前序列化的用户会话。 * 若是你使用了`combineReducers`方法来生成最终的reducer。那么这个初始状 * 态对象的结构必须与调用`combineReducers`方法时传入的参数的结构保持相 * 同。 * * @param {Function} [enhancer] store加强器。你能够选择性的传入一个加强函 * 数取加强 store,例如中间件,时间旅行,持久化。这 redux 惟一一个自带的 * 加强器是的 applyMiddleware * * @returns {Store} 一个可让你读状态,发布 actions 和订阅变化的 redux * store */ export default function createStore(reducer, preloadedState, enhancer) { // 若是 preloadedState类型是function,enhancer类型是undefined,那认为用 // 户没有传入preloadedState,就将preloadedState的值传给 // enhancer,preloadedState值设置为undefined if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') { enhancer = preloadedState preloadedState = undefined } // enhancer类型必须是一个function if (typeof enhancer !== 'undefined') { if (typeof enhancer !== 'function') { throw new Error('Expected the enhancer to be a function.') } // 返回使用enhancer加强后的store return enhancer(createStore)(reducer, preloadedState) } // reducer必须是一个function if (typeof reducer !== 'function') { throw new Error('Expected the reducer to be a function.') } let currentReducer = reducer let currentState = preloadedState let currentListeners = [] let nextListeners = currentListeners let isDispatching = false // 在每次修改监听函数数组以前复制一份,实际的修改的是新 // 复制出来的数组上。确保在某次 dispatch 发生前就存在的监听器, // 在该次dispatch以后都能被触发一次。 function ensureCanMutateNextListeners() { if (nextListeners === currentListeners) { nextListeners = currentListeners.slice() } } /** * 读取 store 管理的状态树 * * @returns {any} 返回应用中当前的状态树 */ function getState() { return currentState } /** * 添加一个改变监听器。它将在一个action被分发的时候触发,而且状态数的某 * 些部分可能已经发生了变化。那么你能够调用 getState 来读取回调中的当前 * 状态树。 * * 你能够从一个改变的监听中调用 dispatch(),注意事项: * * 1.在每一次调用 dispatch() 以前监听器数组都会被复制一份。若是你在监听函 * 数中订阅或者取消订阅,这个不会影响当前正在进行的 dispatch()。而下次 * dispatch()是不是嵌套调用,都会使用最新的修改后的监听列表。 * 2.监听器不但愿看到哦啊全部状态的改变,如状态可能在监听器被调用前可能 * 在嵌套 dispatch() 可能更新过屡次。可是,在某次dispatch * 触发以前已经注册的监听函数均可以读取到此次diapatch以后store的最新状 * 态。 * * @param {Function} listener 在每次 dispatch 以后会执行的回调函数。 * @returns {Function} 返回一个用于取消此次订阅的函数。 */ function subscribe(listener) { if (typeof listener !== 'function') { throw new Error('Expected listener to be a function.') } let isSubscribed = true ensureCanMutateNextListeners() nextListeners.push(listener) return function unsubscribe() { if (!isSubscribed) { return } isSubscribed = false ensureCanMutateNextListeners() const index = nextListeners.indexOf(listener) nextListeners.splice(index, 1) } } /** * 发送一个 action,这是惟一一种触发状态改变的方法。 * 每次发送 action,用于建立 store 的 `reducer` 都会被调用一次。调用时传入 * 的参数是当前的状态以及被发送的 action。的返回值将被看成下一次的状 * 态,而且监听器将会被通知。 * * 基础实现只支持简单对象的 actions。若是你但愿能够发送 * Promise,Observable,thunk火气其余形式的 action,你须要用相应的中间 * 件把 store建立函数封装起来 。例如,你能够参阅 `redux-thunk`包的文档。 * 不过这些中间件仍是经过 dispatch 方法发送简单对象形式的 action。 * * @param {Object} action,一个标识改变了什么的对象。这是一个很好的点子 * 保证 actions 可被序列化,这样你就能够记录而且回放用户的操做,或者使用 * 能够穿梭时间的插件 `redux-devtools`。一个 action 必须有一个值不为 * `undefined`的type属性,推荐使用字符串常量做为 action types。 * * @returns {Object} 为了方便起见,返回传入的 action 对象。 * * 要注意的是,若是你使用一个自定义的中间件,可能会把`dispatch()`的返回 * 值封装成其余内容(好比,一个能够await的Promise)。 */ function dispatch(action) { // 若是 action不是一个简单对象,抛出异常 if (!isPlainObject(action)) { throw new Error( 'Actions must be plain objects. ' + 'Use custom middleware for async actions.' ) } if (typeof action.type === 'undefined') { throw new Error( 'Actions may not have an undefined "type" property. ' + 'Have you misspelled a constant?' ) } // reducer内部不容许再次调用dispatch,不然抛出异常 if (isDispatching) { throw new Error('Reducers may not dispatch actions.') } try { isDispatching = true currentState = currentReducer(currentState, action) } finally { isDispatching = false } const listeners = currentListeners = nextListeners for (let i = 0; i < listeners.length; i++) { const listener = listeners[i] listener() } return action } /** * 替换 store 当前使用的 reducer 函数。 * * 若是你的程序代码实现了代码拆分,而且你但愿动态加载某些 reducers。或 * 者你为 redux 实现一个热加载的时候,你也会用到它。 * * @param {Function} nextReducer 替换后的reducer * @returns {void} */ function replaceReducer(nextReducer) { if (typeof nextReducer !== 'function') { throw new Error('Expected the nextReducer to be a function.') } currentReducer = nextReducer dispatch({ type: ActionTypes.INIT }) } /** * 为 observable/reactive库预留的交互接口。 * @returns {observable} 标识状态变动的最简单 observable兑现。 * 想要得到更多的信息,能够查看 observable的提案: * https://github.com/tc39/proposal-observable */ function observable() { const outerSubscribe = subscribe return { /** * 一个最简单的 observable 订阅方法。 * @param {Object} observer,任何的能够被做为observer使用的对象。 * observer对象应该包含`next`方法。 * @returns {subscription} 返回一个 object 带有用于从store 解除 observable而且进一步中止接收 值 的`unsubscribe`方法的对象。 */ subscribe(observer) { if (typeof observer !== 'object') { throw new TypeError('Expected the observer to be an object.') } function observeState() { if (observer.next) { observer.next(getState()) } } observeState() const unsubscribe = outerSubscribe(observeState) return { unsubscribe } }, [$$observable]() { return this } } } // 当一个store建立好,一个 "INIT" 的 action 就会分发,以便每一个 reducer返回 // 初始的状态,这有效填充初始的状态树。 dispatch({ type: ActionTypes.INIT }) return { dispatch, subscribe, getState, replaceReducer, [$$observable]: observable } }
查看源码发现createStore,能够接受一个更改的state,配合redux-thunk最后的代码以下:
//二、引入redux和引入reducer import {createStore, applyMiddleware, compose} from 'redux'; //import reducer from './reducers'; import rootReducer from './combineReducers'; import thunk from 'redux-thunk'; //三、建立store const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; let store = null; const loadState = () => { try { const serializedState = sessionStorage.getItem('state'); if (serializedState === null) { return undefined; } else { return JSON.parse(serializedState); } } catch (err) { // ... 错误处理 return undefined; } } if(process.env.NODE_ENV === 'development'){ store = createStore(rootReducer,loadState(), composeEnhancers( applyMiddleware(thunk) )); }else{ store = createStore(rootReducer,loadState(),applyMiddleware(thunk)) } export default store;
因为store的数据变化会经过subscribe来监听,因此这时候保存到sessionStorage里的数据是最新的store数据
createStore的时候会从sessionStorage里取。问题解决。
在本次解决问题的过程当中,使用过react-persist这个插件,发现它的数据确实也同步给sessionStorage了,可是页面刷新
store数据没了,也同步给sessionStorage里了,最后只好用了以上的办法了。
看到的小伙伴若是有更好的办法欢迎留言指教哇。