<

1. redux-saga #

redux-saga 就是用来处理上述副作用(异步任务)的一个中间件。它是一个接收事件,并可能触发新事件的过程管理者,为你的应用管理复杂的流程。

2. redux-saga工作原理 #

3. redux-saga分类 #

4. 构建项目 #

4.1 初始化项目 #

cnpm install create-react-app -g 
create-react-app zhufeng-saga-start
cd zhufeng-saga-start
cnpm i redux react-redux redux-saga tape --save

5. 跑通saga #

5.1 src\index.js #

src\index.js

import store from './store';

5.2 store\index.js #

src\store\index.js

import {createStore, applyMiddleware} from 'redux';
import reducer from './reducer';
import createSagaMiddleware from 'redux-saga';
//首先我们引入 ./sagas 模块中的 Saga。然后使用 redux-saga 模块的 createSagaMiddleware 工厂函数来创建一个 Saga middleware
import {helloSaga} from './sagas';
let sagaMiddleware = createSagaMiddleware();
//运行 helloSaga 之前,我们必须使用 applyMiddleware 将 middleware 连接至 Store。然后使用 sagaMiddleware.run(helloSaga) 运行 Saga。
let store=applyMiddleware(sagaMiddleware)(createStore)(reducer);
sagaMiddleware.run(helloSaga);
export default store;

5.3 store\reducer.js #

src\store\reducer.js

export default function (state,action) {}

5.4 store\sagas.js #

src\store\sagas.js

//helloSaga它只是打印了一条消息,然后退出。
export function* helloSaga() {
    console.log('Hello Saga!');
}

6. 异步计数器 #

6.1 components/Counter.js #

src/components/Counter.js

import React,{Component} from 'react'
import {connect} from 'react-redux';
import actions from '../store/actions';
class Counter extends Component{
    render() {
        return (
            <div>
                <p>{this.props.number}</p>
                <button onClick={this.props.incrementAsync}>+</button>
            </div>
      )
  }
}
export default connect(
    state => state,
    actions
)(Counter);

6.2 src/index.js #

src/index.js

import React from 'react'
import ReactDOM from 'react-dom';
import Counter from './components/Counter';
import {Provider} from 'react-redux';
import store from './store';
ReactDOM.render(<Provider store={store}>
  <Counter/>
</Provider>,document.querySelector('#root'));

6.3 store/action-types.js #

src/store/action-types.js

export const INCREMENT='INCREMENT';
export const INCREMENT_ASYNC='INCREMENT_ASYNC';

6.4 store/actions.js #

src/store/actions.js

import * as types from './action-types';
export default {
    incrementAsync() {
        return {type:types.INCREMENT_ASYNC}
    }
}

6.5 src/store/index.js #

src/store/index.js

import {createStore, applyMiddleware} from 'redux';
import reducer from './reducer';
import createSagaMiddleware from 'redux-saga';
//首先我们引入 ./sagas 模块中的 Saga。然后使用 redux-saga 模块的 createSagaMiddleware 工厂函数来创建一个 Saga middleware
import rootSaga from './sagas';
let sagaMiddleware = createSagaMiddleware();
//运行 rootSaga 之前,我们必须使用 applyMiddleware 将 middleware 连接至 Store。然后使用 sagaMiddleware.run(helloSaga) 运行 Saga。
let store=applyMiddleware(sagaMiddleware)(createStore)(reducer);
sagaMiddleware.run(rootSaga);
export default store;

6.6 store/reducer.js #

src/store/reducer.js

import * as types from './action-types';
export default function (state={number:0},action) {
    switch(action.type){
        case types.INCREMENT:
            return {number: state.number+1};
        default:
            return state;
    }
}

6.7 store/sagas.js #

src/store/sagas.js

import { delay,all,put, takeEvery } from 'redux-saga/effects'

