对于目前普遍的“单页应用”,其中的好处是,前端可以从容的处理较复杂的数据模型,同时基于数据模型可以进行变换,实现更为良好的交互操作。
良好的交互操作背后,其实是基于一个对应到页面组件状态的模型,随便称其为UI模型。
数据模型对应的是后端数据库中的业务数据,UI模型对应的是用户在浏览器一系列操作后组件所呈现的状态。
这两个模型不是对等的!
比如下图中这个管控台(不存在所谓的子页面,来进行单页路由的切换,而是一个类似portal的各块组件的切换):
我们构建的这个单页应用,后端的数据库和提供的接口,是存储和管理数据模型的状态。
但是用户操作管控台中,左侧面板的打开/关闭、列表选中的项目、编辑面板的打开等,这些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最后的代码如下:
//2、引入redux和引入reducer
import {createStore, applyMiddleware, compose} from 'redux';
//import reducer from './reducers';
import rootReducer from './combineReducers';
import thunk from 'redux-thunk';
//3、创建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里了,最后只好用了以上的办法了。
React项目实战:react-redux-router基本原理,Redux 的 React 绑定库包含了 容器组件和展示组件相分离 的开发思想。明智的做法是只在最顶层组件(如路由操作)里使用 Redux。其余内部组件仅仅是展示性的,所有数据都通过 props 传入。
flux四大元素:Dispatcher:根据注册派发动作(action),Store: 存储数据,处理数据,Action:用于驱动Dispatcher,View: 用户界面视图。flux的目的:纠正MVC框架的无法禁绝view与model通信的缺点。Redux基本原则:继承Flux基本原则:单向数据流
Redux:状态管理工具,与React没有任何关系,其他UI框架也可以使用Redux,react-redux:React插件,作用:方便在React项目中使用Redux,react-thunk:中间件,作用:支持异步action
compose,英文意思 组成,构成。它的作用也是通过一系列的骚操作,实现任意的、多种的、不同的功能模块的组合,用来加强组件。很容易实现功能的组合拼装、代码复用;可以根据需要组合不同的功能;
这里要讲的就是一个Redux在React中的应用问题,讲一讲Redux,react-redux,redux-thunk,redux-actions,redux-promise,redux-sage这些包的作用和他们解决的问题。
最近几天对 redux 的中间件进行了一番梳理,又看了 redux-saga 的文档,和 redux-thunk 和 redux-promise 的源码,结合前段时间看的redux的源码的一些思考,感觉对 redux 中间件的有了更加深刻的认识,因此总结一下
类似于 Vue,React 中组件之间的状态管理 第三方包为:react-redux。react-redux 其实是 Redux的官方React绑定库,它能够使你的React组件从Redux store中读取数据,并且向store分发actions以更新数据。
看到“reducer”这个词,容易让人联想到Redux,但是在本文中,不必先理解Redux才能阅读这篇文章。咱们将一起讨论“reducer”实际上是什么,以及如何利用useReducer来管理组件中的复杂状态,以及这个新钩子对Redux意味着什么?
我在读React-Redux源码的过程中,很自然的要去网上找一些参考文章,但发现这些文章基本都没有讲的很透彻,很多时候就是平铺直叙把API挨个讲一下,而且只讲某一行代码是做什么的,却没有结合应用场景和用法解释清楚为什么这么做。
我的许多同事最近通过各种方式问同一类问题:“如果我们开始用 hook 后,那还有必要用 Redux 吗?”“React hook 不是让 Redux 过时了吗?那只用 Hooks 就可以做 Redux 所有能做的事了吧?”
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!