深刻理解React 高阶组件

React 中的五种组件形式

目前的前端开发主流技术都已经往组件化方向发展了,而每学一种新的框架的时候,最基础的部分必定是学习其组件的编写方式。这就好像学习一门新的编程语言的时候,老是要从hello world开始同样。而在React中,咱们经常使用的组件编写方式又有哪些呢?或者说各类不一样的组件又能够分为几类呢?css

无状态组件

无状态组件(Stateless Component)是最基础的组件形式,因为没有状态的影响因此就是纯静态展现的做用。通常来讲,各类UI库里也是最开始会开发的组件类别。如按钮、标签、输入框等。它的基本组成结构就是属性(props)加上一个渲染函数(render)。因为不涉及到状态的更新,因此这种组件的复用性也最强。html

const PureComponent = (props) => (
    <div>
        //use props
    </div>
)复制代码

无状态组件的写法十分简单,比起使用传统的组件定义方式,我一般就直接使用ES6语法中提供的箭头函数来声明这种组件形式。固然,若是碰到稍微复杂点的,可能还会带有生命周期的hook函数。这时候就须要用到Class Component的写法了。前端

有状态组件

在无状态组件的基础上,若是组件内部包含状态(state)且状态随着事件或者外部的消息而发生改变的时候,这就构成了有状态组件(Stateful Component)。有状态组件一般会带有生命周期(lifecycle),用以在不一样的时刻触发状态的更新。这种组件也是一般在写业务逻辑中最常用到的,根据不一样的业务场景组件的状态数量以及生命周期机制也不尽相同。node

class StatefulComponent extends Component {

    constructor(props) {
        super(props);
        this.state = {
            //定义状态
        }
    }

    componentWillMount() {
        //do something
    }

    componentDidMount() {
        //do something
    }
    ... //其余生命周期

    render() {
        return (
            //render
        );
    }
}复制代码

容器组件

在具体的项目实践中,咱们一般的前端数据都是经过Ajax请求获取的,并且获取的后端数据也须要进一步的作处理。为了使组件的职责更加单一,引入了容器组件(Container Component)的概念。咱们将数据获取以及处理的逻辑放在容器组件中,使得组件的耦合性进一步地下降。react

var UserListContainer = React.createClass({
  getInitialState: function() {
    return {
      users: []
    }
  },

  componentDidMount: function() {
    var _this = this;
    axios.get('/path/to/user-api').then(function(response) {
      _this.setState({users: response.data});
    });
  },

  render: function() {
    return (<UserList users={this.state.users} />);
  }
});复制代码

如上面这个容器组件,就是负责获取用户数据,而后以props的形式传递给UserList组件来渲染。容器组件也不会在页面中渲染出具体的DOM节点,所以,它一般就充当数据源的角色。目前不少经常使用的框架,也都采用这种组件形式。如:React Redux的connect(), Relay的createContainer(), Flux Utils的Container.create()等。ios

高阶组件

其实对于通常的中小项目来讲,你只须要用到以上的这三种组件方式就能够很好地构造出所需的应用了。可是当面对复杂的需求的时候,咱们每每能够利用高阶组件(Higher-Order Component)编写出可重用性更强的组件。那么什么是高阶组件呢?其实它和高阶函数的概念相似,就是一个会返回组件的组件。或者更确切地说,它实际上是一个会返回组件的函数。就像这样:git

const HigherOrderComponent = (WrappedComponent) => {
  return class WrapperComponent extends Component {
    render() {
      //do something with WrappedComponent
    }
  }
}复制代码

作为一个高阶组件,能够在原有组件的基础上,对其增长新的功能和行为。咱们通常但愿编写的组件尽可能纯净或者说其中的业务逻辑尽可能单一。可是若是各类组件间又须要增长新功能,如打印日志,获取数据和校验数据等和展现无关的逻辑的时候,这些公共的代码就会被重复写不少遍。所以,咱们能够抽象出一个高阶组件,用以给基础的组件增长这些功能,相似于插件的效果。es6