//worker Saga: 将执行异步的 increment 任务
//incrementAsync Saga 通过 delay(1000) 延迟了 1 秒钟,然后 dispatch 一个叫 INCREMENT 的 action。
export function* incrementAsync() {
    //工具函数 delay,这个函数返回一个延迟 1 秒再 resolve 的 Promise 我们将使用这个函数去 block(阻塞) Generator
    //Sagas 被实现为 Generator functions,它会 yield 对象到 redux-saga middleware。 被 yield 的对象都是一类指令,指令可被 middleware 解释执行。当 middleware 取得一个 yield 后的 Promise,middleware 会暂停 Saga,直到 Promise 完成
    //incrementAsync 这个 Saga 会暂停直到 delay 返回的 Promise 被 resolve,这个 Promise 将在 1 秒后 resolve
    //当 middleware 拿到一个被 Saga yield 的 Effect,它会暂停 Saga,直到 Effect 执行完成,然后 Saga 会再次被恢复
    yield delay(1000)
    //一旦 Promise 被 resolve,middleware 会恢复 Saga 接着执行,直到遇到下一个 yield
    //在这里下一个语句是另一个被 yield 的对象:调用 put({type: 'INCREMENT'}) 的结果,意思是告诉 middleware 发起一个 INCREMENT 的 action
    //put 就是我们称作 Effect 的一个例子。Effects 是一些简单 Javascript 对象,包含了要被 middleware 执行的指令
    yield put({ type: 'INCREMENT' })
}
//takeEvery,用于监听所有的 INCREMENT_ASYNC action,并在 action 被匹配时执行 incrementAsync 任务
export function* watchIncrementAsync() {
    yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}

export function* helloSaga() {
    console.log('Hello Saga!');
}

export default function* rootSaga() {
    //这个 Saga yield 了一个数组,值是调用 helloSaga 和 watchIncrementAsync 两个 Saga 的结果。意思是说这两个 Generators 将会同时启动
    yield all([
      helloSaga(),
      watchIncrementAsync()
    ])
  }

7. 单元测试 #

7.1 安装模块 #

cnpm i @babel/core @babel/node @babel/plugin-transform-modules-commonjs --save-dev

7.2 测试脚本 #

"scripts": {
    "test": "babel-node src/store/sagas.spec.js --plugins @babel/plugin-transform-modules-commonjs"
}

7.3 utils.js #

src\utils.js

export const   delay = (ms)=>{
    return new Promise(function(resolve){
        setTimeout(()=>{
            resolve();
        },ms);
    });
}

7.4 store\sagas.spec.js #

src\store\sagas.spec.js

import test from 'tape';
import { all,put, takeEvery,call } from 'redux-saga/effects';
import { incrementAsync }  from './sagas';
import {delay} from '../utils';
/**
 * incrementAsync 是一个 Generator 函数。执行的时候返回一个 iterator object,这个 iterator 的 next 方法返回一个对象
 * gen.next() // => { done: boolean, value: any }
 * value 字段包含被 yield 后的表达式,也就是 yield 后面那个表达式的结果。
 * done 字段指示 generator 是结束了,还是有更多的 yield 表达式
 */
test('incrementAsync Saga test', (assert) => {
    const gen = incrementAsync();
    assert.deepEqual(
    gen.next().value,
    call(delay,3000),
    'incrementAsync should return a Promise that will resolve after 3 second'
  ) 

   assert.deepEqual(
    gen.next().value,
    put({type: 'INCREMENT'}),
    'incrementAsync Saga must dispatch an INCREMENT action'
  )
  assert.deepEqual(
    gen.next(),
    {done:true,value:undefined},
    'incrementAsync Saga must be done'
  )

  assert.end() 
});

8. 声明式effects #

8.1 src/store/sagas.js #

src/store/sagas.js

import {all,put, takeEvery,call,takeLatest,cps,apply } from 'redux-saga/effects'
import {delay,read} from '../utils';

export function* readAsync() {
    let content = yield cps(read,'1.txt');//cps(fn, ...args)
    console.log('content=',content);
}

export function* incrementAsync() {
    //工具函数 delay,这个函数返回一个延迟 1 秒再 resolve 的 Promise 我们将使用这个函数去 block(阻塞) Generator
    //Sagas 被实现为 Generator functions,它会 yield 对象到 redux-saga middleware。 被 yield 的对象都是一类指令,指令可被 middleware 解释执行。当 middleware 取得一个 yield 后的 Promise,middleware 会暂停 Saga,直到 Promise 完成
    //incrementAsync 这个 Saga 会暂停直到 delay 返回的 Promise 被 resolve,这个 Promise 将在 1 秒后 resolve
    //put 还是 call 都不执行任何 dispatch 或异步调用,它们只是简单地返回 plain Javascript 对象
    yield call(delay,3000);//call(fn, ...args)
    //yield call([null,delay],3000);  call([context, fn], ...args)
    //yield apply(null,delay,3000); apply(context, fn, args)
    //yield delay(3000);
    yield put({ type: 'INCREMENT' })
}

