基於 React 和 Redux 的 API 集成解決方案

在前端開發的過程當中,咱們可能會花很多的時間去集成 API、與 API 聯調、或者解決 API 變更帶來的問題。若是你也但願減輕這部分負擔,提升團隊的開發效率,那麼這篇文章必定會對你有所幫助。前端

文章中使用到的技術棧主要有:react

  • React 全家桶
  • TypeScript
  • RxJS

文章中會講述集成 API 時遇到的一些複雜場景,並給出對應解決方案。經過本身寫的小工具,自動生成 API 集成的代碼,極大提高團隊開發效率。ios

本文的全部代碼都在這個倉庫:requestgit

自動生成代碼的工具在這裏:ts-codegengithub

1. 統一處理 HTTP 請求

1.1 爲何要這樣作?

咱們能夠直接經過 fetch 或者 XMLHttpRequest 發起 HTTP 請求。可是,若是在每一個調用 API 的地方都採用這種方式,可能會產生大量模板代碼,並且很難應對一些業務場景:typescript

  • 如何爲全部的請求添加 loading 動畫?
  • 如何統一顯示請求失敗以後的錯誤信息?
  • 如何實現 API 去重?
  • 如何經過 Google Analytics 追蹤請求?

所以,爲了減小模板代碼並應對各類複雜業務場景,咱們須要對 HTTP 請求進行統一處理。json

1.2 如何設計和實現?

經過 redux,咱們能夠將 API 請求 「action 化」。換句話說,就是將 API 請求轉化成 redux 中的 action。一般來講,一個 API 請求會轉化爲三個不一樣的 action: request action、request start action、request success/fail action。分別用於發起 API 請求,記錄請求開始、請求成功響應和請求失敗的狀態。而後,針對不一樣的業務場景,咱們能夠實現不一樣的 middleware 去處理這些 action。redux

1.2.1 Request Action

redux 的 dispatch 是一個同步方法,默認只用於分發 action (普通對象)。但經過 middleware,咱們能夠 dispatch 任何東西,好比 function (redux-thunk) 和 observable,只要確保它們被攔截便可。緩存

要實現異步的 HTTP 請求,咱們須要一種特殊的 action,本文稱之爲 request action 。request action 會攜帶請求參數的信息,以便以後發起 HTTP 請求時使用。與其餘 action 不一樣的是,它須要一個 request 屬性做爲標識。其定義以下:異步

interface IRequestAction<T = any> {
  type: T
  meta: {
    request: true // 標記 request action
  };
  payload: AxiosRequestConfig; // 請求參數
}

redux 的 action 一直飽受詬病的一點,就是會產生大量模板代碼並且純字符串的 type 也很容易寫錯。因此官方不推薦咱們直接使用 action 對象,而是經過 action creator 函數來生成相應的 action。好比社區推出的 redux-actions,就可以幫助咱們很好地建立 action creator。參考它的實現,咱們能夠實現一個函數 createRequestActionCreator ,用於建立以下定義的 action creator:

interface IRequestActionCreator<TReq, TResp = any, TMeta = any> {
  (args: TReq, extraMeta?: TMeta): IRequestAction;

  TReq: TReq;   // 請求參數的類型
  TResp: TResp; // 請求響應的類型
  $name: string; // request action creator 函數的名字
  toString: () => string;
  start: {
    toString: () => string;
  };
  success: {
    toString: () => string;
  };
  fail: {
    toString: () => string;
  };
}

在上面的代碼中,TReq 和 TResp 分別表示 請求參數的類型請求響應的類型。它們保存在 request action creator 函數的原型上。這樣,經過 request action creator,咱們就能迅速知道一個 API 請求參數的類型和響應數據的類型。

const user: typeof getUser.TResp = { name: "Lee", age: 10 };

對於 API 請求來講,請求開始、請求成功和請求失敗這幾個節點很是重要。由於每個節點都有可能觸發 UI 的改變。咱們能夠定義三種特定 type 的 action 來記錄每一個異步階段。也就是咱們上面提到的 request start action、request success action 和 request fail action,其定義以下:

interface IRequestStartAction<T = any> {
  type: T; // xxx_START
  meta: {
    prevAction: IRequestAction; // 保存其對應的 reqeust action
  };
}

interface IRequestSuccessAction<T = any, TResp = any> {
  type: T; // xxx_SUCCESS
  payload: AxiosResponse<TResp>; // 保存 API Response
  meta: {
    prevAction: IRequestAction; 
  };
}

interface IRequestFailAction<T = any> {
  type: T; // xxx_FAIL
  error: true;
  payload: AxiosError; // 保存 Error
  meta: {
    prevAction: IRequestAction; 
  };
}

在上面的代碼中,咱們在 request action creator 的原型上綁定了 toString 方法,以及 startsuccessfail 屬性。由於 action type 是純字符串,手寫很容易出錯,因此咱們但願經過 request action creator 直接獲取它們的 type,就像下面這樣:

`${getData}` // "GET_DATA"
`${getData.start}` // "GET_DATA_START"
`${getData.success}` // "GET_DATA_SUCCESS"
`${getData.fail}`  // "GET_DATA_FAIL"

1.2.2 Request Middleware

接下來,咱們須要建立一個 middleware 來統一處理 request action。middleware 的邏輯很簡單,就是攔截全部的 request action,而後發起 HTTP 請求:

  • 請求開始:dispatch xxx_STAT action,方便顯示 loading
  • 請求成功:攜帶 API Response,dispatch xxx_SUCCESS action
  • 請求失敗:攜帶 Error 信息,dispatch xxx_FAIL action

這裏須要注意的是,request middleware 須要「吃掉」request action,也就是說不把這個 action 交給下游的 middleware 進行處理。一是由於邏輯已經在這個 middleware 處理完成了,下游的 middleware 無需處理這類 action。二是由於若是下游的 middleware 也 dispatch request action,會形成死循環,引起沒必要要的問題。

1.3 如何使用?

咱們能夠經過分發 request action 來觸發請求的調用。而後在 reducer 中去處理 request success action,將請求的響應數據存入 redux store。

可是,不少時候咱們不只要發起 API 請求,還要在 請求成功請求失敗 的時候去執行一些邏輯。這些邏輯不會對 state 形成影響,所以不須要在 reducer 中去處理。好比:用戶填寫了一個表單,點擊 submit 按鈕時發起 API 請求,當 API 請求成功後執行頁面跳轉。這個問題用 Promise 很好解決,你只須要將邏輯放到它的 then 和 catch 中便可。然而,將請求 「action化」以後,咱們不能像 Promise 同樣,在調用請求的同時註冊請求成功和失敗的回調。

如何解決這個問題呢?咱們能夠實現一種相似 Promise 的調用方式,容許咱們在分發 request action 的同時去註冊請求成功和失敗的回調。也就是咱們即將介紹的 useRequest。

1.3.1 useRequest: 基於 React Hooks 和 RXJS 調用請求

爲了讓發起請求、請求成功和請求失敗這幾個階段再也不割裂,咱們設計了 onSuccess 和 onFail 回調。相似於 Promise 的 then 和 catch。但願可以像下面這樣去觸發 API 請求的調用:

// 僞代碼

useRequest(xxxActionCreator, {
  onSuccess: (requestSuccessAction) => {
    // do something when request success
  },
  onFail: (requestFailAction) => {
    // do something when request fail
  },
});

經過 RxJS 處理請求成功和失敗的回調

Promise 和 callback 都像「潑出去的水」,正所謂「覆水難收」,一旦它們開始執行便沒法取消。若是遇到須要「取消」的場景就會比較尷尬。雖然能夠經過一些方法繞過這個問題,但始終以爲代碼不夠優雅。所以,咱們引入了 RxJS,嘗試用一種新的思路去探索並解決這個問題。