一个比较常见的例子是表单的校验。github

//检验规则,表格组件
const FormValidator = (WrappedComponent, validator, trigger) => {

   getTrigger(trigger, validator) {
      var originTrigger = this.props[trigger];

      return function(event) {
          //触发验证机制,更新状态
          // do something ...
          originTrigger(event);
      }
  }

  var newProps = {
    ...this.props,
    [trigger]:   this.getTrigger(trigger, validator) //触发时机,从新绑定原有触发机制
  };

  return <WrappedComponent  {...newProps} />
}复制代码

值得提一句,一样是给组件增长新功能的方法,相比于使用mixins这种方式高阶组件则更加简洁和职责更加单一。你若是使用过多个mixins的时候,状态污染就十分容易发生,以及你很难从组件的定义上看出隐含在mixins中的逻辑。而高阶组件的处理方式则更加容易维护。ajax

另外一方面,ES7中新的语法Decorator也能够用来实现和上面写法同样的效果。

function LogDecorator(msg) {
  return (WrappedComponent) => {
    return class LogHoc extends Component {
      render() {
        // do something with this component
        console.log(msg);
        <WrappedComponent {...this.props} />
      }
    }
  }
}

@LogDecorator('hello world')
class HelloComponent extends Component {

  render() {
    //...
  }
}复制代码

Render Callback组件

还有一种组件模式是在组件中使用渲染回调的方式,将组件中的渲染逻辑委托给其子组件。就像这样:

import { Component } from "react";

class RenderCallbackCmp extends Component {
  constructor(props) {
    super(props);
    this.state = {
      msg: "hello"
    };
  }

  render() {
    return this.props.children(this.state.msg);
  }
}

const ParentComponent = () =>
  (<RenderCallbackCmp>
    {msg =>
      //use the msg
      <div>
        {msg}
      </div>}
  </RenderCallbackCmp>);复制代码

父组件获取了内部的渲染逻辑,所以在须要控制渲染机制时可使用这种组件形式。







1. 基本概念

高阶组件是React 中一个很重要且较复杂的概念,高阶组件在不少第三方库(如Redux)中都被常用,即便你开发的是普通的业务项目,用好高阶组件也能显著提升你的代码质量。

高阶组件的定义是类比于高阶函数的定义。高阶函数接收函数做为参数,而且返回值也是一个函数。相似的,高阶组件接收React组件做为参数,而且返回一个新的React组件。高阶组件本质上也是一个函数,并非一个组件,这一点必定要注意。

2. 应用场景

为何React引入高阶组件的概念?它到底有何威力?让咱们先经过一个简单的例子说明一下。

假设我有一个组件,须要从LocalStorage中获取数据,而后渲染出来。因而咱们能够这样写组件代码:

import React, { Component } from 'react'

class MyComponent extends Component {

  componentWillMount() {
      let data = localStorage.getItem('data');
      this.setState({data});
  }

  render() {
    return <div>{this.state.data}</div>
  }
}复制代码

代码很简单,但当我有其余组件也须要从LocalStorage中获取一样的数据展现出来时,我须要在每一个组件都重复componentWillMount中的代码,这显然是很冗余的。下面让咱们来看看使用高阶组件能够怎么改写这部分代码。

import React, { Component } from 'react'

function withPersistentData(WrappedComponent) {
  return class extends Component {
    componentWillMount() {
      let data = localStorage.getItem('data');
        this.setState({data});
    }

    render() {
      // 经过{...this.props} 把传递给当前组件的属性继续传递给被包装的组件WrappedComponent
      return <WrappedComponent data={this.state.data} {...this.props} />
    }
  }
}

class MyComponent2 extends Component {  
  render() {
    return <div>{this.props.data}</div>
  }
}

const MyComponentWithPersistentData = withPersistentData(MyComponent2)复制代码

