avatar

veedrin

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菜单,点击即可看到调试面板。

illustration

如果是非浏览器扩展的方式,界面是内嵌在网页中的:

illustration

不好意思,图中并没有时间旅行的进度条。因为要显示进度条,得安装另外的npm包。

import { createDevTools } from 'redux-devtools';
import SliderMonitor from 'redux-slider-monitor';

const DevTools = createDevTools(<SliderMonitor />);

然后效果就是这样的:

illustration

所以呀,浏览器扩展其实是将各种模块都集成到一起,打包成一个解决方案。

文献

这是一个瑞典人的开源项目,分别用两种方式写的demo,可以立刻查看效果。

基于浏览器扩展的Redux时间旅行demo

不基于浏览器扩展的Redux时间旅行demo