Redux 入门教程(二):中间件与异步操作

hwh1990 8年前
   <p><a href="http://www.open-open.com/lib/view/open1474426332980.html">上一篇文章</a>,我介绍了 Redux 的基本做法:用户发出 Action,Reducer 函数算出新的 State,View 重新渲染。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/bdbf6018f45b1641dcd03dba7391d0ba.jpg"></p>    <p>但是,一个关键问题没有解决:异步操作怎么办?Action 发出以后,Reducer 立即算出 State,这叫做同步;Action 发出以后,过一段时间再执行 Reducer,这就是异步。</p>    <p>怎么才能 Reducer 在异步操作结束后自动执行呢?这就要用到新的工具:中间件(middleware)。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/18bcc6b6df4023b7cc94898f9501a8f4.jpg"></p>    <h2><strong>一、中间件的概念</strong></h2>    <p>为了理解中间件,让我们站在框架作者的角度思考问题:如果要添加功能,你会在哪个环节添加?</p>    <p>(1)Reducer:纯函数,只承担计算 State 的功能,不合适承担其他功能,也承担不了,因为理论上,纯函数不能进行读写操作。</p>    <p>(2)View:与 State 一一对应,可以看作 State 的视觉层,也不合适承担其他功能。</p>    <p>(3)Action:存放数据的对象,即消息的载体,只能被别人操作,自己不能进行任何操作。</p>    <p>想来想去,只有发送 Action 的这个步骤,即 store.dispatch() 方法,可以添加功能。举例来说,要添加日志功能,把 Action 和 State 打印出来,可以对 store.dispatch 进行如下改造。</p>    <pre>  <code class="language-javascript">let next = store.dispatch;  store.dispatch = function dispatchAndLog(action) {    console.log('dispatching', action);    next(action);    console.log('next state', store.getState());  }</code></pre>    <p>上面代码中,对 store.dispatch 进行了重定义,在发送 Action 前后添加了打印功能。这就是中间件的雏形。</p>    <p>中间件就是一个函数,对 store.dispatch 方法进行了改造,在发出 Action 和执行 Reducer 这两步之间,添加了其他功能。</p>    <h2><strong>二、中间件的用法</strong></h2>    <p>本教程不涉及如何编写中间件,因为常用的中间件都有现成的,只要引用别人写好的模块即可。比如,上一节的日志中间件,就有现成的 <a href="/misc/goto?guid=4959716596875596096" rel="nofollow,noindex">redux-logger</a> 模块。这里只介绍怎么使用中间件。</p>    <pre>  <code class="language-javascript">import { applyMiddleware, createStore } from 'redux';  import createLogger from 'redux-logger';  const logger = createLogger();    const store = createStore(    reducer,    applyMiddleware(logger)  );</code></pre>    <p>上面代码中, redux-logger 提供一个生成器 createLogger ,可以生成日志中间件 logger 。然后,将它放在 applyMiddleware 方法之中,传入 createStore 方法,就完成了 store.dispatch() 的功能增强。</p>    <p>这里有两点需要注意:</p>    <p>(1) createStore 方法可以接受整个应用的初始状态作为参数,那样的话, applyMiddleware 就是第三个参数了。</p>    <pre>  <code class="language-javascript">const store = createStore(    reducer,    initial_state,    applyMiddleware(logger)  );</code></pre>    <p>(2)中间件的次序有讲究。</p>    <pre>  <code class="language-javascript">const store = createStore(    reducer,    applyMiddleware(thunk, promise, logger)  );</code></pre>    <p>上面代码中, applyMiddleware 方法的三个参数,就是三个中间件。有的中间件有次序要求,使用前要查一下文档。比如, logger 就一定要放在最后,否则输出结果会不正确。</p>    <h2><strong>三、applyMiddlewares()</strong></h2>    <p>看到这里,你可能会问, applyMiddlewares 这个方法到底是干什么的?</p>    <p>它是 Redux 的原生方法,作用是将所有中间件组成一个数组,依次执行。下面是它的源码。</p>    <pre>  <code class="language-javascript">export default function applyMiddleware(...middlewares) {    return (createStore) => (reducer, preloadedState, enhancer) => {      var store = createStore(reducer, preloadedState, enhancer);      var dispatch = store.dispatch;      var chain = [];        var middlewareAPI = {        getState: store.getState,        dispatch: (action) => dispatch(action)      };      chain = middlewares.map(middleware => middleware(middlewareAPI));      dispatch = compose(...chain)(store.dispatch);        return {...store, dispatch}    }  }</code></pre>    <p>上面代码中,所有中间件被放进了一个数组 chain ,然后嵌套执行,最后执行 store.dispatch 。可以看到,中间件内部( middlewareAPI )可以拿到 getState 和 dispatch 这两个方法。</p>    <h2><strong>四、异步操作的基本思路</strong></h2>    <p>理解了中间件以后,就可以处理异步操作了。</p>    <p>同步操作只要发出一种 Action 即可,异步操作的差别是它要发出三种 Action。</p>    <ul>     <li>操作发起时的 Action</li>     <li>操作成功时的 Action</li>     <li>操作失败时的 Action</li>    </ul>    <p>以向服务器取出数据为例,三种 Action 可以有两种不同的写法。</p>    <pre>  <code class="language-javascript">// 写法一:名称相同,参数不同  { type: 'FETCH_POSTS' }  { type: 'FETCH_POSTS', status: 'error', error: 'Oops' }  { type: 'FETCH_POSTS', status: 'success', response: { ... } }    // 写法二:名称不同  { type: 'FETCH_POSTS_REQUEST' }  { type: 'FETCH_POSTS_FAILURE', error: 'Oops' }  { type: 'FETCH_POSTS_SUCCESS', response: { ... } }</code></pre>    <p>除了 Action 种类不同,异步操作的 State 也要进行改造,反映不同的操作状态。下面是 State 的一个例子。</p>    <pre>  <code class="language-javascript">let state = {    // ...     isFetching: true,    didInvalidate: true,    lastUpdated: 'xxxxxxx'  };</code></pre>    <p>上面代码中,State 的属性 isFetching 表示是否在抓取数据。 didInvalidate 表示数据是否过时, lastUpdated 表示上一次更新时间。</p>    <p>现在,整个异步操作的思路就很清楚了。</p>    <ul>     <li>操作开始时,送出一个 Action,触发 State 更新为"正在操作"状态,View 重新渲染</li>     <li>操作结束后,再送出一个 Action,触发 State 更新为"操作结束"状态,View 再一次重新渲染</li>    </ul>    <h2><strong>五、redux-thunk 中间件</strong></h2>    <p>异步操作至少要送出两个 Action:用户触发第一个 Action,这个跟同步操作一样,没有问题;如何才能在操作结束时,系统自动送出第二个 Action 呢?</p>    <p>奥秒就在 Action Creator 之中。</p>    <pre>  <code class="language-javascript">class AsyncApp extends Component {    componentDidMount() {      const { dispatch, selectedPost } = this.props      dispatch(fetchPosts(selectedPost))    }    // ...</code></pre>    <p>上面代码是一个异步组件的例子。加载成功后( componentDidMount 方法),它送出了( dispatch 方法)一个 Action,向服务器要求数据 fetchPosts(selectedSubreddit) 。这里的 fetchPosts 就是 Action Creator。</p>    <p>下面就是 fetchPosts 的代码,关键之处就在里面。</p>    <p><img src="https://simg.open-open.com/show/b99a9c7d00e3ea0454fb3aaf8c3d83fa.jpg"></p>    <pre>  <code class="language-javascript">const fetchPosts = postTitle => (dispatch, getState) => {    dispatch(requestPosts(postTitle));    return fetch(`/some/API/${postTitle}.json`)      .then(response => response.json())      .then(json => dispatch(receivePosts(postTitle, json)));    };  };    // 使用方法一  store.dispatch(fetchPosts('reactjs'));  // 使用方法二  store.dispatch(fetchPosts('reactjs')).then(() =>    console.log(store.getState())  );</code></pre>    <p>上面代码中, fetchPosts 是一个Action Creator(动作生成器),返回一个函数。这个函数执行后,先发出一个Action( requestPosts(postTitle) ),然后进行异步操作。拿到结果后,先将结果转成 JSON 格式,然后再发出一个 Action( receivePosts(postTitle, json) )。</p>    <p>上面代码中,有几个地方需要注意。</p>    <p>(1) fetchPosts 返回了一个函数,而普通的 Action Creator 默认返回一个对象。</p>    <p>(2)返回的函数的参数是 dispatch 和 getState 这两个 Redux 方法,普通的 Action Creator 的参数是 Action 的内容。</p>    <p>(3)在返回的函数之中,先发出一个 Action( requestPosts(postTitle) ),表示操作开始。</p>    <p>(4)异步操作结束之后,再发出一个 Action( receivePosts(postTitle, json) ),表示操作结束。</p>    <p>这样的处理,就解决了自动发送第二个 Action 的问题。但是,又带来了一个新的问题,Action 是由 store.dispatch 方法发送的。而 store.dispatch 方法正常情况下,参数只能是对象,不能是函数。</p>    <p>这时,就要使用中间件 <a href="/misc/goto?guid=4959673858534479573" rel="nofollow,noindex"> redux-thunk </a> 。</p>    <pre>  <code class="language-javascript">import { createStore, applyMiddleware } from 'redux';  import thunk from 'redux-thunk';  import reducer from './reducers';    // Note: this API requires redux@>=3.1.0  const store = createStore(    reducer,    applyMiddleware(thunk)  );</code></pre>    <p>上面代码使用 redux-thunk 中间件,改造 store.dispatch ,使得后者可以接受函数作为参数。</p>    <p>因此,异步操作的第一种解决方案就是,写出一个返回函数的 Action Creator,然后使用 redux-thunk 中间件改造 store.dispatch 。</p>    <h2><strong>六、redux-promise 中间件</strong></h2>    <p>既然 Action Creator 可以返回函数,当然也可以返回其他值。另一种异步操作的解决方案,就是让 Action Creator 返回一个 Promise 对象。</p>    <p>这就需要使用 redux-promise 中间件。</p>    <pre>  <code class="language-javascript">import { createStore, applyMiddleware } from 'redux';  import promiseMiddleware from 'redux-promise';  import reducer from './reducers';    const store = createStore(    reducer,    applyMiddleware(promiseMiddleware)  );</code></pre>    <p>这个中间件使得 store.dispatch 方法可以接受 Promise 对象作为参数。这时,Action Creator 有两种写法。写法一,返回值是一个 Promise 对象。</p>    <pre>  <code class="language-javascript">const fetchPosts =     (dispatch, postTitle) => new Promise(function (resolve, reject) {       dispatch(requestPosts(postTitle));       return fetch(`/some/API/${postTitle}.json`)         .then(response => {           type: 'FETCH_POSTS',           payload: response.json()         });  });</code></pre>    <p>写法二,Action 对象的 payload 属性是一个 Promise 对象。这需要从 <a href="/misc/goto?guid=4959673857811052727" rel="nofollow,noindex"> redux-actions </a> 模块引入 createAction 方法,并且写法也要变成下面这样。</p>    <pre>  <code class="language-javascript">import { createAction } from 'redux-actions';    class AsyncApp extends Component {    componentDidMount() {      const { dispatch, selectedPost } = this.props      // 发出同步 Action      dispatch(requestPosts(selectedPost));      // 发出异步 Action      dispatch(createAction(        'FETCH_POSTS',         fetch(`/some/API/${postTitle}.json`)          .then(response => response.json())      ));    }</code></pre>    <p>上面代码中,第二个 dispatch 方法发出的是异步 Action,只有等到操作结束,这个 Action 才会实际发出。注意, createAction 的第二个参数必须是一个 Promise 对象。</p>    <p>看一下 redux-promise 的 <a href="/misc/goto?guid=4959716597021646341" rel="nofollow,noindex">源码</a> ,就会明白它内部是怎么操作的。</p>    <pre>  <code class="language-javascript">export default function promiseMiddleware({ dispatch }) {    return next => action => {      if (!isFSA(action)) {        return isPromise(action)          ? action.then(dispatch)          : next(action);      }        return isPromise(action.payload)        ? action.payload.then(            result => dispatch({ ...action, payload: result }),            error => {              dispatch({ ...action, payload: error, error: true });              return Promise.reject(error);            }          )        : next(action);    };  }</code></pre>    <p>从上面代码可以看出,如果 Action 本身是一个 Promise,它 resolve 以后的值应该是一个 Action 对象,会被 dispatch 方法送出( action.then(dispatch) ),但 reject 以后不会有任何动作;如果 Action 对象的 payload 属性是一个 Promise 对象,那么无论 resolve 和 reject, dispatch 方法都会发出 Action。</p>    <p>中间件和异步操作,就介绍到这里。下一篇文章将是最后一部分,介绍如何使用 react-redux 这个库。</p>    <p> </p>    <p> </p>    <p> </p>    <p>来自:http://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_two_async_operations.html</p>    <p> </p>