withPersistentData就是一个高阶组件,它返回一个新的组件,在新组件的componentWillMount中统一处理从LocalStorage中获取数据的逻辑,而后将获取到的数据以属性的方式传递给被包装的组件WrappedComponent,这样在WrappedComponent中就能够直接使用this.props.data获取须要展现的数据了,如MyComponent2所示。当有其余的组件也须要这段逻辑时,继续使用withPersistentData这个高阶组件包装这些组件就能够了。

经过这个例子,能够看出高阶组件的主要功能是封装并分离组件的通用逻辑,让通用逻辑在组件间更好地被复用。高阶组件的这种实现方式,本质上是一个装饰者设计模式。

高阶组件的参数并不是只能是一个组件,它还能够接收其余参数。例如,组件MyComponent3须要从LocalStorage中获取key为name的数据,而不是上面例子中写死的key为data的数据,withPersistentData这个高阶组件就不知足咱们的需求了。咱们可让它接收额外的一个参数,来决定从LocalStorage中获取哪一个数据:

import React, { Component } from 'react'

function withPersistentData(WrappedComponent, key) {
  return class extends Component {
    componentWillMount() {
      let data = localStorage.getItem(key);
        this.setState({data});
    }

    render() {
      // 经过{...this.props} 把传递给当前组件的属性继续传递给被包装的组件WrappedComponent
      return <WrappedComponent data={this.state.data} {...this.props} />
    }
  }
}

class MyComponent2 extends Component {  
  render() {
    return <div>{this.props.data}</div>
  }

  //省略其余逻辑...
}

class MyComponent3 extends Component {  
  render() {
    return <div>{this.props.data}</div>
  }

  //省略其余逻辑...
}

const MyComponent2WithPersistentData = withPersistentData(MyComponent2, 'data');
const MyComponent3WithPersistentData = withPersistentData(MyComponent3, 'name');复制代码

新版本的withPersistentData就知足咱们获取不一样key值的需求了。高阶组件中的参数固然也能够有函数,咱们将在下一节进一步说明。

3. 进阶用法

高阶组件最多见的函数签名形式是这样的:

HOC([param])([WrappedComponent])

用这种形式改写withPersistentData,以下:

import React, { Component } from 'react'

function withPersistentData = (key) => (WrappedComponent) => {
  return class extends Component {
    componentWillMount() {
      let data = localStorage.getItem(key);
        this.setState({data});
    }

    render() {
      // 经过{...this.props} 把传递给当前组件的属性继续传递给被包装的组件WrappedComponent
      return <WrappedComponent data={this.state.data} {...this.props} />
    }
  }
}

class MyComponent2 extends Component {  
  render() {
    return <div>{this.props.data}</div>
  }

  //省略其余逻辑...
}

class MyComponent3 extends Component {  
  render() {
    return <div>{this.props.data}</div>
  }

  //省略其余逻辑...
}

const MyComponent2WithPersistentData = withPersistentData('data')(MyComponent2);
const MyComponent3WithPersistentData = withPersistentData('name')(MyComponent3);复制代码

实际上,此时的withPersistentData和咱们最初对高阶组件的定义已经不一样。它已经变成了一个高阶函数,但这个高阶函数的返回值是一个高阶组件。咱们能够把它当作高阶组件的变种形式。这种形式的高阶组件大量出如今第三方库中。如react-redux中的connect就是一个典型。connect的定义以下:

connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])复制代码

这个函数会将一个React组件链接到Redux 的 store。在链接的过程当中,connect经过函数参数mapStateToProps,从全局store中取出当前组件须要的state,并把state转化成当前组件的props;同时经过函数参数mapDispatchToProps,把当前组件用到的Redux的action creator,以props的方式传递给当前组件。connect并不会修改传递进去的组件的定义,而是它会返回一个新的组件。

例如,咱们把组件ComponentA链接到Redux上的写法相似于:

const ConnectedComponentA = connect(componentASelector, componentAActions)(ComponentA);复制代码

咱们能够把它拆分来看:

