美文网首页Redux思想程序员Web前端之路
翻译|Async operations using redux-

翻译|Async operations using redux-

作者: smartphp | 来源:发表于2017-04-23 09:24 被阅读609次

    原文请见.

    saga其实也是一种设计模式, 他的作用是wrapper包装器的原理,现在我手头没有javascirpt设计模式的书,不知道有没有这个模式.简单说Saga就是把逻辑进行了打包,
    这样在书写逻辑关系的时候就相对比较容易了.所有有关的逻辑卸载一起,符合组件分离,集中控制的原理.(我们的电脑就是这么搞的, 外界设备很多,但是控制权在操作系统上,cpu其实就是我们的saga,用来处理操作流程的)

    开始

    几天前我的同事做了一个关于异步操作管理的讲座.他正在使用几个工具来扩展Redux的功能.听了他的报告,简直让我产生了javascirpt 疲劳.

    让我们面对事实:如果你习惯于基于需求来使用技术完成工作-并不是从技术本身来考虑问题的话-构建React 生态系统将会是非常令人沮丧和花费时间的.(译注:高屋建瓴的话,细细体会:意思别光顾眼前的项目,要有长远的开发眼光).

    我已经在Angular 项目上花费了两年时间,非常的喜欢MVC模型对于state控制的能力.但是我要说即使我已经有了Backbone.js(译注:另一个前端MVC框架)
    的背景,但是Angular.js的学习曲线仍然很有挑战性.我因此获得了不错的工作,因此我也有机会在有关的项目上使用它。我从Angular的舍去学习到了很多的东西.

    还挺酷的,但是Fatigue还是在继续(译注:Angular的水很深啊,吃不透的意思)
    所以我迁移到了时髦的框架上:React,Redux,Sagas.

    几年以前,我遇到一篇文章,是关于扁平promise对象链的文章.我从这篇文章上学到了很多东西。甚至是两年以后,我仍然能回想起文章里的真知灼见.

    申明:我将乎继续使用一样的场景,并扩展他.我希望能创建相同方法的讨论.我会假设读者已经对于Promise对象,React,Redux有了基础的了解,当然还有JavaScript(基础的基础).

    首先要做的事情

    根据Redux-saga的创建者Yassine Flouafi的想法:
    redux-saga是一个在React/Redux应用中,针对性解决异步操作的库.

    基本上,他是一个助手库,基于Sagas和ES6的Generators函数来进行组织异步和分发操作.如果你想进了解Saga的模式,可以参见Caitie McCattrey的视频.

    航班展示案例

    场景是这样的


    航班展示示意图航班展示示意图

    如上面的图所示,三个API的调用时一个承前启后的过程,getDeparture->getFlight->getForecast,所以我们的API Serivces类看起来是这样的:

     class TravelServiceApi {//旅行服务API
     static getUser() { //首先异步获取用户信息
       return new Promise((resolve) => {
         setTimeout(() => {
           resolve({//模拟返回的数据
                email : "somemockemail@email.com",
                repository: "http://github.com/username"
           });
         }, 3000);
       });
     }
     static getDeparture(user) {//获取用户航班信息
      return new Promise((resolve) => {
       setTimeout(() => {
        resolve({//模拟返回的数据
          userID : user.email,
          flightID : “AR1973”,
          date : “10/27/2016 16:00PM”
         });
        }, 2500);
       });
     }
     static getForecast(date) {//获取天气情况
      return new Promise((resolve) => {
          setTimeout(() => {
            resolve({ //模拟返回的数据
                date: date,
                forecast: "rain"
            });
          }, 2000);
       });
      }
    }
    
    

    这个API非常的直接,允许我们使用假数据来设定场景.首先我们的需要一个用户,然后根据用户的的信息,来获取用户的航班信息,我们得到离港信息,航班信息和天气信息,据此我们可以创建几个丑陋的仪表盘

    仪表盘示意图仪表盘示意图

    使用的React组件可以看这里.这是三个不同过的组件,数据来源是redux store中的三个reducer.类似下面的样子:

     const dashboard = (state = {}, action) => {
     switch(action.type) {
      case ‘FETCH_DASHBOARD_SUCCESS’:
      return Object.assign({}, state, action.payload);
      default :
      return state;
     }
    };
    

    因为有三个不同的场景,我们为每一个面板使用一个不同的reducer,这么做就可以让组件从Redux的StateProps函数获取到需要的部分:

    const mapStateToProps =(state) => ({
     user : state.user,
     dashboard : state.dashboard
    });
    

    每个步骤都配置好以后(我很清楚,很多的细节问题都还没有解释,但是我想集中注意在sagas上),准备开始运行了.

    展现我们的Sagas

    William Deming 曾经说过:

    如果你不能描述出你所做事情的步骤,那么你就不知道你到底在做什么

    (译注:process或者flow对于Redux的学习很重要,初学者总是不能把分散的一些部分连城一个整体去考虑).

    那么,我们就来一步一步的使用Redux Saga创建我们的工作流程.

    1.注册Sagas

    我将会使用我自己的表达方式来描述API 暴露出来的方法.如果你需要更多的技术细节,参考文档

    首先我们需要创建saga 生成器函数(译注:generator,这算是坎吧,要先理解ES6的内容),并且注册一下.

    function* rootSaga() {
      yield[
        fork(loadUser),
        takeLatest('LOAD_DASHBOARD', loadDashboardSequenced)
      ];
    }
    

    Redux saga暴露出了几个方法,这几个方法被成为effects,我们将会定义几个effects:

    • Fork 非阻塞执行传递进来的函数(译注:这算是有一个坎儿,javascript的函数是一类对象,可以作为函数的参数来传递,初学者很难理解这个问题)

    • Take 暂停执行直达收到匹配的action

    • Race 同时运行effects,然后其中之一完成了,就会立即退出,其他的effects也就会终止

    • Call 执行一个函数,如果他返回一个promise对象(译注:异步操作的又一个坎儿),saga就会在这里终止,知道promise返回 resolved

    • put dispatch一个动作

    • Select 运行selector函数从state获取数据

    • takeLatest 意思是我们将会执行一个操作,操作只会返回最新的一个滴啊用的结果.如果我们出发几个cases,将会忽略所有的操作,除了最后一个.

    • takeEvery将会返回每个调用的结果

    我们将会注册两个不同的sagas.稍后会定义他们.目前我们为了user信息使用了fork和takeLeatest,他们会等待直到”LOAD_DASBOARD“的调用后,才会执行.

    2.把Saga中间件注入到Redux的store中

    当我们定义了Redux store并初始化以后,大多数情况下看起来像这样:

     const sagaMiddleware = createSagaMiddleware();
    const store = createStore(rootReducer, [], compose(
          applyMiddleware(sagaMiddleware)  
    );
    sagaMiddleware.run(rootSaga); /* inject our sagas into the middleware*/
    

    3.创建Sagas

    首先,我们将定义loadUser的流程 saga:

    function* loadUser() {
      try {
       //1st step
        const user = yield call(getUser);
       //2nd step
        yield put({type: 'FETCH_USER_SUCCESS', payload: user});
      } catch(error) {
        yield put({type: 'FETCH_FAILED', error});
      }
    }
    

    我按照下面的方式解读一下:

    • 首先调用getUser函数,返回的结果赋值给user常量
    • 之后,dispatch一个叫做FETCH_USER_SUCCESS的action,user传递给store去处理.
    • 如果操作中出问题了,dispatch一个FETCH_FAILED的 action

    如你所见的,的确是非常的酷,我们把yield操作的结果赋值给了一个变量

    接着来创建saga序列

     function* loadDashboardSequenced() {
     try {
      
      yield take(‘FETCH_USER_SUCCESS’);
      const user = yield select(state => state.user);
      
      const departure = yield call(loadDeparture, user);
      const flight = yield call(loadFlight, departure.flightID);
      const forecast = yield call(loadForecast, departure.date);
      yield put({type: ‘FETCH_DASHBOARD_SUCCESS’, payload: {forecast,  flight, departure} });
      } catch(error) {
        yield put({type: ‘FETCH_FAILED’, error: error.message});
      }
    }
    

    按照下面的步骤来解读:

    • 等待FETCH_USER_SUCCESSaction的被派发,这个操作的基础是基于一个事件被暂定直到触发为止.我们使用take effect来实施这个过程

    • 我们从store中获取一个值.select effects接收一个函数可以接入到store.我们把用户信息赋值给user常量

    • 执行一个异步操作来加载depature信息,使用call effect ,user常量作为参数

    • loadDeparture 完成以后,我们执行 loadFlight,参数是前一个操作异步获取的departure对象.

    • forecast的操作是一样的,我们需要等待航班信息加载完成以后才可以执行下一个call effect

    • 最后,当所有的操作都完成以后,使用put effect来分发一个action,把所有获取的信息发送到Redux的store.

    正如你看到的,一个saga就是一系列等待前一个action来修改他们行为的集合体(译注:集合中每个步骤都会等待前一个步骤的操作结果).一旦完成整个流程,所有的信息就可以提供给Redux的store来处理了.

    是不是相当的整洁啊!

    接下来,我们看看一个不同的示例.考虑一下 getFlightgetForecast 同时触发,他们不需要一个等另一个完成了再执行下一个.
    所以我们可以创建一个不同的面板

    并发操作并发操作

    非阻塞Saga

    为了执行两个非阻塞操作,需要对之前的saga做一点改动:

     function* loadDashboardNonSequenced() {
      try {
        //Wait for the user to be loaded
        yield take('FETCH_USER_SUCCESS');
        //Take the user info from the store
        const user = yield select(getUserFromState);
        //Get Departure information
        const departure = yield call(loadDeparture, user);
        //Here is when the magic happens
        const [flight, forecast] = yield [call(loadFlight, departure.flightID), call(loadForecast, departure.date)];
        //Tell the store we are ready to be displayed
        yield put({type: 'FETCH_DASHBOARD2_SUCCESS', payload: {departure, flight, forecast}});
    } catch(error) {
        yield put({type: 'FETCH_FAILED', error: error.message});
      }
    }
    
    

    我们把yield注册为一个数组:

    const [flight, forecast] = yield [call(loadFlight, departure.flightID), call(loadForecast, departure.date)];
    

    因此,两个操作会同时被调用(并行),但是在在最后们会等待两个操作都返回结果以后,如果有需求再更新UI

    接着我们在rootSaga中注册saga

    function* rootSaga() {
      yield[
        fork(loadUser),
        takeLatest('LOAD_DASHBOARD', loadDashboardSequenced),
        takeLatest('LOAD_DASHBOARD2' loadDashboardNonSequenced)
      ];
    }
    

    如果操作一旦完成就需要更新UI,应该怎么办?
    别担心,我们往回看一下

    非序列和非阻塞Sagas

    我们可以隔离我们的saga,也可以合并他们,意思是saga可以独立的工作.这就是我们需要的操作.看看操作步骤

    step #1 把Forecast和Flight Saga隔离开,这两个Saga都依赖departure的操作

     /* **************Flight Saga************** */
    function* isolatedFlight() {
      try {
        /* departure will take the value of the object passed by the put*/
        const departure = yield take('FETCH_DEPARTURE3_SUCCESS');
     
        const flight = yield call(loadFlight, departure.flightID);
     
        yield put({type: 'FETCH_DASHBOARD3_SUCCESS', payload: {flight}});
      } catch (error) {
        yield put({type: 'FETCH_FAILED', error: error.message});
      }
    }
    
    /* **************Forecast Saga************** */
    function* isolatedForecast() {
        try {
          /* departure will take the value of the object passed by the put*/
         const departure = yield take('FETCH_DEPARTURE3_SUCCESS');
         const forecast = yield call(loadForecast, departure.date);
         
         yield put({type: 'FETCH_DASHBOARD3_SUCCESS', payload: { forecast, }});
    } catch(error) {
          yield put({type: 'FETCH_FAILED', error: error.message});
        }
    }
    
    

    有什么要注意的吗?这是我们的构建过程

    • 两个saga都等待同一个 Action 事件(FETCH_DEPARTURE3_SUCCESS)的结果,来执行后续的工作.
    • 当这个事件被触发的时候,他们会收到一个值,这个问题的细节,下一步会讲.
    • 两个saga使用call effect来执行异步操作,异步操作完成以后,他们会触发同一个事件.但是发送到store的数据是不同的.感谢Redux的巨大威力,我们这样操作,但是不会对reducer做任何修改.

    step #2 对departure序列做出改动,发送一个departure值到两个其他的saga:

    function* loadDashboardNonSequencedNonBlocking() {
      try {
        //Wait for the action to start
        yield take('FETCH_USER_SUCCESS');
        //Take the user info from the store
        const user = yield select(getUserFromState);
        //Get Departure information
        const departure = yield call(loadDeparture, user);
        //Update the store so the UI get updated
        yield put({type: 'FETCH_DASHBOARD3_SUCCESS', payload: { departure, }});
        //trigger actions for Forecast and Flight to start...
        //We can pass and object into the put statement
        yield put({type: 'FETCH_DEPARTURE3_SUCCESS', departure});
      } catch(error) {
        yield put({type: 'FETCH_FAILED', error: error.message});
      }
    }
    

    之前的代码没有变化,put effect这里有改变.我们可以给一个action传递一个对象,它将会把yield的结果赋值给一个departure常量,departure saga和flight saga都这样操作.

    看看demo,注意一下第三个面板加载的foreacast要比flight快一点,因为flight的延时长了一点,模拟了慢速的请求过程.

    在实际生产的app中,处理的过程可能有一点不一样.我只是想说明在使用put effect的时候怎么传递值.

    关于测试?

    你会测试你的代码,对不?

    Saga很容易测试,因为saga耦合了操作步骤,根据生成器函数的逻辑,操作步骤被序列化了.让我们看看实例:

     describe('Sequenced Saga', () => {
      const saga = loadDashboardSequenced();
      let output = null;
    it('should take fetch users success', () => {
          output = saga.next().value;
          let expected = take('FETCH_USER_SUCCESS');
          expect(output).toEqual(expected);
      });
    it('should select the state from store', () => {
          output = saga.next().value;
          let expected = select(getUserFromState);
          expect(output).toEqual(expected);
      });
    it('should call LoadDeparture with the user obj', (done) => {
        output = saga.next(user).value;
        let expected = call(loadDeparture, user);
        done();
        expect(output).toEqual(expected);
      });
    it('should Load the flight with the flightId', (done) => {
        let output = saga.next(departure).value;
        let expected = call(loadFlight, departure.flightID);
        done();
        expect(output).toEqual(expected);
      });
    it('should load the forecast with the departure date', (done) => {
          output = saga.next(flight).value;
          let expected = call(loadForecast, departure.date);
          done();
          expect(output).toEqual(expected);
        });
    it('should put Fetch dashboard success', (done) => {
           output = saga.next(forecast, departure, flight ).value;
           let expected = put({type: 'FETCH_DASHBOARD_SUCCESS', payload: {forecast, flight, departure}});
           const finished = saga.next().done;
           done();
           expect(finished).toEqual(true);
           expect(output).toEqual(expected);
        });
    });
    
    1. 确保导入所有需要测试的effect和助手函数
    2. 当你在一个yield中存储一个值时,需要传递一个mock 数据 到下一个函数.注意看看第三个,第四个和第五个测试
    3. 在测试的幕后,当下一个方法被调用以后,yield完成以后,就会移动到下一步.这就是为什么我们使用saga.next().value的原因.
    4. 序列会被固话,如果你改变saga的步骤,测试就不会通过了.

    结论

    我非常喜欢测试新技术.每天都会返现新东西.这就像时装:一旦一些事情被公众接受,好像每个人都想使用它.有时候我会在这些事情中发现一些价值(译注:意思是别人说好,你接受了,也可以获得很多有意义的东西,但是不是全部),但是坐下来考虑一下我们正真需要什么是非常重要的.

    我发现thunks更容易实现和维护,但是对于复杂的操作,Redux-saga能做的更好.

    再次声明,感谢 Thomas 为这篇文章提供的灵感.我希望其他人也可以从我的文章中激发一些灵感:)

    如果你阅读中有问题,可以tweet我.我乐意提供帮助.


    翻译结束. 好文章.知乎好像有也有一篇翻译稿,才看到.如果有兴趣可以参考一下

    相关文章

      网友评论

      • Bravo682:这篇文章对我帮助很大,多谢翻译。

      本文标题:翻译|Async operations using redux-

      本文链接:https://www.haomeiwen.com/subject/eqwezttx.html