如何使用插件化機制優雅的封裝你的請求

来源:https://www.cnblogs.com/gopal/archive/2022/08/11/16573866.html
-Advertisement-
Play Games

useRequest 是 ahooks 最核心的功能之一,它的功能非常豐富,但核心代碼(Fetch 類)相對簡單,這得益於它的插件化機制 ...


本文是深入淺出 ahooks 源碼系列文章的第二篇,該系列已整理成文檔-地址。覺得還不錯,給個 star 支持一下哈,Thanks。

本文來講下 ahooks 的核心 hook —— useRequest。

useRequest 簡介

根據官方文檔的介紹,useRequest 是一個強大的非同步數據管理的 Hooks,React 項目中的網路請求場景使用 useRequest 就夠了。

useRequest 通過插件式組織代碼,核心代碼極其簡單,並且可以很方便的擴展出更高級的功能。目前已有能力包括:

  • 自動請求/手動請求
  • 輪詢
  • 防抖
  • 節流
  • 屏幕聚焦重新請求
  • 錯誤重試
  • loading delay
  • SWR(stale-while-revalidate)
  • 緩存

這裡可以看到 useRequest 的功能是非常強大的,如果讓你來實現,你會如何實現?也可以從介紹中看到官方的答案——插件化機制。

架構


如上圖所示,我把整個 useRequest 分成了幾個模塊。

  • 入口 useRequest。它負責的是初始化處理數據以及將結果返回。
  • Fetch。是整個 useRequest 的核心代碼,它處理了整個請求的生命周期。
  • plugin。在 Fetch 中,會通過插件化機制在不同的時機觸發不同的插件方法,拓展 useRequest 的功能特性。
  • utils 和 types.ts。提供工具方法以及類型定義。

useRequest 入口處理

先從入口文件開始,packages/hooks/src/useRequest/src/useRequest.ts

function useRequest<TData, TParams extends any[]>(
  service: Service<TData, TParams>,
  options?: Options<TData, TParams>,
  plugins?: Plugin<TData, TParams>[],
) {
  return useRequestImplement<TData, TParams>(service, options, [
    // 插件列表,用來拓展功能,一般用戶不使用。文檔中沒有看到暴露 API
    ...(plugins || []),
    useDebouncePlugin,
    useLoadingDelayPlugin,
    usePollingPlugin,
    useRefreshOnWindowFocusPlugin,
    useThrottlePlugin,
    useAutoRunPlugin,
    useCachePlugin,
    useRetryPlugin,
  ] as Plugin<TData, TParams>[]);
}

export default useRequest;

這裡第一(service 請求實例)第二個參數(配置選項),我們比較熟悉,第三個參數文檔中沒有提及,其實就是插件列表,用戶可以自定義插件拓展功能。

可以看到返回了 useRequestImplement 方法。主要是對 Fetch 類進行實例化。

const update = useUpdate();
// 保證請求實例都不會發生改變
const fetchInstance = useCreation(() => {
  // 目前只有 useAutoRunPlugin 這個 plugin 有這個方法
  // 初始化狀態,返回 { loading: xxx },代表是否 loading
  const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);
  // 返回請求實例
  return new Fetch<TData, TParams>(
    serviceRef,
    fetchOptions,
    // 可以 useRequestImplement 組件
    update,
    Object.assign({}, ...initState),
  );
}, []);
fetchInstance.options = fetchOptions;
// run all plugins hooks
// 執行所有的 plugin,拓展能力,每個 plugin 中都返回的方法,可以在特定時機執行
fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));

實例化的時候,傳參依次為請求實例,options 選項,父組件的更新函數,初始狀態值。

這裡需要非常留意的一點是最後一行,它執行了所有的 plugins 插件,傳入的是 fetchInstance 實例以及 options 選項,返回的結果賦值給 fetchInstance 實例的 pluginImpls

另外這個文件做的就是將結果返回給開發者了,這點不細說。

Fetch 和 Plugins

接下來最核心的源碼部分 —— Fetch 類。其代碼不多,算是非常精簡,先簡化一下:

export default class Fetch<TData, TParams extends any[]> {
  // 插件執行後返回的方法列表
  pluginImpls: PluginReturn<TData, TParams>[];
  count: number = 0;
  // 幾個重要的返回值
  state: FetchState<TData, TParams> = {
    loading: false,
    params: undefined,
    data: undefined,
    error: undefined,
  };

