关于javascript:简单优雅的JavaScript代码片段三合并请求成批发出

37次阅读

共计 5350 个字符,预计需要花费 14 分钟才能阅读完成。

简略优雅的 JavaScript 代码片段文章系列:
简略优雅的 JavaScript 代码片段(一):异步控制
简略优雅的 JavaScript 代码片段(二):流控和重试
简略优雅的 JavaScript 代码片段(三):合并申请,成批收回

场景阐明

后端提供的接口具备批量查问能力(比方,同时查问 10 个资源的详情信息),然而调用者每次只须要申请一个资源的详情信息。

比方:

import React from "react";

const List = ({ids}: {ids: string[] }) => {
  return (
    <div>
      {ids.map((id) => (<ResourceDetail key={id} id={id} />
      ))}
    </div>
  );
};

const ResourceDetail = ({id}: {id: string}) => {
  // 在这个组件申请资源详情并渲染...
  // 留神在这个组件中,你只关怀这一个资源的信息,你不再具备”整个列表“的数据视角。};

// 后端接口反对同时申请多个资源的信息
declare function api(ids: string[]): Promise<Record<string, {details: any}>>;

这个时候,个别只有 2 个计划:

  • 间接应用批量接口来申请单个资源的信息。在 ResourceDetail 组件中,间接调用 api 来申请以后资源的信息:api([id]),只传入以后资源 ID。

    • 益处是简略间接,易于了解;
    • 害处是 没有将接口自身的批量申请能力利用起来,收回过多申请,导致接口流控限度或效率低下。
  • 将申请逻辑晋升至更高的组件档次,在具备”整个列表“的数据视角的时候进行批量申请。在下层的 List 组件中,批量申请多个资源的信息:api([id1, id2, ...]),而后将后果传递给子组件进行渲染。

    • 益处是将接口自身的批量申请能力利用起来;
    • 害处是 代码耦合严密 (ResourceDetail 依赖父组件帮它申请资源信息); 代码逻辑设计投合技术因素,不合乎直觉 造成 List 组件职责扩充、逻辑简单(比方接口每次最多只能反对 10 个资源批量申请,因而你要将列表分为 10 个一组,别离申请)。

有没有两败俱伤计划呢?既能让组件职责简略清晰,又能将接口自身的批量申请能力利用起来?

解决方案

这篇文章介绍一个工具函数,将一个【批量申请】的接口转换成【单个申请】的接口:wrapBatchProcess。示例用法:

// 先应用 wrapBatchProcess 工具将 api 转换成单个申请的接口
// const wrappedAPI: (input: string) => Promise<{details: any}>
const wrappedAPI = wrapBatchProcess<string, {details: any}>(async (inputs, onResult, onError) => {
    // 工具函数将「会集」成一大批申请,调用你提供的这个回调函数
    // 因而你就能够在这个回调中同时解决一大批申请了!const result = await api(inputs.map(({ input}) => input))
    Object.keys(result).forEach((key) => {
      // 申请到后果当前,将后果提供给工具函数,而后工具函数就会将后果提供给调用者
      onResult(result[key], key)
    })
  },
  // getKey 函数,在这个场景下比较简单。实现中的代码正文有解释
  (input) => input
)

// 调用 wrappedAPI,每次只须要传入一个申请。工具会主动合并申请,通过 api 来批量收回。// 后果达到当前,wrappedAPI 会将【对应于这个申请的后果】返回回来
wrappedAPI(id) // Promise<{details: any}>

wrappedAPI实质上是一个申请的缓冲队列(buffer),尽管它一个一个地承受申请,然而它不会立刻将申请收回,而是期待一段时间(debounce),将申请累积起来,而后将累积起来的申请成批收回。

wrapBatchProcess就是一个创立 wrappedAPI 的工具函数。用户传入一个函数来定义【如何解决一批申请】,而后它给用户返回wrappedAPI,一个一个地接管申请。

劣势

封装后的 api 非常简单:(input) => Promise<TheOutput>。因而 api 使用者(ResourceDetail)只须要 简略地调用wrappedAPI,就能够拿到它想要的数据!

使用者(ResourceDetail)不须要关怀【申请是如何合并收回的】,简单的申请合并逻辑被抽离到了【wrapBatchProcess 的调用代码】中,这些代码齐全能够放在其余的文件中(关注点拆散)。不会净化 ResourceDetail 和 List 组件。

接口自身的批量申请能力被充分利用,晋升申请效率,缩小申请数量。

实现代码

import debounce from "lodash.debounce";

/**
 *「会集」解决申请,成批处理。* 将「批量申请」接口封装成「一一申请」的接口。* 让使用者享受「一一解决」的简略性,同时在底层通过「批量申请」来取得效率晋升。* 使用者只须要关怀以后 input 的解决,而不须要关怀这个 input 是如何与其余 input 一起成批申请的(关注点拆散)。*/
export function wrapBatchProcess<InputItem, OutputItem>(
  // 之所以通过 callback 的形式来返回数据,是因为要反对分批屡次返回(拿到一批响应就立即返回给调用者),而不是期待所有后果达到当前再全副一次性返回
  fn: (inputs: { input: InputItem; key: string}[],
    /**
     * 返回后果的时候,须要返回 key,与 input 的 key 对应,这样咱们能力晓得每个 output 对应于哪个 input
     */
    onResult: (output: OutputItem, key: string) => void,
    onError: (error: any, key: string) => void
  ) => void,
  // input 可能是一个简单对象,而 wrapBatchProcess 须要一个 string 来标识一个申请
  getKey: (input: InputItem) => string
  /** 封装后函数的使用者不须要理解 key 的概念 */
): (input: InputItem) => Promise<OutputItem> {let buffer: Map<string, BufferItem> = new Map();

  const check = debounce(() => {if (buffer.size === 0) return;
      // 将整个 buffer 作为一批,发出请求,并清空 buffer
      const batch = new Map(buffer);
      buffer = new Map();
      const inputs = Array.from(batch.values()).map((item) => ({
        key: item.key,
        input: item.input,
      }));
      fn(
        inputs,
        (output, key) => {const item = batch.get(key);
          if (!item) return;
          item.resolve(output);
          batch.delete(key);
        },
        (error, key) => {const item = batch.get(key);
          if (!item) return;
          item.reject(error);
          batch.delete(key);
        }
      );
    },
    // 期待若干毫秒作为一个申请收集窗口,而后将收集到的所有申请作为一批收回
    50,
    {
      leading: false,
      trailing: true,
      // 防止一直有申请到来,导致 debounce 始终无奈被调用,这个参数可调
      maxWait: 200,
    }
  );

  function schedule(input: InputItem) {const key = getKey(input);
    // 如果曾经有雷同的 input 在 buffer 中,则不反复调度它,而是与前一个 input 共享同一个后果
    const existBufferItem = buffer.get(key);
    if (existBufferItem) return existBufferItem.promise;
    // 将 input 信息退出 buffer 中,筹备调度
    const bufferItem: BufferItem = {
      input,
      key,
      ...createControllablePromise<OutputItem>(),};
    buffer.set(key, bufferItem);
    check();
    return bufferItem.promise;
  }

  return schedule;

  type BufferItem = {
    input: InputItem;
    key: string;
    promise: Promise<OutputItem>;
    resolve: (ret: OutputItem) => void;
    reject: (error: any) => void;
  };
}

function createControllablePromise<T>(): {
  promise: Promise<T>;
  resolve: (ret: T) => void;
  reject: (error: any) => void;
} {let result: any = {};
  result.promise = new Promise<T>((resolve, reject) => {
    result.resolve = resolve;
    result.reject = reject;
  });
  return result;
}

接口批量能力无限

很多时候,尽管后端接口反对在一个申请中同时查问多个资源 ID 的信息,然而这个反对的数量也会有一个下限。比方每个申请最多只反对查问 10 个资源 ID。

那么咱们在调用 api 之前也须要做对应的适配:

const wrappedAPI = wrapBatchProcess<string, {details: any}>(async (inputs, onResult, onError) => {
    // 因为 api 接口每次最多只能反对 10 个资源查问,而 inputs 数组可能很多
    // 因而要先将 inputs 切分成多个组,每 10 个资源一组,确保 api 能承受
    const groups: string[][] = splitArrayIntoGroups(inputs.map(({ input}) => input),
      10
    );
    groups.forEach(async (group: string[]) => {const result = await api(group);
      Object.keys(result).forEach((key) => {onResult(result[key], key);
      });
    });
  },
  (input) => input
);

function splitArrayIntoGroups<T>(array: T[], groupSize: number): T[][] {const result: T[][] = [];
  for (let i = 0; i < array.length; i += groupSize) {const group = array.slice(i, i + groupSize);
    result.push(group);
  }
  return result;
}

可组合性

这篇文章介绍的工具函数能够与简略优雅的 JavaScript 代码片段(二):流控和重试介绍的工具函数组合应用。

比方,在咱们后面介绍的”接口批量能力无限“的例子中,如果一次性到来了 110 个资源 ID,那么在成批收回的时候,就会同时收回 11 个申请(每个申请蕴含 10 个资源 ID 查问)。

然而咱们的 api10 次 / 秒 的流控限度。如何防止超出这个限度?

你能够利用上篇文章介绍的 wrapFlowControl,将api 加强成apiWithFlowControl(适配流控的申请办法):

const apiWithFlowControl = wrapFlowControl(api, 10);

const wrappedAPI = wrapBatchProcess<string, {details: any}>(async (inputs, onResult, onError) => {const groups: string[][] = splitArrayIntoGroups(inputs.map(({ input}) => input),
      10
    );
    groups.forEach(async (group: string[]) => {
      // 这一行替换成 apiWithFlowControl 即可!const result = await apiWithFlowControl(group);
      Object.keys(result).forEach((key) => {onResult(result[key], key);
      });
    });
  },
  (input) => input
);

这样,你取得了 wrapBatchProcess 的所有益处,同时还能确保它会 在流控容许的范畴内调度 api

正文完
 0