咱們能夠改造 redux 的 dispatch 方法,在每次 dispatch 一個 action 以前,再 dispatch 一個 subject$ (觀察者)。接着,在 middleware 中建立一個 rootSubject$ (可觀察對象),用於攔截 dispatch 過來的 subject$,並讓它成爲 rootSubject$ 的觀察者。rootSubject$ 會把 dispatch 過來的 action 推送給它的全部觀察者。所以,只須要觀察請求成功和失敗的 action,執行對應的 callback 便可。

resize,w_1244-20190911221920949.png

利用 Rx 自身的特性,咱們能夠方便地控制複雜的異步流程,固然也包括取消。

實現 useRequest Hook

useRequest 提供用於分發 request action 的函數,同時在請求成功或失敗時,執行相應的回調函數。它的輸入和輸出大體以下:

interface IRequestCallbacks<TResp> {
  onSuccess?: (action: IRequestSuccessAction<TResp>) => void;
  onFail?: (action: IRequestFailAction) => void;
}

export enum RequestStage {
  START = "START",
  SUCCESS = "SUCCESS",
  FAILED = "FAIL",
}

const useRequest = <T extends IRequestActionCreator<T["TReq"], T["TResp"]>>(
  actionCreator: T,
  options: IRequestCallbacks<T["TResp"]> = {},
  deps: DependencyList = [],
) => {
  
  // ...
  
  return [request, requestStage$] as [typeof request, BehaviorSubject<RequestStage>];
};

它接收 actionCreator 做爲第一個參數,並返回一個 request 函數,當你調用這個函數時,就能夠分發相應的 request action從而發起 API 請求

同時它也會返回一個可觀察對象 requestStage$(可觀察對象) ,用於推送當前請求所處的階段。其中包括:請求開始、成功和失敗三個階段。這樣,在發起請求以後,咱們就可以輕鬆地追蹤到它的狀態。這在一些場景下很是有用,好比當請求開始時,在頁面上顯示 loading 動畫,請求結束時關閉這個動畫。

爲何返回可觀察對象 requestStage$ 而不是返回 requestStage 狀態呢?若是返回狀態,意味着在請求開始、請求成功和請求失敗時都須要去 setState。但並非每個場景都須要這個狀態。對於不須要這個狀態的組件來講,就會形成一些浪費(re-render)。所以,咱們返回一個可觀察對象,當你須要用到這個狀態時,去訂閱它就行了。

 options 做爲它的第二個參數,你能夠經過它來指定 onSuccess 和 onFail 回調。onSuccess 會將 request success action 做爲參數提供給你,你能夠經過它拿到請求成功響應以後的數據。而後,你能夠選擇將數據存入 redux store,或是 local state,又或者你根本不在意它的響應數據,只是爲了在請求成功時去跳轉頁面。但不管如何,經過 useRequest,咱們都能更加便捷地去實現需求。

const [getBooks] = useRequest(getBooksUsingGET, {
  success: (action) => {
    saveBooksToStore(action.payload.data); // 將 response 數據存入 redux store
  },
});

const onSubmit = (values: { name: string; price: number }) => {
  getBooks(values);
};

複雜場景

useRequest 封裝了調用請求的邏輯,經過組合多個 useRequest ,能夠應對不少複雜場景。

處理多個相互獨立的 Request Action

同時發起多個不一樣的 request action,這些 request action 之間相互獨立,並沒有關聯。這種狀況很簡單,使用多個 useRequest 便可。

const [requestA] = useRequest(A);
const [requestB] = useRequest(B);
const [requestC] = useRequest(C);

useEffect(() => {
  requestA();
  requestB();
  requestC();
}, []);
處理多個相互關聯的 Request Action

同時發起多個不一樣的 request action,這些 request action 之間有前後順序。好比發起 A 請求,A 請求成功了以後發起 B 請求,B 請求成功了以後再發起 C 請求。