  constructor(
    // React.MutableRefObject —— useRef創建的類型,可以修改
    public serviceRef: MutableRefObject<Service<TData, TParams>>,
    public options: Options<TData, TParams>,
    // 訂閱-更新函數
    public subscribe: Subscribe,
    // 初始值
    public initState: Partial<FetchState<TData, TParams>> = {},
  ) {
    this.state = {
      ...this.state,
      loading: !options.manual, // 非手動,就loading
      ...initState,
    };
  }

  // 更新狀態
  setState(s: Partial<FetchState<TData, TParams>> = {}) {
    this.state = {
      ...this.state,
      ...s,
    };
    this.subscribe();
  }

  // 執行插件中的某個事件(event),rest 為參數傳入
  runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) {
    // 省略代碼...
  }

  // 如果設置了 options.manual = true,則 useRequest 不會預設執行,需要通過 run 或者 runAsync 來觸發執行。
  // runAsync 是一個返回 Promise 的非同步函數,如果使用 runAsync 來調用,則意味著你需要自己捕獲異常。
  async runAsync(...params: TParams): Promise<TData> {
    // 省略代碼...
  }
  // run 是一個普通的同步函數,其內部也是調用了 runAsync 方法
  run(...params: TParams) {
    // 省略代碼...
  }

  // 取消當前正在進行的請求
  cancel() {
    // 省略代碼...
  }

  // 使用上一次的 params,重新調用 run
  refresh() {
    // 省略代碼...
  }

  // 使用上一次的 params,重新調用 runAsync
  refreshAsync() {
    // 省略代碼...
  }

  // 修改 data。參數可以為函數,也可以是一個值
  mutate(data?: TData | ((oldData?: TData) => TData | undefined)) {
    // 省略代碼...
}

state 以及 setState

在 constructor 中,主要是進行了數據的初始化。其中維護的數據主要包含一下幾個重要的數據以及通過 setState 方法設置數據,設置完成通過 subscribe 調用通知 useRequestImplement 組件重新渲染,從而獲取最新值。

// 幾個重要的返回值
state: FetchState<TData, TParams> = {
  loading: false,
  params: undefined,
  data: undefined,
  error: undefined,
};
// 更新狀態
setState(s: Partial<FetchState<TData, TParams>> = {}) {
  this.state = {
    ...this.state,
    ...s,
  };
  this.subscribe();
}

插件化機制的實現

上文有提到所有的插件運行的結果都賦值給 pluginImpls。它的類型定義如下:

export interface PluginReturn<TData, TParams extends any[]> {
  onBefore?: (params: TParams) =>
    | ({
        stopNow?: boolean;
        returnNow?: boolean;
      } & Partial<FetchState<TData, TParams>>)
    | void;

  onRequest?: (
    service: Service<TData, TParams>,
    params: TParams,
  ) => {
    servicePromise?: Promise<TData>;
  };

  onSuccess?: (data: TData, params: TParams) => void;
  onError?: (e: Error, params: TParams) => void;
  onFinally?: (params: TParams, data?: TData, e?: Error) => void;
  onCancel?: () => void;
  onMutate?: (data: TData) => void;
}

除了最後一個 onMutate 之外,可以看到返回的方法都是在一個請求的生命周期中的。一個請求從開始到結束,如下圖所示:

如果你比較仔細,你會發現基本所有的插件功能都是在一個請求的一個或者多個階段中實現的,也就是說我們只需要在請求的相應階段,執行我們的插件的邏輯,就能完成我們插件的功能

執行特定階段插件方法的函數為 runPluginHandler,其 event 入參就是上面 PluginReturn key 值。

// 執行插件中的某個事件(event),rest 為參數傳入
runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) {
  // @ts-ignore
  const r = this.pluginImpls.map((i) => i[event]?.(...rest)).filter(Boolean);
  return Object.assign({}, ...r);
}

