翻譯|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我.我樂意提供幫助.


翻譯結束. 好文章.知乎好像有也有一篇翻譯稿,才看到.如果有興趣可以參考一下

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容