// connect 是一个函数,返回值enhance也是一个函数
const enhance = connect(componentASelector, componentAActions);
// enhance是一个高阶组件
const ConnectedComponentA = enhance(ComponentA);复制代码

当多个函数的输出和它的输入类型相同时,这些函数是很容易组合到一块儿使用的。例如,有f,g,h三个高阶组件,都只接受一个组件做为参数,因而咱们能够很方便的嵌套使用它们:f( g( h(WrappedComponent) ) )。这里能够有一个例外,即最内层的高阶组件h能够有多个参数,但其余高阶组件必须只能接收一个参数,只有这样才能保证内层的函数返回值和外层的函数参数数量一致(都只有1个)。

例如咱们将connect和另外一个打印日志的高阶组件withLog联合使用:

const ConnectedComponentA = connect(componentASelector)(withLog(ComponentA));复制代码

这里咱们定义一个工具函数:compose(...functions),调用compose(f, g, h)等价于 (...args) => f(g(h(...args)))。用compose函数咱们能够把高阶组件嵌套的写法打平:

const enhance = compose(
  connect(componentASelector),
  withLog
);
const ConnectedComponentA = enhance(ComponentA);复制代码

像Redux等不少第三方库都提供了compose的实现,compose结合高阶组件使用,能够显著提升代码的可读性和逻辑的清晰度。

4.与父组件区别

有些同窗可能会以为高阶组件有些相似父组件的使用。例如,咱们彻底能够把高阶组件中的逻辑放到一个父组件中去执行,执行完成的结果再传递给子组件。从逻辑的执行流程上来看,高阶组件确实和父组件比较相像,可是高阶组件强调的是逻辑的抽象。高阶组件是一个函数,函数关注的是逻辑;父组件是一个组件,组件主要关注的是UI/DOM。若是逻辑是与DOM直接相关的,那么这部分逻辑适合放到父组件中实现;若是逻辑是与DOM不直接相关的,那么这部分逻辑适合使用高阶组件抽象,如数据校验、请求发送等。

5. 注意事项

1)不要在组件的render方法中使用高阶组件,尽可能也不要在组件的其余生命周期方法中使用高阶组件。由于高阶组件每次都会返回一个新的组件,在render中使用会致使每次渲染出来的组件都不相等(===),因而每次render,组件都会卸载(unmount),而后从新挂载(mount),既影响了效率,又丢失了组件及其子组件的状态。高阶组件最适合使用的地方是在组件定义的外部,这样就不会受到组件生命周期的影响了。

2)若是须要使用被包装组件的静态方法,那么必须手动拷贝这些静态方法。由于高阶组件返回的新组件,是不包含被包装组件的静态方法。hoist-non-react-statics能够帮助咱们方便的拷贝组件全部的自定义静态方法。有兴趣的同窗能够自行了解。

3)Refs不会被传递给被包装组件。尽管在定义高阶组件时,咱们会把全部的属性都传递给被包装组件,可是ref并不会传递给被包装组件,由于ref根本不属于React组件的属性。若是你在高阶组件的返回组件中定义了ref,那么它指向的是这个返回的新组件,而不是内部被包装的组件。若是你但愿获取被包装组件的引用,你能够把ref的回调函数定义成一个普通属性(给它一个ref之外的名字)。下面的例子就用inputRef这个属性名代替了常规的ref命名:

function FocusInput({ inputRef, ...rest }) {
  return <input ref={inputRef} {...rest} />;
}

//enhance 是一个高阶组件
const EnhanceInput = enhance(FocusInput);

// 在一个组件的render方法中...
return (<EnhanceInput 
  inputRef={(input) => {
    this.input = input
  }
}>)

// 让FocusInput自动获取焦点
this.input.focus();复制代码




- 首先咱们来看看登录的 Reducer