通過這樣的方式,Fetch 類的代碼會變得非常的精簡,只需要完成整體流程的功能,所有額外的功能(比如重試、輪詢等等)都交給插件去實現。這麼做的優點:

  • 符合職責單一原則。一個 Plugin 只做一件事,相互之間不相關。整體的可維護性更高,並且擁有更好的可測試性。
  • 符合深模塊的軟體設計理念。其認為最好的模塊提供了強大的功能,又有著簡單的介面。試想每個模塊由一個長方形表示,如下圖,長方形的面積大小和模塊實現的功能多少成比例。頂部邊代表模塊的介面,邊的長度代表它的複雜度。最好的模塊是深的:他們有很多功能隱藏在簡單的介面後。深模塊是好的抽象,因為它只把自己內部的一小部分複雜度暴露給了用戶。

核心方法 —— runAsync

可以看到 runAsync 是運行請求的最核心方法,其他的方法比如 run/refresh/refreshAsync 最終都是調用該方法。

並且該方法中就可以看到整體請求的生命周期的處理。這跟上面插件返回的方法設計是保持一致的。

請求前 —— onBefore

處理請求前的狀態,並執行 Plugins 返回的 onBefore 方法,並根據返回值執行相應的邏輯。比如,useCachePlugin 如果還存於新鮮時間內,則不用請求,返回 returnNow,這樣就會直接返回緩存的數據。

this.count += 1;
// 主要為了 cancel 請求
const currentCount = this.count;

const {
  stopNow = false,
  returnNow = false,
  ...state
  // 先執行每個插件的前置函數
} = this.runPluginHandler('onBefore', params);

// stop request
if (stopNow) {
  return new Promise(() => {});
}
this.setState({
  // 開始 loading
  loading: true,
  // 請求參數
  params,
  ...state,
});

// return now
// 立即返回,跟緩存策略有關
if (returnNow) {
  return Promise.resolve(state.data);
}

// onBefore - 請求之前觸發
// 假如有緩存數據,則直接返回
this.options.onBefore?.(params);

進行請求——onRequest

這個階段只有 useCachePlugin 執行了 onRequest 方法,執行後返回 service Promise(有可能是緩存的結果),從而達到緩存 Promise 的效果。

// replace service
// 如果有 cache 的實例,則使用緩存的實例
let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.current, params);

if (!servicePromise) {
  servicePromise = this.serviceRef.current(...params);
}

const res = await servicePromise;

useCachePlugin 返回的 onRequest 方法:

// 請求階段
onRequest: (service, args) => {
  // 看 promise 有沒有緩存
  let servicePromise = cachePromise.getCachePromise(cacheKey);

  // If has servicePromise, and is not trigger by self, then use it
  // 如果有servicePromise,並且不是自己觸發的,那麼就使用它
  if (servicePromise && servicePromise !== currentPromiseRef.current) {
    return { servicePromise };
  }

  servicePromise = service(...args);
  currentPromiseRef.current = servicePromise;
  // 設置 promise 緩存
  cachePromise.setCachePromise(cacheKey, servicePromise);
  return { servicePromise };
},

取消請求 —— onCancel

剛剛在請求開始前定義了 currentCount 變數,其實為了 cancel 請求。

this.count += 1;
// 主要為了 cancel 請求
const currentCount = this.count;

在請求過程中,開發者可以調用 Fetch 的 cancel 方法:

// 取消當前正在進行的請求
cancel() {
  // 設置 + 1,在執行 runAsync 的時候,就會發現 currentCount !== this.count,從而達到取消請求的目的
  this.count += 1;
  this.setState({
    loading: false,
  });

  // 執行 plugin 中所有的 onCancel 方法
  this.runPluginHandler('onCancel');
}

這個時候,currentCount !== this.count,就會返回空數據。

// 假如不是同一個請求,則返回空的 promise
if (currentCount !== this.count) {
  // prevent run.then when request is canceled
  return new Promise(() => {});
}

最後結果處理——onSuccess/onError/onFinally

這部分也就比較簡單了,通過 try...catch...最後成功,就直接在 try 末尾加上 onSuccess 的邏輯,失敗在 catch 末尾加上 onError 的邏輯,兩者都加上 onFinally 的邏輯。

