遇到了一个需求:用户选择或者拍照之后,将图片传递给一个图片识别的接口用于处理一些业务逻辑。但是这个接口对于图片的尺寸是有限制的,如果图片尺寸超过了限制直接返回空,这样体验就很不好了。
由于项目是移动应用,针对这个问题首先排除在 APP 端进行图片压缩处理,因为这样会造成 APP 的体验差,同时也会对移动设备的性能造成负面影响。既然不能在 APP 端处理,那只能在服务端处理了。服务端在上传图片的时候会对图片进行压缩,但是实际尝试了一下,这样会造成接口的速度变慢,每次查询至少会花 5 秒。这一个解决方案也被否定了。考虑到项目使用了 OSS,当前 OSS 的服务商也支持对图片进行处理,包括压缩,转换格式等。但是这样带来了一个问题,没法确定压缩率,即压缩的比例调到多少比较好,低比例压缩达不到要求的尺寸,比例高了会导致图片质量差影响识别。
要确定压缩的比例,只能先给定一个默认的图片质量,比如 80%,然后请求这个处理过的地址(当前 OSS 服务商并不支持直接获取处理过的图片的信息),判断是否小于限制,如果没有达到,在这个比例上进行一定比例的下调,直到小于限制。当然,并不一定要真正请求这个图片,不然效率太低。可以使用 HEAD
请求,然后通过 Content-Length
判断。
针对上述的解决方案进行编码的时候,最直观的解决方案就是通过 async / await
进行循环或者递归相关的函数。那么有没有更优雅的解决方案呢,是有的。RxJS 提供了 expand
操作符可以对 Observable
进行递归,可以利用这个操作符实现上述逻辑。
文档上针对 expand
操作符的说明:
Recursively projects each source value to an Observable which is merged in the output Observable.
下面是针对这一解决方案的 RxJS 实现。
const fetch$ = from(selectPhoto()).pipe(
map(({ uri, size }) => ({
uri,
size,
quality: DEFAULT_QUALITY,
})),
switchMap(({ size, quality, uri }) => {
// 如果图片已经小于限制,直接返回
if (size < MAX_FILE_SIZE) {
return of({ size, quality, uri });
}
return of({ size, quality, uri }).pipe(
expand(({ size, quality, uri }) =>
from(
fetch(`${uri}?x-oss-process=image/quality,Q_${quality}`, {
method: 'HEAD',
}),
).pipe(
// 调整质量参数,继续调用
map((result) => ({
size: Number(res.headers.get('content-length')),
quality: quality - DELTA,
uri,
})),
),
),
takeWhile(({ size }) => size > MAX_FILE_SIZE),
// 获取最后一个超过限制的参数
takeLast(1),
map(({ quality, ...rest }) => ({
quality: quality - DELTA,
...rest,
})),
);
}),
// 调用业务逻辑
switchMap(({ quality, uri }) =>
from(
request.post('/image/search', {
image: `${uri}?x-oss-process=image/quality,Q_${quality}`,
}),
),
),
catchError(() => of({ list: [] })),
map((res) => res?.list ?? []),
);