Orion's Studio.

自动批量请求(2)—— TS 化和代码逻辑优化

2021/01/12

先看图来回顾一下(假设一次 EventLoop 占用 5 个时间单位,请求回复也需要 5 个时间单位):

自动批量请求

我们已经实现了自动把同一时刻的请求合并,并统一渲染。

但写完之后,发现很多写法都比较 low,所以对代码做了一次整体重构。

TS 化

首先,我们把之前涉及到的对象都用 typescript 定义,以方便后期维护:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 通用对象
*/
export type IObject = Record<string | number | symbol, any>;

/**
* 通用返回体
*/
interface IResponse {
code: number; // 状态码,200 为正常
data: IObject; // 返回数据
message?: string; // 报错信息
msg?: string; // 报错信息,某些接口用缩写返回
}

/**
* 继承 Error 的错误返回体
*/
interface IErrorResponse extends Error {
code?: number; // 补充错误的状态码
}

拆分代码功能

原有的逻辑是把所有功能耦合在一次顺序执行,但实际上整个批量请求的流程可以分为:

  1. 添加请求到队列中
  2. 发送请求
  3. 清空请求队列
  4. 处理请求回调

注:处理请求回调是异步执行的,而清空请求队列是每个 EventLoop 发送完请求都会执行一遍的,所以实际发生的时间是:清空在前,处理回调在后。

这其中,第 2 和 第 3 就可以抽成 addRequestclearRequest 方法。

合并松散对象

原先的逻辑是,根据请求 url 作区分,分别用三个对象存请求参数、成功和错误回调的信息,这样存的好处是 paramsMap 可以直接作为 /api/batch 的请求参数。

但是这样子存,感觉过于分散,其实可以耦合成一个对象:

1
2
3
4
5
6
7
8
9
/**
* 单个请求的元数据信息
*/
type IRequestItem = {
name: string; // 请求 url
data: IObject; // 请求参数
success: (obj: IObject) => void; // 成功回调
error: (err: Error) => void; // 错误回调
};

然后把多个请求组合成一个新的 Map 对象,同时在 addRequest 中添加如下处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 同一个队列多个请求组成的 Map,键名是 api
*/
type IRequestMap = {
[url: string]: IRequestItem; // 每个请求对应的元数据信息
};

let requestMap: IRequestMap = {}; // 同一个队列下多个请求组成的 Map,键名是 api

/**
* 增加请求到队列中
* @param requestItem 单个请求信息
*/
const addRequest: (requestItem: IRequestItem) => void = (requestItem) => {
const { name, data, success, error } = requestItem;
requestMap[name] = {
name,
data: deepStringify(data),
success,
error
};
};

// 调用并传参
addRequest({
name,
data: deepStringify(params.data), // 深度 stringify 对象,batch 请求需要
success: params.success ?? success,
error: params.error ?? error
});

以及在 clearRequest 中添加如下处理:

1
2
3
4
5
6
/**
* 清空请求队列
*/
const clearRequest: () => void = () => {
requestMap = {};
};

然后如下方式转化成 batch 请求的参数:

1
2
3
4
5
6
7
const batchData = Object.values(requestMap).reduce(
(pre: IObject, cur: IRequestItem) => {
const { name: requestName, data } = cur;
return { ...pre, [requestName]: data };
},
{}
);

以及封装下发送批量请求的函数 sendRequestBatch,使得逻辑更为清晰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* batch 请求的返回结构,键名是 api
*/
type IBatchResponse = {
[url: string]: IResponse; // 每个请求对应的返回信息
};

/**
* 发送批量请求
* @param tempRequestMap 临时缓存的变量,在执行请求回调时,原来的 requestMap 其实已经清空了
*/
const sendRequestBatch: (tempRequestMap: IRequestMap) => void = (
tempRequestMap
) => {
// 转化为 batch 请求需要的格式
const batchData = {}; /* 见上述代码 */

// 调用批处理请求
fetchBase('/api/batch', {
data: batchData
})
.then((res: IBatchResponse) => {
// 用 unstable_batchedUpdates 统一更新,减少渲染次数
unstable_batchedUpdates(() => {
Object.keys(res).forEach((key) => {
const { code, message, msg, data } = res[key];
const {
success: successCallback,
error: errorCallback
} = tempRequestMap[key];
if (code !== 200) {
const err: IErrorResponse = new Error(
(message || msg) ?? '系统繁忙,请稍后再试'
);
err.code = code;
if (errorCallback) {
errorCallback(err);
}
} else if (successCallback) {
successCallback(data);
}
});
});
})
.catch((err: Error) => {
// 如果 batch 请求报错了,则给每个请求都返回 batch 请求的报错
Object.keys(tempRequestMap).forEach((key) => {
const { error: errorCallback } = tempRequestMap[key];
if (errorCallback) {
errorCallback(err);
}
});
});
};

最后删除 count,直接用 requestMap 的 keys 数量做判断,就生成了最后的 fetchBatch 函数啦:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/**
* fetch 的 options 对象
*/
interface IFetchOptions {
data: IObject;
}

/**
* 空函数
*/
const noop = () => {};

/**
* 自动批处理及批量更新的 fetch,通过闭包保存一些变量
*/
export const fetchBatch = (function anonymousFetch() {
let batchFlag: Promise<void> | null = null; // 标记一次 EventLoop 上是否已存在其他请求
let requestMap: IRequestMap = {}; // 同一个队列(batch)下多个请求组成的 Map,键名是 api

const sendRequestBatch = () => {}; /* 见上述代码 */
const addRequest = () => {}; /* 见上述代码 */
const clearRequest = () => {}; /* 见上述代码 */

// 实际返回匿名函数
return function anonymous(
name: string = '', // 透传 api 地址
options: IParams = { data: {} }, // 透传 options 参数对象,格式为 { data: {} }
success: (obj: IObject) => void = noop, // 成功回调
error: (err: Error) => void = noop // 错误回调
) {
// 如果已存在,就直接发送请求
if (requestMap[name]) {
return fetch(name, options).then(success, error);
}

// 增加请求到队列中
addRequest({
name,
data: deepStringify(options.data), // 深度 stringify 对象,batch 请求需要
success,
error
});

// batchFlag 为空,即同一个 EventLoop 的第一次请求时,创建 Promise
if (!batchFlag) {
// 用微任务合并同一次 EventLoop 上的请求
batchFlag = Promise.resolve().then(() => {
// 当数量大于 1 才启用 batch 请求
if (Object.keys(requestMap).length > 1) {
sendRequestBatch(requestMap);
} else {
// 等于 1 时,则降级到正常请求发送
return fetch(name, options).then(success, error);
}

// 清空变量
clearRequest();
});
}

return null;
};
})();

可以见得,重构后的代码,精简了很多,逻辑也清晰了很多。

重构到此就初步大功告成啦!~ (^▽^)

CATALOG
  1. 1. TS 化
  2. 2. 拆分代码功能
  3. 3. 合并松散对象