8.2 store/sagas.spec.js #

src/store/sagas.spec.js

import test from 'tape';
import { all,put, takeEvery,call,cps,apply } from 'redux-saga/effects';
import { incrementAsync,readAsync }  from './sagas';
import {delay,read} from '../utils';

test('readAsync Saga test', (assert) => {
  const gen = readAsync();
  assert.deepEqual(
    gen.next().value,
    cps(read,'1.txt'),
    'readAsync should be done  after 3 second'
  )
  assert.deepEqual(
    gen.next(),
    {done:true,value:undefined},
    'readAsync Saga must be done'
  )
  assert.end();
});

8.3 src/utils.js #

src/utils.js

export function read(filename,callback){
    setTimeout(function(){
        console.log('read',filename);
     callback(null,filename);
    },1000);
 }

9. 错误处理 #

9.1 try catch #

src\store\sagas.js

export const delay2=ms => new Promise((resolve,reject) => {
    setTimeout(() => {
        if(Math.random()>.5){
            resolve();
        }else{
            reject();
        }
    },ms);
});
export function* incrementAsync2() {
    try{
        yield call(delay2,3000);
        yield put({ type:'INCREMENT'});
        alert('操作成功');
    }catch(error){
        alert('操作失败');
    }
}
//takeEvery,用于监听所有的 INCREMENT_ASYNC action,并在 action 被匹配时执行 incrementAsync 任务
export function* watchIncrementAsync() {
    //yield takeEvery('INCREMENT_ASYNC', incrementAsync);
    //只想得到最新那个请求的响应,如果已经有一个任务在执行的时候启动另一个 fetchData ,那之前的这个任务会被自动取消
    yield takeLatest('INCREMENT_ASYNC', incrementAsync2);
}

9.2 错误标识 #

10. take #

10.1 store/sagas.js #

src/store/sagas.js

import {all,put,take,select } from 'redux-saga/effects'
import {INCREMENT_ASYNC,INCREMENT} from './action-types';

export function* watchIncrementAsync() {
    for (let i = 0; i < 3; i++) {
        const action = yield take(INCREMENT_ASYNC);
        console.log(action);
        yield put({type:INCREMENT});
    }
    alert('最多只能点三次!');
}
export function* watchAndLog() {
    //take 就像我们更早之前看到的 call 和 put。它创建另一个命令对象,告诉 middleware 等待一个特定的 action
    //在 take 的情况中,它将会暂停 Generator 直到一个匹配的 action 被发起了
    //在 takeEvery 的情况中,被调用的任务无法控制何时被调用, 它们将在每次 action 被匹配时一遍又一遍地被调用。并且它们也无法控制何时停止监听
    //在 take 的情况中,控制恰恰相反。与 action 被 推向(pushed) 任务处理函数不同,Saga 是自己主动 拉取(pulling) action 的。 看起来就像是 Saga 在执行一个普通的函数调用 action = getNextAction(),这个函数将在 action 被发起时 resolve
    while(true){
        let action = yield take('*');
        const state = yield select();
        console.log('action', action);
        console.log('state after', state);
    }
}

export default function* rootSaga() {
    yield all([
        watchAndLog(),
        watchIncrementAsync()
    ])
  }

11. 登陆流程 #

11.1 src/index.js #

src/index.js

import React from 'react'
import ReactDOM from 'react-dom';
import Login from './components/Login';
import {Provider} from 'react-redux';
import store from './store';
ReactDOM.render(<Provider store={store}>
  <Login/>
</Provider>,document.querySelector('#root'));

11.2 store/action-types.js #

src/store/action-types.js

export const INCREMENT='INCREMENT';
export const INCREMENT_ASYNC='INCREMENT_ASYNC';
export const LOGIN_REQUEST='LOGIN_REQUEST';
export const LOGIN_SUCCESS='LOGIN_SUCCESS';
export const SET_USERNAME='SET_USERNAME';
export const LOGIN_ERROR='LOGIN_ERROR';
export const LOGOUT='LOGOUT';

11.3 store/actions.js #

src/store/actions.js