export const auth = (state = initialState, action = {}) => {
  switch (action.type) {
    case LOGIN_USER:
      return state.merge({
        'user': action.data,
        'error': null,
        'token': null,
      });
    case LOGIN_USER_SUCCESS:
      return state.merge({
        'token': action.data,
        'error': null
      });
    case LOGIN_USER_FAILURE:
      return state.merge({
        'token': null,
        'error': action.data
      });
    default:
      return state
  }
};
复制代码

Sagas 监听发起的 action,而后决定基于这个 action 来作什么:是发起一个异步调用(好比一个 Ajax 请求),仍是发起其余的 action 到 Store,甚至是调用其余的 Sagas。

具体到这个登录功能就是咱们在登录弹窗点击登录时会发出一个 LOGIN_USER action,Sagas 监听到 LOGIN_USER action,发起一个 Ajax 请求到后台,根据结果决定发起 LOGIN_USER_SUCCESSaction 仍是LOGIN_USER_FAILUREaction

接下来,咱们来实现这个流程

  • 建立 Saga middleware 链接至 Redux store

在 package.json 中添加 redux-saga 依赖

"redux-saga": "^0.15.4"

修改 src/redux/store/store.js

/**
 * Created by Yuicon on 2017/6/27.
 */
import {createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga'
import reducer from '../reducer/reducer';

import rootSaga from '../sagas/sagas';

const sagaMiddleware = createSagaMiddleware();

const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
);

sagaMiddleware.run(rootSaga);

export default store;

复制代码

Redux-saga 使用 Generator 函数实现

  • 监听 action

建立 src/redux/sagas/sagas.js

/**
 * Created by Yuicon on 2017/6/28.
 */
import { takeLatest } from 'redux-saga/effects';
import {registerUserAsync, loginUserAsync} from './users';
import {REGISTER_USER, LOGIN_USER} from '../action/users';

export default function* rootSaga() {
  yield [
    takeLatest(REGISTER_USER, registerUserAsync),
    takeLatest(LOGIN_USER, loginUserAsync)
  ];
}
复制代码

咱们能够看到在 rootSaga 中监听了两个 action 登录和注册 。

在上面的例子中,takeLatest 只容许执行一个 loginUserAsync 任务。而且这个任务是最后被启动的那个。 若是以前已经有一个任务在执行,那以前的这个任务会自动被取消。

若是咱们容许多个 loginUserAsync 实例同时启动。在某个特定时刻,咱们能够启动一个新 loginUserAsync 任务, 尽管以前还有一个或多个 loginUserAsync 还没有结束。咱们可使用 takeEvery 辅助函数。

  • 发起一个 Ajax 请求
  • 获取 Store state 上的数据

selectors.js

/**
 * Created by Yuicon on 2017/6/28.
 */
export const getAuth = state => state.auth;
复制代码
  • api

api.js

/**
 * Created by Yuicon on 2017/7/4.
 * https://github.com/Yuicon
 */

/**
 * 这是我本身的后台服务器,用 Java 实现
 * 项目地址:https://github.com/DigAg/digag-server
 * 文档:http://139.224.135.86:8080/swagger-ui.html#/
 */
const getURL = (url) => `http://139.224.135.86:8080/${url}`;

export const login = (user) => {
  return fetch(getURL("auth/login"), {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(user)
  }).then(response => response.json())
    .then(json => {
      return json;
    })
    .catch(ex => console.log('parsing failed', ex));
};

复制代码
  • 建立 src/redux/sagas/users.js
/**
 * Created by Yuicon on 2017/6/30.
 */
import {select, put, call} from 'redux-saga/effects';
import {getAuth, getUsers} from './selectors';
import {loginSuccessAction, loginFailureAction, registerSuccessAction, registerFailureAction} from '../action/users';
import {login, register} from './api';
import 'whatwg-fetch';