因爲 useRequest 會建立發起請求的函數,並在請求成功以後執行 onSuccess 回調。所以,咱們能夠經過 useRequest 建立多個 request 函數,並預設它們成功響應以後的邏輯。就像 RXJS 中「預鋪設管道」同樣,當事件發生以後,系統會按照預設的管道運做。

// 預先建立全部的 request 函數,並預設 onSuccess 的邏輯
const [requestC] = useRequest(C);

const [requestB] = useRequest(B, {
  onSuccess: () => {
    requestC();
  },
});
const [requestA] = useRequest(A, {
  onSuccess: () => {
    requestB();
  },
});

// 當 requestA 真正調用以後,程序會按照預設的邏輯執行。

<form onSubmit={requestA}>
處理多個相同的 request action

同時發起多個徹底相同的 request action,可是出於性能的考慮,咱們一般會「吃掉」相同的 action,只有最後一個 action 會發起 API 請求。也就是咱們前面提到過的 API 去重。可是對於 request action 的回調函數來講,可能會有下面兩種不一樣的需求:

  1. 每一個相同 request action 所對應的 onSuccess/onFail 回調在請求成功時都會被執行。
  2. 只執行真正發起請求的這個 action 所對應的 onSuccess/onFail 回調。

對於第一個場景來講,咱們能夠判斷 action 的 type 和 payload 是否一致,若是一致就執行對應的 callback,這樣相同 action 的回調均可以被執行。對於第二個場景,咱們能夠從 action 的 payload 上作點「手腳」,action 的 payload 放置的是咱們發起請求時須要的 request config,經過添加一個 UUID,可讓這個和其餘 action「相同」的 action 變得「不一樣」,這樣就只會執行這個 request action 所對應的回調函數。

組件卸載

一般咱們會使用 Promise 或者 XMLHttpRequest 發起 API 請求,但因爲 API 請求是異步的,在組件卸載以後,它們的回調函數仍然會被執行。這就可能致使一些問題,好比在已卸載的組件裏執行 setState。

組件被卸載以後,組件內部的邏輯應該隨之「銷燬」,咱們不該該再執行任何組件內包含的任何邏輯。利用 RxJS,useRequest 可以在組件銷燬時自動取消全部邏輯。換句話說,就是再也不執行請求成功或者失敗的回調函數。

2. 存儲並使用請求響應的數據

對於 API Response 這一類數據,咱們應該如何存儲呢?因爲不一樣的 API Response 數據對應用有着不一樣的做用,所以咱們能夠抽象出對應的數據模型,而後分類存儲。就像咱們收納生活用品同樣,第一個抽屜放餐具,第二個抽屜放零食......

1567418882391-583b61d5-7c01-4ced-a992-7cf648f8184a-20190911222034323.png

按照數據變化的頻率,或者說數據的存活時間,咱們能夠將 API response 大體歸爲兩類:

一類是變化頻率很是高的數據,好比排行榜列表,可能每一秒都在發生變化,這一類數據沒有緩存價值,咱們稱之爲臨時數據(temporary data)。臨時數據用完以後會被銷燬。

另外一類是不常發生變化的數據,咱們稱之爲實體數據(entity),好比國家列表、品牌列表。這一類數據不少時候須要緩存到本地,將它們歸爲一類更易於作數據持久化。

2.1 useTempData

2.1.2 背景

經過 useRequest 咱們已經可以很是方便的去調用 API 請求了。可是對於大部分業務場景來講,仍是會比較繁瑣。試想一個很是常見的需求:將 API 數據渲染到頁面上。咱們一般須要如下幾個步驟:

Step1: 組件 mount 時,dispatch 一個 request action。這一步能夠經過 useRequest 實現。

Step2: 處理 request success action,並將數據存入 store 中。

Step3: 從 store 的 state 中 pick 出對應的數據,並將其提供給組件。

Step4: 組件拿到數據並渲染頁面。

Step5: 執行某些操做以後,用新的 request 參數從新發起請求。