import * as types from './action-types';
export default {
    incrementAsync() {
        return {type:types.INCREMENT_ASYNC}
    },
    login(username,password){
        return {type:types.LOGIN_REQUEST,username,password}
    },
    logout(){
        return {type:types.LOGOUT}
    }
}

11.4 store/reducer.js #

src/store/reducer.js

import * as types from './action-types';
export default function (state={number:0,username:null},action) {
    switch(action.type){
        case types.INCREMENT:
            return {number: state.number+1};
        case types.LOGIN_ERROR:
            return {error: action.error};
        case types.SET_USERNAME:
            return {username: action.username};
        default:
            return state;
    }
}

11.5 store/sagas.js #

src/store/sagas.js

import { call, all, put, take } from "redux-saga/effects";
import {LOGIN_ERROR,LOGIN_REQUEST,SET_USERNAME,LOGOUT} from "./action-types";
import Api from "../Api";
//首先我们创建了一个独立的 Generator login,它将执行真实的 API 调用并在成功后通知 Store。
function* login(username, password) {
  try {
    //如果 Api 调用成功了,login 将发起一个 LOGIN_SUCCESS action 然后返回获取到的 token。 如果调用导致了错误,将会发起一个 LOGIN_ERROR action。
    const token = yield call(Api.login, username, password);
    return token;
  } catch (error) {
    alert(error);
    //在 login 失败的情况下,它将返回一个 undefined 值,这将导致 loginFlow 跳过当前处理进程并等待一个新的 LOGIN_REQUEST action
    yield put({
      type: LOGIN_ERROR,
      error
    });
  }
}

function* loginFlow() {
  //一旦到达流程最后一步(LOGOUT),通过等待一个新的 LOGIN_REQUEST action 来启动一个新的迭代
  while (true) {
    //loginFlow 首先等待一个 LOGIN_REQUEST action,然后调用一个 call 到 login 任务
    //call 不仅可以用来调用返回 Promise 的函数。我们也可以用它来调用其他 Generator 函数
    //loginFlow 将等待 login 直到它终止或返回(即执行 api 调用后,发起 action 然后返回 token 至 loginFlow)
    const { username, password } = yield take(LOGIN_REQUEST);
    const token = yield call(login, username, password);
    //在 login 失败的情况下,它将返回一个 undefined 值,这将导致 loginFlow 跳过当前处理进程并等待一个新的 LOGIN_REQUEST action
    if (token) {
      yield put({
        type: SET_USERNAME,
        username
      });
      //如果调用 login 成功,loginFlow 将在 DOM storage 中存储返回的 token,并等待 LOGOUT action
      Api.storeItem("token", token);
      //当用户登出,我们删除存储的 token 并等待一个新的用户登录
      yield take(LOGOUT);
      Api.clearItem("token");
      yield put({
        type: SET_USERNAME,
        username: null
      });
    }
  }
}

export default function* rootSaga() {
  yield all([loginFlow()]);
}

11.6 src/Api.js #

src/Api.js

export default {
    login(username, password){
        return new Promise(function(resolve,reject){
            setTimeout(()=>{
                if(Math.random()>.5){
                    resolve(username+'-'+password);
                }else{
                    reject('登录失败');
                }
            },1000);
        });
    },
    storeItem(key,value){
        localStorage.setItem(key,value);
    },
    clearItem(){
        localStorage.removeItem('token');
    }
}

11.7 components/Login.js #

src/components/Login.js

import React,{Component} from 'react'
import {connect} from 'react-redux';
import actions from '../store/actions';
class Login extends Component{
    constructor(props){
        super(props);
        this.username=React.createRef();
        this.password=React.createRef();
    }
    login = (event)=>{
        event.preventDefault();
        let username = this.username.current.value;
        let password = this.password.current.value;
        this.props.login(username,password);
    }
    logout = (event)=>{
        event.preventDefault();
        this.props.logout();
    }
    render() {
        let {username} = this.props;
        let loginForm = (
            <form>
                <label>用户名</label><input ref={this.username}/><br/>
                <label>密码</label><input ref={this.password}/><br/>
                <button onClick={this.login}>登录</button>
            </form>
        )
        let logoutForm = (
            <form >
                 用户名:{username}<br/>
                <button onClick={this.logout}>退出</button>
            </form>
        )
        return (
            username?logoutForm:loginForm
      )
  }
}
export default connect(
    state => state,
    actions
)(Login);