export function* loginUserAsync() {
  // 获取Store state 上的数据
  const auth = yield select(getAuth);
  const user = auth.get('user');
  // 发起 ajax 请求
  const json = yield call(login.bind(this, user), 'login');
  if (json.success) {
    localStorage.setItem('token', json.data);
    // 发起 loginSuccessAction
    yield put(loginSuccessAction(json.data));
  } else {
    // 发起 loginFailureAction
    yield put(loginFailureAction(json.error));
  }
}
复制代码

select(selector, ...args) 用于获取Store state 上的数据
put(action) 发起一个 action 到 Store
call(fn, ...args) 调用 fn 函数并以 args 为参数,若是结果是一个 Promise,middleware 会暂停直到这个 Promise 被 resolve,resolve 后 Generator 会继续执行。 或者直到 Promise 被 reject 了,若是是这种状况,将在 Generator 中抛出一个错误。

Redux-saga 详细api文档

  • 结语

我在工做时用的是 Redux-Thunk, Redux-Thunk 相对来讲更容易实现和维护。可是对于复杂的操做,尤为是面对复杂异步操做时,Redux-saga 更有优点。到此咱们完成了一个 Redux-saga 的入门教程,Redux-saga 还有不少奇妙的地方,你们能够自行探索。

上回说到用React写了一个带Header的首页,咱们此次实践就使用Redux进行状态管理

Rudex

应用中全部的 state 都以一个对象树的形式储存在一个单一的 store 中。
唯一改变 state 的办法是触发 action,一个描述发生什么的对象。
为了描述 action 如何改变 state 树,你须要编写 reducers。

咱们接下来开始开始进行登录与注册的状态管理

首先在 src 目录下建立 redux 文件夹,目录以下

digag
├── README.md
├── node_modules
├── package.json
├── .gitignore
├── public
│   └── favicon.ico
│   └── index.html
│   └── manifest.json
└── src
    └── components
        └── Index
            └── Header.js
            └── LoginDialog.js
            └── RegisterDialog.js
    └── containers
        └── App
            └── App.js
            └── App.css
    └── redux
        └── action
            └── users.js
        └── reducer
            └── auth.js
            └── users.js
        └── sagas
            └── api.js
            └── sagas.js
            └── selectors.js.js
            └── users.js
        └── store
            └── store.js
    └── App.test.js
    └── index.css
    └── index.js
    └── logo.svg
    └── registerServiceWorker.js
复制代码

代码可今后获取

记得在 package.json 中更新依赖

接下来我会开始解释关键代码

  • action
    action/users.js
/*
 * action 类型
 */
export const REGISTER_USER = 'REGISTER_USER';
// 省略其余action 类型

/*
 * action 建立函数
 */
export const registerAction = (newUser) => {
  return{
    type:REGISTER_USER,
    data: newUser,
  }
};
// 省略其余 action 建立函数
复制代码
  • reducer
    reducer/users.js
//Immutable Data 就是一旦建立,就不能再被更改的数据。
//对 Immutable 对象的任何修改或添加删除操做都会返回一个新的 Immutable 对象。
import Immutable from 'immutable';
//从 action 导入须要的 action 类型
import {REGISTER_USER, REGISTER_USER_SUCCESS, REGISTER_USER_FAILURE} from '../action/users';

// 初始化状态
const initialState = Immutable.fromJS({
  newUser: null,
  error: null,
  saveSuccess: false,
});

//  reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。
export const users = (state = initialState, action = {}) => {
  switch (action.type) { // 判断 action 类型
    case REGISTER_USER:  
      return state.merge({   // 更新状态
        'newUser': action.data,
        'saveSuccess': false,
        'error': null,
      });
    case REGISTER_USER_SUCCESS:
      return state.set('saveSuccess', action.data);
    case REGISTER_USER_FAILURE:
      return state.set('error', action.data);
    default:
      return state
  }
};
复制代码
  • store
    store/store.js