Step6: 重複 Step二、Step三、Step4。

若是每一次集成 API 都要經過上面的這些步驟才能完成,不只會浪費大量時間,也會生產大量模板代碼。而且,因爲邏輯很是地分散,咱們沒法爲它們統一添加測試,所以須要在每一個使用的地方單獨去測。可想而知,開發效率必定會大打折扣。

爲了解決這個問題,咱們抽象了 useTempData。以前也提到過 temp data 的概念,其實它就是指頁面上的臨時數據,一般都是「閱後即焚」。咱們項目上經過 API 請求獲取的數據大部分都是這一類。useTempData 主要用於在組件 mount 時自動獲取 API 數據,並在組件 unmount 時自動銷燬它們。

2.1.3 輸入和輸出

useTempData 會在組件 mount 時自動分發 request action,當請求成功以後將響應數據存入 redux  store,而後從 store 提取出響應數據,將響應數據提供給外部使用。固然,你也能夠經過配置,讓 useTempData 響應請求參數的變化,當請求參數發生變化時,useTempData 會攜帶新的請求參數從新發起請求。

其核心的輸入輸出以下:

export const useTempData = <T extends IRequestActionCreator<T["TReq"], T["TResp"]>>(
  actionCreator: T,
  args?: T["TReq"],
  deps: DependencyList = [],
) => {
  // ...
  return [data, requestStage, fetchData] as [
    typeof actionCreator["TResp"],
    typeof requestStage,
    typeof fetchData,
  ];
};

它接收 actionCreator 做爲第一個參數,用於建立相應的 request action。當組件 mount 時,會自動分發 request action。args 做爲第二個參數,用於設置請求參數。 deps 做爲第三個參數,當它發生變化時,會從新分發 request action。

同時,它會返回 API 響應的數據 data、表示請求當前所處階段的 requestStage  以及用於分發 request action 的函數 fetchData 。

使用起來也很是方便,若是業務場景比較簡單,集成 API 就是一行代碼的事:

const [books] = useTempData(getBooksUsingGET, { bookType }, [bookType]);

// 拿到 books 數據,渲染 UI

2.1.4 實現思路

useTempData 基於 useRequest 實現。在組件 mount 時分發 request action,而後在請求成功的回調函數 onSuccess 中再分發另外一個 action,將請求響應的數據存入 redux store。

const [fetchData] = useRequest(actionCreator, {
  success: (action) => {
    dispatch(updateTempData(groupName, reducer(dataRef.current, action))),
  },
});

useEffect(() => {
  fetchData(args as any);
}, deps);

2.1.5 組件卸載

當組件卸載時,若是 store 的 state 已經保存了這個 request action 成功響應的數據,useTempData 會自動將它清除。發起 API 請求以後,若是組件已經卸載,useTempData 就不會將請求成功響應的數據存入 redux store。

2.2 useEntity

基於 useTempData 的設計,咱們能夠封裝 useEntity, 用於統一處理 entity 這類數據。這裏再也不贅述。

3. 自動生成代碼

利用代碼生成工具,咱們能夠經過 swagger 文檔自動生成 request action creator 以及接口定義。而且,每一次都會用服務端最新的 swagger json 來生成代碼。這在接口變動時很是有用,只須要一行命令,就能夠更新接口定義,而後經過 TypeScript 的報錯提示,依次修改使用的地方便可。

同一個 swagger 生成的代碼咱們會放到同一個文件裏。在多人協做時,爲了不衝突,咱們會將生成的 request action creator 以及接口定義按照字母順序進行排序,而且每一次生成的文件都會覆蓋以前的文件。所以,咱們在項目上還硬性規定了:生成的文件只能自動生成,不可以手動修改。

4. 最後

自動生成代碼工具爲咱們省去了很大一部分工做量,再結合咱們以前講過的 useRequest、useTempData 和 useEntity,集成 API 就變成了一項很是輕鬆的工做。

相關文章
相關標籤/搜索