12. fork #

12.1 store/sagas.js #

src/store/sagas.js

import {call,all,put,take,fork} from 'redux-saga/effects'
import {LOGIN_ERROR,LOGOUT,LOGIN_REQUEST,LOGIN_SUCCESS,SET_USERNAME} from './action-types';
import Api from '../Api'
//首先我们创建了一个独立的 Generator login,它将执行真实的 API 调用并在成功后通知 Store。
function* login(username, password) {
    try {
        //如果 Api 调用成功了,login 将发起一个 LOGIN_SUCCESS action 然后返回获取到的 token。 如果调用导致了错误,将会发起一个 LOGIN_ERROR action。
        const token = yield call(Api.login, username, password);
        yield put({type: LOGIN_SUCCESS, token});
        yield put({type: SET_USERNAME, username});
        //如果调用 login 成功,loginFlow 将在 DOM storage 中存储返回的 token,并等待 LOGOUT action   
        Api.storeItem('token',token);
    } catch(error) {
        //在 login 失败的情况下,它将返回一个 undefined 值,这将导致 loginFlow 跳过当前处理进程并等待一个新的 LOGIN_REQUEST action
        yield put({type: LOGIN_ERROR, error});
    }
  }

  function* loginFlow() {
    //一旦到达流程最后一步(LOGOUT),通过等待一个新的 LOGIN_REQUEST action 来启动一个新的迭代
    while(true) {
        //loginFlow 首先等待一个 LOGIN_REQUEST action,然后调用一个 call 到 login 任务
        //call 不仅可以用来调用返回 Promise 的函数。我们也可以用它来调用其他 Generator 函数
        //loginFlow 将等待 login 直到它终止或返回(即执行 api 调用后,发起 action 然后返回 token 至 loginFlow)
        const {username, password} = yield take(LOGIN_REQUEST);
        //自从 login 的 action 在后台启动之后,我们获取不到 token 的结果,所以我们需要将 token 存储操作移到 login 任务内部
        yield fork(login, username, password);
        //yield take(['LOGOUT', 'LOGIN_ERROR'])。意思是监听 2 个并发的 action
        //如果 login 任务在用户登出之前成功了,它将会发起一个 LOGIN_SUCCESS action 然后结束。 然后 loginFlow Saga 只会等待一个未来的 LOGOUT action 被发起
        //如果 login 在用户登出之前失败了,它将会发起一个 LOGIN_ERROR action 然后结束
        //如果在 login 结束之前,用户就登出了,那么 loginFlow 将收到一个 LOGOUT action 并且也会等待下一个 LOGIN_REQUEST
        yield take([LOGOUT,LOGIN_ERROR]);
        Api.clearItem('token');
    }
  }
export default function* rootSaga() {
    yield all([
        loginFlow()
    ])
}

13. 取消任务 #

13.1 components/Login.js #

src/components/Login.js

class Login extends Component{
    render() {
        let {token} = this.props;
        let loginForm = (
            <form>
                <label>用户名</label><input ref={this.username}/><br/>
                <label>密码</label><input ref={this.password}/><br/>
                <button onClick={this.login}>登录</button>
+                <button onClick={this.logout}>退出</button>
            </form>
        )
  }
}

13.2 store/sagas.js #

src/store/sagas.js

import {call,all,put,take,fork,cancel,cancelled} from 'redux-saga/effects'
import {LOGIN_ERROR,LOGOUT,LOGIN_REQUEST,LOGIN_SUCCESS} from './action-types';
import Api from '../Api'