try {
  const res = await servicePromise;
  // 省略代碼...
  this.options.onSuccess?.(res, params);
  // plugin 中 onSuccess 事件
  this.runPluginHandler('onSuccess', res, params);
  // service 執行完成時觸發
  this.options.onFinally?.(params, res, undefined);
  if (currentCount === this.count) {
    // plugin 中 onFinally 事件
    this.runPluginHandler('onFinally', params, res, undefined);
  }
  return res;
  // 捕獲報錯
} catch (error) {
  // 省略代碼...
  // service reject 時觸發
  this.options.onError?.(error, params);
  // 執行 plugin 中的 onError 事件
  this.runPluginHandler('onError', error, params);
  // service 執行完成時觸發
  this.options.onFinally?.(params, undefined, error);
  if (currentCount === this.count) {
    // plugin 中 onFinally 事件
    this.runPluginHandler('onFinally', params, undefined, error);
  }
  // 拋出錯誤。
  // 讓外部捕獲感知錯誤
  throw error;
}

思考與總結

useRequest 是 ahooks 最核心的功能之一,它的功能非常豐富,但核心代碼(Fetch 類)相對簡單,這得益於它的插件化機制,把特定功能交給特定的插件去實現,自己只負責主流程的設計,並暴露相應的執行時機即可。

這對於我們平時的組件/hook 封裝很有幫助,我們對一個複雜功能的抽象,可以儘可能保證對外介面簡單。內部實現需要遵循單一職責的原則,通過類似插件化的機制,細化拆分組件,從而提升組件可維護性、可測試性。

參考


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 首先nosql可以被理解為not only sql 泛指非關係型資料庫,也就是說不僅僅是sql,所以它既包含了sql的一些東西,但是又和sql不同,併在其的基礎上改變或者說擴展了一些東西。 提到nosql,首先我們就要分析一下關係型資料庫的行式存儲和非關係型資料庫的列式存儲區別在哪? 行式存儲我們都 ...
  • 原文:Jetpack Compose學習(9)——Compose中的列表控制項(LazyRow和LazyColumn) - Stars-One的雜貨小窩 經過前面的學習,大致上已掌握了compose的基本使用了,本篇繼續進行擴展,講解下載Compose中的列表控制項LazyRow和LazyColumn ...
  • 數據訂閱是運動健康類應用中很常見的功能,用戶訂閱運動記錄、健康記錄等數據,當這些數據發生變化時,用戶能夠即時在App上接收到推送通知。 例如某位用戶最近正在鍛煉身體,為自己設定了每天走1萬步,每周達成3次的目標;常規的數據訂閱可以做到每天給用戶推送當天步數,但用戶需要自己進行二次計算才能得知自己是否 ...
  • 盒模型 點擊打開視頻教程 標準盒模型、怪異盒模型(IE盒模型) 什麼是盒模型? 盒模型的作用:規定了網頁元素如何顯示以及元素間的相互關係 盒模型的概念:盒模型是css佈局的基石,它規定了網頁元素如何顯示以及元素間的相互關係。 css定義所有的元素都可以擁有像盒子一樣的外形和平面空間。即都包含內容區、 ...
  • 背景 項目中用到了vue的element-ui框架,用到了el-tree組件。由於數據量很大,使用了數據懶載入模式,即非同步樹。非同步樹採用覆選框進行結點選擇的時候,沒法自動展開,官方文檔找了半天也沒有找到好的辦法! 找不到相關的配置,或者方法可以使用。 經過調試與閱讀elment-ui源碼才發現有現成 ...
  • 有時候我們不希望組件被重新渲染影響使用體驗;或者處於性能考慮,避免多次重覆渲染降低性能。而是希望組件可以緩存下來,維持當前的狀態。這時候就需要用到keep-alive組件。 開啟keep-alive 生命周期的變化. 初次進入時: onMounted> onActivated 退出後觸發 deact ...
  • 本文是深入淺出 ahooks 源碼系列文章的第三篇,該系列已整理成文檔-地址。覺得還不錯,給個 star 支持一下哈,Thanks。 本文來探索一下 ahooks 是怎麼解決 React 的閉包問題的?。 React 的閉包問題 先來看一個例子: import React, { useState, ...
  • 介紹 es表示ECMASCript ,他是從es3,es5,es6,es5是2009.12月發佈的,es6是2015.6月發佈的。vue2完全支持es5的(vue3完全支持es6的),react完全支持es6 es5的新特性 嚴格模式(對應的相反的稱為怪異模式) 'use strict' //一般用 ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...