import {createStore, combineReducers, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga'
import * as reducer from '../reducer/users';

import rootSaga from '../sagas/sagas';

const sagaMiddleware = createSagaMiddleware();

const store = createStore(
  combineReducers(reducer),
  applyMiddleware(sagaMiddleware)
);

sagaMiddleware.run(rootSaga);

export default store;
复制代码

而后在入口文件使用 store

src/index.js

import {Provider} from 'react-redux';
import store from './redux/store/store';
// 省略其余

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>, document.getElementById('root')
);
复制代码

在 App.js 中获取 action 和 状态

import {registerAction, loginAction} from '../../redux/action/users';
import {connect} from "react-redux";
import {bindActionCreators} from "redux";
 //省略其余

class App extends Component {

  render(){
    return(
      <div className="App">
        //省略
      </div>
    )
  }

}

export default connect(
  (state) => {
// 获取状态   state.users  是指 reducer/users.js 文件中导出的 users
// 能够 `console.log(state);` 查看状态树
  return { users: state.users }
},
  (dispatch) => {
  return {
// 建立action
    registerActions: bindActionCreators(registerAction, dispatch),
    loginActions: bindActionCreators(loginAction, dispatch),
  }
})(App);
// 在App 组件的props里就有 this.props.users  this.props.registerActions this.props.loginActions 了
// 须要注意的是这里this.props.users是Immutable 对象,取值须要用this.props.users.get('newUser') 
// 也可在 reducer 里改用 js 普通对象
复制代码

装饰器版本:
须要在Babel中开启装饰器
装饰器插件babel-plugin-transform-decorators-legacy

@connect(
  (state) => {
    console.log(state);
    return ({
      users: state.users,
    });
  },
  {registerActions: registerAction, loginActions: loginAction}
)
复制代码

最后把 registerActions 传给RegisterDialog子组件,

src/components/Index/RegisterDialog.js

// 省略其余代码
 handleSubmit = (e) => {
    e.preventDefault();
    // 验证表单数据
    this.refs.user.validate((valid) => {
      if (valid) {
        // this.state.user 为表单收集的 用户注册数据
        this.props.registerActions(this.state.user);
        this.setState({loading: true});
      }
    });
  };

复制代码

流程是:

  • 调用 action
    this.props.registerActions(this.state.user);
    返回action 为
{
    type:REGISTER_USER,
    data: this.state.user,
}
复制代码
  • reducer 根据action类型更新状态
