翻译|Async operations using redux-saga

原文请见.

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我.我乐意提供帮助.


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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容