//首先我们创建了一个独立的 Generator login,它将执行真实的 API 调用并在成功后通知 Store。
function* login(username, password) {
    try {
        //如果 Api 调用成功了,login 将发起一个 LOGIN_SUCCESS action 然后返回获取到的 token。 如果调用导致了错误,将会发起一个 LOGIN_ERROR action。
        Api.storeItem('loading','true');
        const token = yield call(Api.login, username, password);
        yield put({type: LOGIN_SUCCESS, token});
        //如果调用 login 成功,loginFlow 将在 DOM storage 中存储返回的 token,并等待 LOGOUT action   
        Api.storeItem('token',token);
        Api.storeItem('loading','xx');
    } catch(error) {
        //在 login 失败的情况下,它将返回一个 undefined 值,这将导致 loginFlow 跳过当前处理进程并等待一个新的 LOGIN_REQUEST action
        yield put({type: LOGIN_ERROR, error});
        Api.storeItem('loading','false');
    } finally {
        console.log(cancelled())
        if (yield cancelled()) {
          // ... put special cancellation handling code here
          Api.storeItem('loading','false');
        }
      }
  }

  function* loginFlow() {
    //一旦到达流程最后一步(LOGOUT),通过等待一个新的 LOGIN_REQUEST action 来启动一个新的迭代
    while(true) {
        //loginFlow 首先等待一个 LOGIN_REQUEST action,然后调用一个 call 到 login 任务
        //call 不仅可以用来调用返回 Promise 的函数。我们也可以用它来调用其他 Generator 函数
        //loginFlow 将等待 login 直到它终止或返回(即执行 api 调用后,发起 action 然后返回 token 至 loginFlow)
        const {username, password} = yield take(LOGIN_REQUEST);
        //自从 login 的 action 在后台启动之后,我们获取不到 token 的结果,所以我们需要将 token 存储操作移到 login 任务内部
        //yield fork 的返回结果是一个 Task Object
        const task = yield fork(login, username, password);
        //yield take(['LOGOUT', 'LOGIN_ERROR'])。意思是监听 2 个并发的 action
        //如果 login 任务在用户登出之前成功了,它将会发起一个 LOGIN_SUCCESS action 然后结束。 然后 loginFlow Saga 只会等待一个未来的 LOGOUT action 被发起
        //如果 login 在用户登出之前失败了,它将会发起一个 LOGIN_ERROR action 然后结束
        //如果在 login 结束之前,用户就登出了,那么 loginFlow 将收到一个 LOGOUT action 并且也会等待下一个 LOGIN_REQUEST
        const action = yield take([LOGOUT,LOGIN_ERROR]);
        //将task 传入给 cancel Effect。 如果任务仍在运行,它会被中止,如果任务已完成,那什么也不会发生
        if(action.type == LOGOUT){
            yield cancel(task);
        }
        Api.clearItem('token');
    }
}
export default function* rootSaga() {
    yield all([
        loginFlow()
    ])
}

14. race #

14.1 src/index.js #

src/index.js

import React from 'react'
import ReactDOM from 'react-dom';
import Login from './components/Login';
import Recorder from './components/Recorder';
import {Provider} from 'react-redux';
import store from './store';
ReactDOM.render(<Provider store={store}>
  <Recorder/>
</Provider>,document.querySelector('#root'));

14.2 store/action-types.js #

src/store/action-types.js

export const CANCEL_TASK='CANCEL_TASK';

14.3 store/actions.js #

src/store/actions.js

stop(){
        return {type:types.CANCEL_TASK}
}

14.4 store/sagas.js #

src/store/sagas.js

import {call,all,put,take,race} from 'redux-saga/effects'
import {INCREMENT,CANCEL_TASK} from './action-types';
import {delay} from '../utils';
/**
 * 有时候我们同时启动多个任务,但又不想等待所有任务完成,我们只希望拿到 胜利者:即第一个被 resolve(或 reject)的任务
 * a=1000 b=undefined
 */
function* raceFlow() {
    const {a, b} = yield race({
        a: call(delay, 1000),
        b: call(delay, 2000)
    });
    console.log('a='+a,'b='+b);
}

//每隔一秒钟让数字加1

function* start() {
    while(true){
        yield call(delay,1000);
        yield put({type:INCREMENT});
    }
}
//race 的另一个有用的功能是,它会自动取消那些失败的 Effects
function* recorder() {
    yield race({
        start: call(start),
        stop: take(CANCEL_TASK)
    });
}
export default function* rootSaga() {
    //yield all([raceFlow()])
    yield all([recorder()])
}

14.5 components/Counter.js #

src/components/Counter.js

import React,{Component} from 'react'
import {connect} from 'react-redux';
import actions from '../store/actions';
class Counter extends Component{
    render() {
        return (
            <div>
                <p>{this.props.number}</p>
                <button onClick={this.props.stop}>停止</button>
            </div>
      )
  }
}
export default connect(
    state => state,
    actions
)(Counter);

参考 #