switch (action.type) {
    case REGISTER_USER:
      return state.merge({
        'newUser': action.data,
        'saveSuccess': false,
        'error': null,
      });
//省略其余代码
复制代码

这时咱们的store里的状态 newUser就被更新为 注册弹窗里收集的数据
到这里都仍是同步的action,而注册是一个异步的操做。
下篇文章会介绍如何使用 redux-saga 进行异步操做。
redux-saga 已经在使用了,有兴趣的能够自行查看代码理解。

- 首先咱们来看看登录的 Reducer

export const auth = (state = initialState, action = {}) => {
  switch (action.type) {
    case LOGIN_USER:
      return state.merge({
        'user': action.data,
        'error': null,
        'token': null,
      });
    case LOGIN_USER_SUCCESS:
      return state.merge({
        'token': action.data,
        'error': null
      });
    case LOGIN_USER_FAILURE:
      return state.merge({
        'token': null,
        'error': action.data
      });
    default:
      return state
  }
};
复制代码

Sagas 监听发起的 action,而后决定基于这个 action 来作什么:是发起一个异步调用(好比一个 Ajax 请求),仍是发起其余的 action 到 Store,甚至是调用其余的 Sagas。

具体到这个登录功能就是咱们在登录弹窗点击登录时会发出一个 LOGIN_USER action,Sagas 监听到 LOGIN_USER action,发起一个 Ajax 请求到后台,根据结果决定发起 LOGIN_USER_SUCCESSaction 仍是LOGIN_USER_FAILUREaction

接下来,咱们来实现这个流程

  • 建立 Saga middleware 链接至 Redux store

在 package.json 中添加 redux-saga 依赖

"redux-saga": "^0.15.4"

修改 src/redux/store/store.js

/**
 * Created by Yuicon on 2017/6/27.
 */
import {createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga'
import reducer from '../reducer/reducer';

import rootSaga from '../sagas/sagas';

const sagaMiddleware = createSagaMiddleware();

const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
);

sagaMiddleware.run(rootSaga);

export default store;

复制代码

Redux-saga 使用 Generator 函数实现

  • 监听 action

建立 src/redux/sagas/sagas.js

/**
 * Created by Yuicon on 2017/6/28.
 */
import { takeLatest } from 'redux-saga/effects';
import {registerUserAsync, loginUserAsync} from './users';
import {REGISTER_USER, LOGIN_USER} from '../action/users';

export default function* rootSaga() {
  yield [
    takeLatest(REGISTER_USER, registerUserAsync),
    takeLatest(LOGIN_USER, loginUserAsync)
  ];
}
复制代码

咱们能够看到在 rootSaga 中监听了两个 action 登录和注册 。

在上面的例子中,takeLatest 只容许执行一个 loginUserAsync 任务。而且这个任务是最后被启动的那个。 若是以前已经有一个任务在执行,那以前的这个任务会自动被取消。

若是咱们容许多个 loginUserAsync 实例同时启动。在某个特定时刻,咱们能够启动一个新 loginUserAsync 任务, 尽管以前还有一个或多个 loginUserAsync 还没有结束。咱们可使用 takeEvery 辅助函数。

  • 发起一个 Ajax 请求
  • 获取 Store state 上的数据

selectors.js

/**
 * Created by Yuicon on 2017/6/28.
 */
export const getAuth = state => state.auth;
复制代码
  • api

api.js

/**
 * Created by Yuicon on 2017/7/4.
 * https://github.com/Yuicon
 */

/**
 * 这是我本身的后台服务器,用 Java 实现
 * 项目地址:https://github.com/DigAg/digag-server
 * 文档:http://139.224.135.86:8080/swagger-ui.html#/
 */
const getURL = (url) => `http://139.224.135.86:8080/${url}`;

export const login = (user) => {
  return fetch(getURL("auth/login"), {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(user)
  }).then(response => response.json())
    .then(json => {
      return json;
    })
    .catch(ex => console.log('parsing failed', ex));
};

复制代码
  • 建立 src/redux/sagas/users.js
/**
 * Created by Yuicon on 2017/6/30.
 */
import {select, put, call} from 'redux-saga/effects';
import {getAuth, getUsers} from './selectors';
import {loginSuccessAction, loginFailureAction, registerSuccessAction, registerFailureAction} from '../action/users';
import {login, register} from './api';
import 'whatwg-fetch';

export function* loginUserAsync() {
  // 获取Store state 上的数据
  const auth = yield select(getAuth);
  const user = auth.get('user');
  // 发起 ajax 请求
  const json = yield call(login.bind(this, user), 'login');
  if (json.success) {
    localStorage.setItem('token', json.data);
    // 发起 loginSuccessAction
    yield put(loginSuccessAction(json.data));
  } else {
    // 发起 loginFailureAction
    yield put(loginFailureAction(json.error));
  }
}
复制代码

select(selector, ...args) 用于获取Store state 上的数据
put(action) 发起一个 action 到 Store
call(fn, ...args) 调用 fn 函数并以 args 为参数,若是结果是一个 Promise,middleware 会暂停直到这个 Promise 被 resolve,resolve 后 Generator 会继续执行。 或者直到 Promise 被 reject 了,若是是这种状况,将在 Generator 中抛出一个错误。

Redux-saga 详细api文档

  • 结语

我在工做时用的是 Redux-Thunk, Redux-Thunk 相对来讲更容易实现和维护。可是对于复杂的操做,尤为是面对复杂异步操做时,Redux-saga 更有优点。到此咱们完成了一个 Redux-saga 的入门教程,Redux-saga 还有不少奇妙的地方,你们能够自行探索。

相关文章
相关标签/搜索