redux-saga 实践总结

mao103 8年前
   <p style="text-align:center"><img src="https://simg.open-open.com/show/56809e18805c90e631824c8c3eaaa9ef.png"></p>    <p>有关 redux-saga 的文章,网络上早已是汗牛充栋。因此,本篇主要谈一谈自己的理解,以及实践中的经验总结。</p>    <p>众所周知,redux 大部分的想法,都来自于 elm 。在 elm 和 redux 中,整个应用就是一个纯函数。elm 通过在 reducer 中返回一些声明副作用的 task 来处理异步问题,而 redux 借鉴 koa 的插件机制,用中间件改造 dispatch ,从而诞生了一批通过构造满足特殊 pattern 条件的 action 来解决副作用的问题。</p>    <p>而 redux-saga 独辟蹊径,监听 action 来执行有副作用的 task,以保持 action 的简洁性。并且引入了 sagas 的机制和 generator 的特性,让redux-saga 非常方便地处理复杂异步问题。</p>    <p>有意思的是,redux 借鉴了 elm,但在处理异步问题(副作用问题在前端一般为异步问题)上,借鉴了 koa 中间件的形式,而 redux-saga 却又去从 elm 取经,借鉴了独立 task 的形式。但是说到底,redux-saga 是一个 redux 的中间件。这个故事告诉我们,有好的设计不如有强大的扩展性。</p>    <p>redux-saga 本身也有良好的扩展性。比如,易证得,但凡 redux 中间件,都可以用 redux-saga 来重写。当然了,不是说用了 redux-saga,其它异步中间件就不能用了,只是说不能保证 redux-saga 能恰好和你之前使用的中间件配合良好。</p>    <h2><strong>redux-saga 简介</strong></h2>    <p>redux-saga 是一个 redux 中间件,它具有如下特性:</p>    <ul>     <li> <p>集中处理 redux 副作用问题。</p> </li>     <li> <p>被实现为 generator 。</p> </li>     <li> <p>类 redux-thunk 中间件。</p> </li>     <li> <p>watch/worker(监听->执行) 的工作形式。</p> </li>    </ul>    <p>对于刚接触 redux-saga 的同学,可以先来一段简单的代码快速了解 redux-saga 诸多特性。</p>    <pre>  <code class="language-javascript">// 类 thunk 的 worker “进程”  function* load() {    yield put({ type: BEGIN_LOAD_DATA });        try {      const result = yield call(fetch, UrlMap.loadData);        yield put({        type: LOAD_DATA_SUCCESS,        payload: result,      });    } catch (e) {      yield put({        type: LOAD_DATA_ERROR,        payload: e,        error: true,      });    }  }    function* saga() {    // 创建一个监听“进程”    yield fork(watch(CLICK_LOAD_BUTTON, load))  }</code></pre>    <h3><strong>Effects</strong></h3>    <p>Effect 是一个 javascript 对象,里面包含描述副作用的信息,可以通过 yield 传达给 sagaMiddleware 执行</p>    <p>在 redux-saga 世界里,所有的 Effect 都必须被 yield 才会执行,所以有人写了 eslint-plugin-redux-saga 来检查是否每个 Effect 都被 yield。并且原则上来说,所有的 yield 后面也只能跟Effect,以保证代码的易测性。</p>    <p>例如:</p>    <pre>  <code class="language-javascript">yield fetch(UrlMap.fetchData);</code></pre>    <p>应该用 call Effect :</p>    <pre>  <code class="language-javascript">yield call(fetch, UrlMap.fetchData)</code></pre>    <p>从而可以使代码可测:</p>    <pre>  <code class="language-javascript">assert.deepEqual(iterator.next().value, call(fetch, UrlMap.fetchData))</code></pre>    <p>关于各个 Effect 的具体介绍,文档已经写得很详细了,这里只做简要介绍。</p>    <h2><strong>1、put</strong></h2>    <p>作用和 redux 中的 dispatch 相同。</p>    <pre>  <code class="language-javascript">yield put({ type: 'CLICK_BTN' });</code></pre>    <h2><strong>2、select</strong></h2>    <p>作用和 redux thunk 中的 getState 相同。</p>    <pre>  <code class="language-javascript">const id = yield select(state => state.id);</code></pre>    <h2><strong>3、take</strong></h2>    <p>等待 redux dispatch 匹配某个 pattern 的 action 。</p>    <p>在这个例子中,先等待一个按钮点击的 action ,然后执行按钮点击的 saga:</p>    <pre>  <code class="language-javascript">while (true) {    yield take('CLICK_BUTTON');    yield fork(clickButtonSaga);  }</code></pre>    <p>再举一个利用 take 实现 logMiddleware 的例子:</p>    <pre>  <code class="language-javascript">while (true) {    const action = yield take('*');    const newState = yield select();        console.log('received action:', action);    console.log('state become:', newState);  }</code></pre>    <p>这种监听一个 action ,然后执行相应任务的方式,在 redux-saga 中非常常用,因此 redux-saga 提供了一个辅助 Effect —— takeEvery ,让 watch/worker 的代码更加清晰。</p>    <pre>  <code class="language-javascript">yield takeEvery('*', function* logger(action) {    const newState = yield select();      console.log('received action:', action);    console.log('state become:', newState);  });</code></pre>    <h2><strong>4、阻塞调用和无阻塞调用</strong></h2>    <p>redux-saga 可以用 fork 和 call 来调用子 saga ,其中 fork 是无阻塞型调用,call 是阻塞型调用。</p>    <p>如果看过 saga 的论文,就知道 saga 是由许多子 saga (或者 subtransaction)组合起来的。fork Effect 和它的字面意思一样,即创建一个子 saga 。</p>    <h3><strong>4.1、fork</strong></h3>    <p>下面写一个倒数的例子,当接收到 BEGIN_COUNT 的 action,则开始倒数,而接收到 STOP_COUNT 的 action, 则停止倒数。</p>    <pre>  <code class="language-javascript">function* count(number) {    let currNum = number;      while (currNum >= 0) {      console.log(currNum--);      yield delay(1000);    }  }    function countSaga* () {    while (true) {      const { payload: number } = yield take(BEGIN_COUNT);      const countTaskId = yield fork(count, number);        yield take(STOP_TASK);      yield cancel(countTaskId);    }  }</code></pre>    <h3><strong>4.2、call</strong></h3>    <p>有阻塞地调用 saga 或者返回 promise 的函数。</p>    <p>同样写一个例子:</p>    <pre>  <code class="language-javascript">const project = yield call(fetch, { url: UrlMap.fetchProject });  const members = yield call(fetchMembers, project.id);</code></pre>    <p>另附 redux-saga 文档:</p>    <h2><strong>传统异步中间件简介</strong></h2>    <p>在介绍 redux-saga 优缺点之前,这里先简要介绍传统的 redux 异步中间件,以便和 redux-saga 做比较。对传统异步中间件已经充分了解的读者,可以直接跳到 “redux-saga 优缺点分析” 进行阅读。</p>    <h3><strong>1. fetch-middleware</strong></h3>    <p>使用redux的前端技术团队或个人,大多数都有一套自己 fetch-middleware,一来可以封装异步请求的业务逻辑,避免重复代码,二来可以写一些公共的异步请求逻辑,比如异常接口数据采集、接口缓存、接口处理等等。例如 redux-composable-fetch , redux-api-middleware 。</p>    <p>在当前 redux 社区中,fetch-middleware 封装结果一般如下:</p>    <pre>  <code class="language-javascript">function loadData(id) {    return {      url: '/api.json',      types: [LOADING_ACTION_TYPE, SUCCESS_ACTION_TYPE, SUCCESS_ACTION_TYPE],      params: {        id,      },    };  }</code></pre>    <p>值得一提的是,大多数 fetch-middleware 都会用到一个小技巧 —— 把最终处理好的 promise 返回出来,以便在 thunk-middleware 中复用,并组织不同异步过程的先后逻辑。</p>    <pre>  <code class="language-javascript">function loadDetailThunk(id) {    return (dispatch) => {      // 先请求到 loadData 的结果,再请求 loadDetail      dispatch(loadData(id)).then(result => {        const { id: detailId } = result;        dispatch(loadDetail(detailId));      });    };  }</code></pre>    <p>这个技巧在 redux-saga 中也同样有效。</p>    <pre>  <code class="language-javascript">function* loadDetailSaga(id) {    const result = yield put.sync(loadData(id));    const { id: detailId } = result;      yield put.sync(loadDetail(detailId));  }</code></pre>    <h3><strong>2. redux-thunk-middleware</strong></h3>    <p>redux 中大量应用了 thunk 的概念,例如 getState 以延迟执行的方式可以始终获得最新值,redux-thunk 以延迟执行的方式把副作用的责任推卸到用户身上。</p>    <p>任何异步问题都能在 thunk 中解决。</p>    <h3><strong>3. <a href="/misc/goto?guid=4959722118823277551" rel="nofollow,noindex"> sequence-middleware </a></strong></h3>    <p>sequence-middleware 用于保证 action 依次执行,无论是异步 action 还是普通 aciton ,和 fetch-middleware 配合使用非常方便。</p>    <p>这里可以把每个 action 可以写成 thunk action,在 thunk 函数内从 store 拿到参数,避免 action 之间的依赖。这样不管业务逻辑有多复杂,都可以通过用 sequence action 轻易组织。</p>    <pre>  <code class="language-javascript">function loadDetailThunk() {    return function(dispatch, getState) {      const detailId = _.get(getState(), `${currPath}.detailId`);        dispatch({        url: UrlMap.getDetail,        params: { detailId },      });    };  }    function loadDetail() {    return [loadData(), loadDetailThunk()];  }</code></pre>    <h2><strong>redux-saga 优缺点分析</strong></h2>    <h3><strong>缺点</strong></h3>    <ul>     <li> <p>redux-saga 不强迫我们捕获异常,这往往会造成异常发生时难以发现原因。因此,一个良好的习惯是,相信任何一个过程都有可能发生异常。如果出现异常但没有被捕获,redux-saga 的错误栈会给你一种一脸懵逼的感觉。</p> </li>     <li> <p>generator 的调试环境比较糟糕,babel 的 source-map 经常错位,经常要手动加 debugger 来调试。</p> </li>     <li> <p>你团队中使用的其它异步中间件,或许难以和 redux-saga 搭配良好。或许需要花费一些代价,用 redux-saga 来重构一部分中间件。</p> </li>    </ul>    <h3><strong>优点</strong></h3>    <ul>     <li> <p>保持 action 的简单纯粹,aciton 不再像原来那样五花八门,让人眼花缭乱。task 的模式使代码更加清晰。</p> </li>     <li> <p>redux-saga 提供了丰富的 Effects,以及 sagas 的机制(所有的 saga 都可以被中断),在处理复杂的异步问题上十分趁手。如果你的应用属于写操作密集型或者业务逻辑复杂,快让 redux-saga 来拯救你。</p> </li>     <li> <p>扩展性强。</p> </li>     <li> <p>声明式的 Effects,使代码更易测试。</p> </li>    </ul>    <h2><strong>利用 redux-saga 写 redux 中间件</strong></h2>    <p>用 redux-saga 来写中间件,可谓事半功倍。这里举一个轮询中间件的例子。</p>    <pre>  <code class="language-javascript">function* pollingSaga(fetchAction) {    const { defaultInterval, mockInterval } = fetchAction;      while (true) {      try {        const result = yield put.sync(fetchAction);        const interval = mockInterval || result.interval;          yield delay(interval * 1000);      } catch (e) {        yield delay(defaultInterval * 1000);      }    }  }    function* beginPolling(pollingAction) {    const { pollingUrl, defaultInterval = 300, mockInterval, types,      params = {} } = pollingAction;      if (!types[1]) {      console.error('pollingAction pattern error', pollingAction);      throw Error('pollingAction types[1] is null');    }      const fetchAction = {      url: pollingUrl,      types,      params,      mockInterval,      defaultInterval,    };      const pollingTaskId = yield fork(pollingSaga, fetchAction);    const pattern = action => action.type === types[1] && action.stopPolling;      yield take(pattern);    yield cancel(pollingTaskId);  }    function* pollingSagaMiddleware() {    yield takeEvery(action => {      const { pollingUrl, types } = action;        return pollingUrl && types && types.length;    }, beginPolling);  };</code></pre>    <p>最后,redux-saga 在实践的沉淀,我已经总结到 redux-saga-sugar .</p>    <p> </p>    <p>来自:https://zhuanlan.zhihu.com/p/23012870</p>    <p> </p>