一、背景
- 项目中原本展示的导航距离是直线导航距离,因不符合实际故迭代为调用“电单车导航服务”;
- 地图侧提供的“电单车导航接口”只能单个调用,即一个接口只能获取 1 组(2 个坐标之间)的导航信息,而不能一次获取多组坐标的导航信息;
二、风险
同一门店在同一时刻必然存在大量订单和骑手,如果要批量获取导航信息,必定要同时并发大量请求,此时带来的问题有:
- 接口串行通信导致延迟大(可排除,线上采用 http2);
- 业务后端需要接受大并发考验,可能因为本需求而被迫扩张;
- 对于没有移动的坐标组重复调用导致经费的浪费(集团内的服务也要收钱);
三、方案
前端
- 最小请求原则,只请求运营人员观察内的订单或骑手对应的导航信息;
- 最大复用原则,未变更的坐标组,直接复用上次的结果,而不是重新请求;
后端
- 后端对二方服务进行封装,封装成批量调用接口暴露给前端;
- 响应
list
每一个item
提供唯一标识符serialNo
,响应list
的item
同样带这个标识符,前端根据该标识符进行区分。
四、方案细节
页面状态流图
以下是页面的数据状态流图,红色部分是本次修改的内容:
缓存方案流程图
编码细节
请求打包函数
因为后端对批量接口的长度进行了限制,限制为最多请求 50 组,所以前端这里需要把批量的请求按照 50 一组进行分割,请求完毕后再打包。
/**
* 打包请求
*/
function zipFetch<ParamsType, FetchRes>(
params: ParamsType,
chunkFn: (params: ParamsType) => ParamsType[],
fetchFn: (params: ParamsType) => Promise<FetchRes>,
) {
const paramsArr = chunkFn(params);
// HACK: Promise.allSettled
return Promise.all(paramsArr.map((params) => promiseAllSettledWrapper(fetchFn)(params))).then((res) => {
// 有一个 promise 成功则认为本次请求成功
const isSuccess = res.some((promise) => promise.status === PromiseStatus.FULFILLED);
if (!isSuccess) {
// 如果失败,则把第一个错误抛出
const errorRes = (res[0] as PromiseRejectedResult)?.reason;
const msg = errorRes?.message;
throw new Error(msg);
}
// 请求成功,把所有的 resolved 的值合并到一起返回
return res.reduce((final, cur) => {
if (cur.status === PromiseStatus.FULFILLED) {
const { value } = (cur as PromiseFulfilledResult<FetchRes>);
return {
...final,
...value,
};
}
return final;
}, {} as FetchRes);
});
}
/*
* 可以直接在业务代码内使用的请求函数
*/
function fetchNavData(params: FetchNavDataQuery) {
return zipFetch<FetchNavDataQuery, NavigateData>(
params,
(params) => {
const { coordinates } = params;
const chunkedParams = chunk(coordinates, 50);
return chunkedParams.map((chunk) => ({
coordinates: chunk,
}));
},
request,
);
}
请求生成器
这里用函数实现主要为了实现每次调用时生成一个干净的闭包用来存放 query
,至于为什么分别实现 pushQuery
& fetch
则是为了关注点分离。
/**
* 请求生成器
*/
function fetchCreator(
succeedCallback?: SucceedCallback,
failedCallback?: FailedCallback,
) {
const query: CoordinateItem[] = [];
const result = {
pushQuery: (params: PushQueryParams) => {
const { srcLocation, destLocation, serialId } = params;
if (srcLocation && destLocation) {
query.push({
serialId,
originCoordinate: srcLocation.join(','),
destinationCoordinate: destLocation.join(','),
});
}
},
fetch: () => {
if (query.length) {
return fetchNavData({ coordinates: query }).then((res) => {
succeedCallback?.(res);
return res;
}).catch((e: Error) => {
failedCallback?.(e);
return {} as NavigateData;
});
}
return Promise.resolve({} as NavigateData).then((res) => succeedCallback?.(res));
},
};
return result;
}
带有缓存的请求生成器
该函数返回的 pushQuery
会对批量传入的坐标组进行检查,看是否有坐标组已经被缓存,如果已经有缓存,那么使用缓存内容,否则将坐标组放到 query
中进行请求,等待请求回来后,将请求内容和缓存内容 merge
到一起进行返回。
/**
* 带有缓存的请求生成器
*/
export function fetchWithCacheCreator(
cache: CacheManager<NavRoute>,
succeedCallback?: SucceedCallback,
failedCallback?: FailedCallback,
) {
const { pushQuery, fetch } = fetchGenerator(undefined, failedCallback);
let curRequestCache = Object.create(null);
const pushCurRequestCache = (key: string, data: NavRoute) => {
curRequestCache[key] = data;
};
const clearCurRequestCache = () => {
curRequestCache = Object.create(null);
};
return {
pushQuery: (params: PushQueryParams) => {
const { serialId } = params;
const cacheItem = cache.getCache(serialId);
if (cacheItem) {
pushCurRequestCache(serialId, cacheItem);
} else {
pushQuery(params);
}
},
fetch: () => fetch().then((res) => {
const mergeCachedData = { ...curRequestCache, ...res };
cache.setCache(mergeCachedData);
succeedCallback?.(mergeCachedData);
return mergeCachedData;
}).finally(clearCurRequestCache),
};
}
五、改进点
骑手到门店 & 骑手到客户之前的导航信息没有使用缓存,其实这里前端也是可以缓存的,通过修改 CacheManager
可以判断骑手如果活动距离过短可以认为其没有移动,然后返回缓存内容。
interface ICacheManager<CacheItem> {
/**
* 设置缓存
*/
setCache: (data: Record<string, CacheItem>) => void
/**
* 取缓存
*/
getCache: (key: string) => CacheItem | undefined
/**
* 删除某个缓存
*/
delCache: (key: string) => void
/**
* 清理缓存
*/
clearCache: () => void
/**
* 新增
* 判断骑手是否没有移动
*/
isNotRemoved: () => boolean
}
export class CacheManager<T> implements ICacheManager<T> {
private cacheMap: Record<string, T>
constructor() {
this.cacheMap = Object.create(null);
}
setCache(data: Record<string, T>) {
forEach(data, (item, key) => {
this.cacheMap[key] = item;
});
}
getCache(key: string) {
if (this.isNotRemoved()) {
return this.cacheMap[key];
}
this.delCache(key);
return undefined;
}
delCache(key: string) {
delete this.cacheMap[key];
}
clearCache() {
this.cacheMap = Object.create(null);
}
isNotRemoved() {
// 实现判断逻辑
return true;
}
}