Redux 专题 04 —— 时间旅行
关于Redux的扩展能力,大家说的最多的是中间件,但是为什么createStore的第三个参数叫enhancer呢?
其实enhancer是一个比中间件更加底层的接口。
applyMiddleware就是Redux官方的enhancer。
增强器
enhancer翻译成中文是增强器。如果说中间件是在dispatch前后植入一段逻辑的话,那么增强器顾名思义是要增强整个Store的能力。很明显它的侵入性更强。
我们以applyMiddleware为例来讲解增强器的特点。
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args);
let dispatch = () => {
throw new Error(
`Dispatching while constructing your middleware is not allowed. ` +
`Other middleware would not be applied to this dispatch.`
);
};
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args),
};
const chain = middlewares.map(middleware => middleware(middlewareAPI));
dispatch = compose(...chain)(store.dispatch);
return { ...store, dispatch };
}
}
applyMiddleware方法执行以后被传入createStore方法,这个时候它依然是一个函数,接受一个唯一的参数createStore。
为什么要将createStore以参数传进来呢?因为applyMiddleware内部要生成一个Store,而生成一个Store的目的是为了保留其余API,替换dispatch API。
如果说中间件改造的是dispatch,那么增强器改造的就是Store。
compose
假如Redux需要用到两个以上的增强器怎么办?
关键时候,还得老将出马。
compose在createStore内部出现过一次,其实低调的它还是Redux的顶级API。
用在哪里,当然是组合若干增强器啦。
const enhancer = compose(applyMiddleware(middleware1, middleware2, middleware3), enhancer2, enhancer1);
const store = createStore(reducer, preloadedState, enhancer);
大家有没有发现一个惊天秘密?
传入多个中间件不需要compose组合,而传入多个增强器需要compose组合。
这是因为,applyMiddleware我们上一章讲过,源码内部就有一个compose,所以就不再需要了。
时间旅行
大家可能用过Git,也可能没用过。
Git最大的本事是可以还原任意版本的仓库,某种意义上这是不是一个时间机器?
现在我给时间机器加上两个限制条件:
- 它只能回溯过去。
- 它只能观测,不能修改历史。
Redux版本的时间机器就是这种阉割版的时间机器。
状态追溯的前提是什么?
状态追溯的前提是不修改状态,这样才能保存时间线上的所有状态,然后在需要的时候回放。
现在知道Reducer为什么每次都要返回一个新对象的原因了吧。
实际上Redux的作者Dan Abramov一开始设计Redux就是奔着时间旅行去的,所以Redux才会有这么多繁琐的步骤。
使用
想要启用时间旅行来调试Redux,市面上有两种方式。
浏览器扩展
首先,从Chrome下载扩展Redux DevTools。
它会暴露一个全局API,之前是window.devToolsExtension
,现在变成了window.__REDUX_DEVTOOLS_EXTENSION__
。目的是让全局属性更不容易被覆盖。
如果应用不需要安装其他中间件和增强器,那么这样写就可以了:
import { createStore } from 'redux';
import reducer from './reducer';
const store = createStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
export default store;
否则这样写:
import { createStore, applyMiddleware, compose } from 'redux';
import reducer from './reducer';
import middlewares from './middlewares';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducer, composeEnhancers(applyMiddleware(...middlewares)));
export default store;
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
是浏览器扩展暴露的另一个全局API。
还没结束,浏览器扩展的作者仍然担心你觉得繁琐,所以又提供了一个npm包。
先来看不需要安装其他中间件和增强器的场景:
import { createStore } from 'redux';
import { devToolsEnhancer } from 'redux-devtools-extension';
import reducer from './reducer';
const store = createStore(reducer, devToolsEnhancer());
export default store;
否则这样写:
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import reducer from './reducer';
import middlewares from './middlewares';
const store = createStore(reducer, composeWithDevTools(applyMiddleware(...middlewares)));
export default store;
非浏览器扩展
首先,浏览器扩展和非浏览器扩展的实现方式是不一样的,不要混为一谈。
redux-devtools-dock-monitor
的作用是让你可以随意控制时间旅行的面板,props中传入的各种key都是键盘的快捷键。
redux-devtools-log-monitor
的作用是在面板上显示历史状态,没有它redux-devtools
就是个空壳子。
import { createDevTools } from 'redux-devtools';
import DockMonitor from 'redux-devtools-dock-monitor';
import LogMonitor from 'redux-devtools-log-monitor';
import reducer from './reducer';
import middlewares from './middlewares';
const DevTools = createDevTools(
<DockMonitor toggleVisibilityKey='ctrl-h' changePositionKey='ctrl-q' defaultIsVisible={true}>
<LogMonitor theme='tomorrow' />
</DockMonitor>
);
const store = createStore(reducer, compose(applyMiddleware(...middlewares), DevTools.instrument()));
如果不需要这么花哨,也可以不安装redux-devtools-dock-monitor
:
const DevTools = createDevTools(<LogMonitor />);
什么也不传入行不行?
不行,createDevTools必须得有一个children。
export default function createDevTools(children) {
const monitorElement = Children.only(children);
// ...
}
界面
如果是浏览器扩展的方式,在Chrome DevTools面板会增加一个Redux菜单,点击即可看到调试面板。
如果是非浏览器扩展的方式,界面是内嵌在网页中的:
不好意思,图中并没有时间旅行的进度条。因为要显示进度条,得安装另外的npm包。
import { createDevTools } from 'redux-devtools';
import SliderMonitor from 'redux-slider-monitor';
const DevTools = createDevTools(<SliderMonitor />);
然后效果就是这样的:
所以呀,浏览器扩展其实是将各种模块都集成到一起,打包成一个解决方案。
文献
这是一个瑞典人的开源项目,分别用两种方式写的demo,可以立刻查看效果。