AFNetworking 在去年年末升级到了 3.0。这个版本更新想必有不少好处,然而让我吃惊的是,它并无 batch request 接口。以前的 1.x 版本、2.x 版本都实现了这个很常见的需求,不知道做者为什么选择在 3.x 中去掉它。html
在 AFNetworking 2 中,咱们只需一行代码就能解决批量上传的问题:ios
[AFURLConnectionOperation batchOfRequestOperations:operations progressBlock:^(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations) { NSLog(@"%lu 上传完成,共 %lu", (long)numberOfFinishedOperations, (long)totalNumberOfOperations); } completionBlock:^(NSArray *operations) { NSLog(@"上传完毕"); }];
但 AFNetworking 3 用的是NSURLSession
,而不是用NSOperation
来包装NSURLConnection
,因此把整个AFURLConnectionOperation
类都干掉了。上面的方法不能再用,而且也没有给出替代品。所以,咱们只能本身动手了。git
实现这个功能,有几个要点:github
同时知足以上要点,主要有3种思路:GCD、NSOperation 以及 promise。这个需求也是示例多线程用法的一个很好的例子,因此我写了这篇比较详细的文章供你们参考。数组
下面的代码以图片上传为例。测试数据配置了 3 张图片,其中第 2 张图片尺寸最小,每每先上传完毕,用来测试请求发出顺序与返回顺序不一致的状况。 promise
咱们知道,GCD dispatch 是多线程处理最简单的方法。所有请求完成后再通知回调的需求,很适合利用 dispatch group 来完成。至于保证返回结果的顺序,咱们只好本身来作了。安全
首先须要一个方法,对于每张图片生成一个上传请求。网络
- (NSURLSessionUploadTask*)uploadTaskWithImage:(UIImage*)image completion:(void (^)(NSURLResponse *response, id responseObject, NSError *error))completionBlock { // 构造 NSURLRequest NSError* error = NULL; NSMutableURLRequest *request = [[AFHTTPRequestSerializer serializer] multipartFormRequestWithMethod:@"POST" URLString:[self uploadUrl] parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> formData) { NSData* imageData = UIImageJPEGRepresentation(image, 1.0); [formData appendPartWithFileData:imageData name:@"file" fileName:@"someFileName" mimeType:@"multipart/form-data"]; } error:&error]; // 可在此处配置验证信息 // 将 NSURLRequest 与 completionBlock 包装为 NSURLSessionUploadTask AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]]; NSURLSessionUploadTask *uploadTask = [manager uploadTaskWithStreamedRequest:request progress:^(NSProgress * _Nonnull uploadProgress) { } completionHandler:completionBlock]; return uploadTask; }
在这个方法里,咱们首先用UIImageJPEGRepresentation
把UIImage
变为NSData
。而后用AFHTTPRequestSerializer
来生成NSMutableURLRequest
,[self uploadUrl]
是上传接口的地址。为安全考虑,通常上传的接口都有身份验证的需求,好比在请求 header 中加入 auth 信息,能够在此配置NSMutableURLRequest
的 header。最后,咱们用 AFURLSessionManager
把 NSURLRequest
和回调 block 包装成一个NSURLSessionUploadTask
。多线程
有了生成请求的方法,批量发出请求的方法以下:并发
- (IBAction)runDispatchTest:(id)sender { // 须要上传的数据 NSArray* images = [self images]; // 准备保存结果的数组,元素个数与上传的图片个数相同,先用 NSNull 占位 NSMutableArray* result = [NSMutableArray array]; for (UIImage* image in images) { [result addObject:[NSNull null]]; } dispatch_group_t group = dispatch_group_create(); for (NSInteger i = 0; i < images.count; i++) { dispatch_group_enter(group); NSURLSessionUploadTask* uploadTask = [self uploadTaskWithImage:images[i] completion:^(NSURLResponse *response, NSDictionary* responseObject, NSError *error) { if (error) { NSLog(@"第 %d 张图片上传失败: %@", (int)i + 1, error); dispatch_group_leave(group); } else { NSLog(@"第 %d 张图片上传成功: %@", (int)i + 1, responseObject); @synchronized (result) { // NSMutableArray 是线程不安全的,因此加个同步锁 result[i] = responseObject; } dispatch_group_leave(group); } }]; [uploadTask resume]; } dispatch_group_notify(group, dispatch_get_main_queue(), ^{ NSLog(@"上传完成!"); for (id response in result) { NSLog(@"%@", response); } }); }
能够看到,咱们把全部请求放在一个 dispatch_group 里。首先用dispatch_group_create()
来建立这个 group。而后,对于每个 uploadTask,在建立以前先执行dispatch_group_enter(group)
,在结束回调的 block里执行dispatch_group_leave(group)
。结束回调的代码放在dispatch_group_notify
里便可。
实际执行中,首先是全部 task 都进入 group,同时开始上传;上传完成以后依次离开 group;最后 group 空了会自动调用传入group_notify
的回调,整个过程完成。
那么如何把回调数据排成正确的顺序呢?借助 block 会保存自动变量的特色,咱们让每一个 task 的回调 block 都自动带上标志请求次序的变量 i,只需把返回结果填入数组的第 i 位便可。因此在开始请求以前,先建立好保存返回结果的数组,元素个数与请求个数相等,每一个位置上用[NSNull null]
占位。每一个请求返回以后,把本身那个位置上的[NSNull null]
替换成返回结果。所有请求返回以后,数组里保存的天然是按请求顺序排列的回调数据。
这里注意,由于 NSMutableArray 是线程不安全的,而每一个请求返回时是在不一样线程操做同一个数组,因此我用@synchronized
把操做数组的代码锁住了,锁的对象就用这个数组便可。这样保证全部线程执行到这一句都得串行,避免线程安全问题。
一次测试结果以下:
2016-05-13 15:49:43.042 HAMAFNetworkingBatchRequestDemo[23102:5717076] 第 2 张图片上传成功: { imageBucket = test; imageKey = "331eb245-741f-4fdc-8769-fdfb9e646da7"; imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/331eb245-741f-4fdc-8769-fdfb9e646da7?imageMogr2/thumbnail/640x"; } 2016-05-13 15:49:43.098 HAMAFNetworkingBatchRequestDemo[23102:5717076] 第 1 张图片上传成功: { imageBucket = test; imageKey = "d08f5370-c8b6-4912-b4e5-c73ea3134637"; imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/d08f5370-c8b6-4912-b4e5-c73ea3134637?imageMogr2/thumbnail/640x"; } 2016-05-13 15:49:43.120 HAMAFNetworkingBatchRequestDemo[23102:5717076] 第 3 张图片上传成功: { imageBucket = test; imageKey = "bdf13097-8128-4f04-bcbc-462bd2a728ab"; imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/bdf13097-8128-4f04-bcbc-462bd2a728ab?imageMogr2/thumbnail/640x"; } 2016-05-13 15:49:43.120 HAMAFNetworkingBatchRequestDemo[23102:5717076] 上传完成! 2016-05-13 15:49:43.121 HAMAFNetworkingBatchRequestDemo[23102:5717076] { imageBucket = test; imageKey = "d08f5370-c8b6-4912-b4e5-c73ea3134637"; imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/d08f5370-c8b6-4912-b4e5-c73ea3134637?imageMogr2/thumbnail/640x"; } 2016-05-13 15:49:43.121 HAMAFNetworkingBatchRequestDemo[23102:5717076] { imageBucket = test; imageKey = "331eb245-741f-4fdc-8769-fdfb9e646da7"; imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/331eb245-741f-4fdc-8769-fdfb9e646da7?imageMogr2/thumbnail/640x"; } 2016-05-13 15:49:43.124 HAMAFNetworkingBatchRequestDemo[23102:5717076] { imageBucket = test; imageKey = "bdf13097-8128-4f04-bcbc-462bd2a728ab"; imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/bdf13097-8128-4f04-bcbc-462bd2a728ab?imageMogr2/thumbnail/640x"; }
能够看到,尽管第 2 张图片尺寸最小、最早传完,第 1 张图片后传完,但最后的结果顺序仍是正确的。
注意:这个方法有点问题,出在用 KVO 监听 task 的 state 的部分:看 AFN 源码能够看到它是在 task 的 didComplete 的 delegate 方法里执行 completionHandler 的,此时 task 的 state 已经变成 Completed。因此 KVO 有可能会略先一点执行,此时最后一个请求的 success block 可能还没执行。加一点点延时应该能解决问题……但这样不太严谨。我再想一想有没有更合适的监听的东西。很是感谢王银博的 demo,帮我发现这个问题~
能用 dispatch 实现的功能,天然也能够用NSOperationQueue
。NSOperation 这一套比 dispatch 写起来要麻烦一些,不过有几个优势:
NSOperation
是对象,不像 dispatch 是 c 函数。这就意味着你能够继承它,能够给它加 category,在执行过程当中也能够始终管理它,访问到它,查看它的状态等,不像 dispatch 是一撒手就够不着了。NSOperation
执行的任务,执行过程当中能够随时取消。dispatch 一经发出是没法取消的。NSOperationQueue
能够限制最大并发数。假如队列里真有 100 个文件要传,开出 100 个线程反而会严重影响性能。NSOperationQueue
能够很方便地设置maxConcurrentOperationCount
。dispatch 也能够限制最大并发数(参考苹果的文档)不过写起来麻烦不少。就咱们的需求而言,用 NSOperation 有一个很方便的特色:dispatch 里的任务各自为政,而NSOperation
以前是能够有依赖关系的。咱们就能够利用这一点,来发起全部任务上传完成后的回调:把这个完成回调也作成一个NSOperation
,让这个NSOperation
前置依赖全部上传的NSOperation
,这样等到全部上传的NSOperation
完成以后,这个回调NSOperation
才会开始执行。
然而,用NSOperation
也有一个很不方便的特色:NSOperationQueue
是用 KVO 观察NSOperation
状态来判断任务是否已结束的。而咱们请求用的NSURLSessionTask
,它长得很像一个NSOperation
,但却并非NSOperation
的子类。因此,这一套方法最麻烦的地方就在于咱们须要写一个自定义的NSOperation
子类,只是为了跟踪NSURLSessionTask
的状态。
自定义的NSOperation
代码以下:
HAMURLSessionWrapperOperation.h #import <Foundation/Foundation.h> @interface HAMURLSessionWrapperOperation : NSOperation + (instancetype)operationWithURLSessionTask:(NSURLSessionTask*)task; @end
#import "HAMURLSessionWrapperOperation.h" @interface HAMURLSessionWrapperOperation () { BOOL executing; // 系统的 finished 是只读的,不能修改,因此只能重写一个。 BOOL finished; } @property (nonatomic, strong) NSURLSessionTask* task; @property (nonatomic, assign) BOOL isObserving; @end @implementation HAMURLSessionWrapperOperation #pragma mark - Observe Task + (instancetype)operationWithURLSessionTask:(NSURLSessionTask*)task { HAMURLSessionWrapperOperation* operation = [HAMURLSessionWrapperOperation new]; operation.task = task; return operation; } - (void)dealloc { [self stopObservingTask]; } - (void)startObservingTask { @synchronized (self) { if (_isObserving) { return; } [_task addObserver:self forKeyPath:@"state" options:NSKeyValueObservingOptionNew context:nil]; _isObserving = YES; } } - (void)stopObservingTask { // 由于要在 dealloc 调,因此用下划线不用点语法 @synchronized (self) { if (!_isObserving) { return; } _isObserving = NO; [_task removeObserver:self forKeyPath:@"state"]; } } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context { if (self.task.state == NSURLSessionTaskStateCanceling || self.task.state == NSURLSessionTaskStateCompleted) { [self stopObservingTask]; [self completeOperation]; } } #pragma mark - NSOperation methods - (void)start { // Always check for cancellation before launching the task. if ([self isCancelled]) { // Must move the operation to the finished state if it is canceled. [self willChangeValueForKey:@"isFinished"]; finished = YES; [self didChangeValueForKey:@"isFinished"]; return; } // If the operation is not canceled, begin executing the task. [self willChangeValueForKey:@"isExecuting"]; [NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil]; executing = YES; [self didChangeValueForKey:@"isExecuting"]; } - (void)main { @try { [self startObservingTask]; [self.task resume]; } @catch (NSException * e) { NSLog(@"Exception %@", e); } } - (void)completeOperation { [self willChangeValueForKey:@"isFinished"]; [self willChangeValueForKey:@"isExecuting"]; executing = NO; finished = YES; [self didChangeValueForKey:@"isExecuting"]; [self didChangeValueForKey:@"isFinished"]; } - (BOOL)isAsynchronous { return YES; } - (BOOL)isExecuting { return executing; } - (BOOL)isFinished { return finished; } @end
代码有点长,但没办法。咱们的目标是对每一个NSURLSessionTask
都包装出一个HAMURLSessionWrapperOperation
,这个NSOperation
彻底随着NSURLSessionTask
的状态而动,在 Task 结束以后发出 KVO 的通知,通知NSOperationQueue
这个任务结束。
系统NSOperation
的finished
属性是只读的,不能修改;为了记录值和发出 KVO 的通知,咱们只能在旁再定义一个finished
的成员变量,经过重写- (BOOL)isFinished
等 getter 方法,盖掉原来的finished
属性。如今几乎全用 property,这种成员变量的写法很久没看见过了,没想到还有这种用处,这种特殊的写法仍是从苹果文档学来的(参考这里)。
这里 start 方法照抄苹果文档,在新线程调起 main 方法。main 方法里就两件事:开始 KVO 观察上传 task 的 state 属性,而后启动 task。一旦 task 完成(或失败),接到 KVO 的通知,咱们中止对 task 的观察,而后发出本身的 KVO 通知去通知NSOperationQueue
。这里咱们手动调起了[self willChangeValueForKey:@"isFinished"];
和[self didChangeValueForKey:@"isFinished"];
,又重写了- (BOOL)isFinished
方法,就把只读的finished
属性偷天换日变成咱们本身定义的finished
成员变量了。
自定义NSOperation
说完了,下面咱们来看看怎么使用这个类。咱们一样要利用上面 dispatch 一节写的那个uploadTaskWithImage:completion
方法,根据图片生成请求。发出请求的代码以下:
- (IBAction)runNSOperationTest:(id)sender { // 须要上传的数据 NSArray* images = [self images]; // 准备保存结果的数组,元素个数与上传的图片个数相同,先用 NSNull 占位 NSMutableArray* result = [NSMutableArray array]; for (UIImage* image in images) { [result addObject:[NSNull null]]; } NSOperationQueue *queue = [[NSOperationQueue alloc] init]; queue.maxConcurrentOperationCount = 5; NSBlockOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{ [[NSOperationQueue mainQueue] addOperationWithBlock:^{ // 回到主线程执行,方便更新 UI 等 NSLog(@"上传完成!"); for (id response in result) { NSLog(@"%@", response); } }]; }]; for (NSInteger i = 0; i < images.count; i++) { NSURLSessionUploadTask* uploadTask = [self uploadTaskWithImage:images[i] completion:^(NSURLResponse *response, NSDictionary* responseObject, NSError *error) { if (error) { NSLog(@"第 %d 张图片上传失败: %@", (int)i + 1, error); } else { NSLog(@"第 %d 张图片上传成功: %@", (int)i + 1, responseObject); @synchronized (result) { // NSMutableArray 是线程不安全的,因此加个同步锁 result[i] = responseObject; } } }]; HAMURLSessionWrapperOperation *uploadOperation = [HAMURLSessionWrapperOperation operationWithURLSessionTask:uploadTask]; [completionOperation addDependency:uploadOperation]; [queue addOperation:uploadOperation]; } [queue addOperation:completionOperation]; }
保持结果顺序的方法与 dispatch 相同,都是咱们本身完成的。咱们把maxConcurrentOperationCount
定成 5,避免并发过多竞争资源。先建立结束回调的 operation,再让它依赖后面建立的每个上传 operation。由于通常回调都要涉及到更新 UI,因此让它回到主线程执行。后面根据每张图片逐一建立 task、包装成 operation。建立好以后,加进 operationQueue 里就开始跑了。
一次测试结果以下:
2016-05-13 15:50:06.269 HAMAFNetworkingBatchRequestDemo[23102:5717076] 第 2 张图片上传成功: { imageBucket = test; imageKey = "cc60ab02-7745-4c60-8697-8bae1501768b"; imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/cc60ab02-7745-4c60-8697-8bae1501768b?imageMogr2/thumbnail/640x"; } 2016-05-13 15:50:06.365 HAMAFNetworkingBatchRequestDemo[23102:5717076] 第 1 张图片上传成功: { imageBucket = test; imageKey = "ee9c1492-a8f1-441c-9bd4-c90756841266"; imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/ee9c1492-a8f1-441c-9bd4-c90756841266?imageMogr2/thumbnail/640x"; } 2016-05-13 15:50:06.413 HAMAFNetworkingBatchRequestDemo[23102:5717076] 第 3 张图片上传成功: { imageBucket = test; imageKey = "6fe8197a-4638-4706-afe1-3aca203cf73f"; imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/6fe8197a-4638-4706-afe1-3aca203cf73f?imageMogr2/thumbnail/640x"; } 2016-05-13 15:50:06.414 HAMAFNetworkingBatchRequestDemo[23102:5717076] 上传完成! 2016-05-13 15:50:06.414 HAMAFNetworkingBatchRequestDemo[23102:5717076] { imageBucket = test; imageKey = "ee9c1492-a8f1-441c-9bd4-c90756841266"; imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/ee9c1492-a8f1-441c-9bd4-c90756841266?imageMogr2/thumbnail/640x"; } 2016-05-13 15:50:06.415 HAMAFNetworkingBatchRequestDemo[23102:5717076] { imageBucket = test; imageKey = "cc60ab02-7745-4c60-8697-8bae1501768b"; imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/cc60ab02-7745-4c60-8697-8bae1501768b?imageMogr2/thumbnail/640x"; } 2016-05-13 15:50:06.415 HAMAFNetworkingBatchRequestDemo[23102:5717076] { imageBucket = test; imageKey = "6fe8197a-4638-4706-afe1-3aca203cf73f"; imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/6fe8197a-4638-4706-afe1-3aca203cf73f?imageMogr2/thumbnail/640x"; }
结果也是正确的。
上面的两种方法,咱们都是本身用数组、占位、逐位替换的方法,本身写代码保证返回数据顺序正确的。其实这种须要多个线程执行、所有结束后回调、结果顺序保证正确的需求,通常最适合用 promise 来作。各个语言都有本身的 promise 实现,iOS 也有好几种。这里咱们试用一下 iOS 最著名的实现 PromiseKit。
在 github 上 5000 多个 star,这个 lib 是 Objective-C 、Swift 通用的,两套代码都有。在网络请求方面,它要依赖同一个做者写的另外一个库 OMGHTTPURLRQ,导入的时候小费周折。PromiseKit 这一套方法与 AFNetworking 库就不要紧了,可能有些离题,可是用起来是最为方便的。
这里咱们再也不须要上面那个生成NSURLSessionTask
的方法了,如今咱们须要把NSURLRequest
包装成AnyPromise
:
- (AnyPromise *)uploadPromiseWithImage:(UIImage *)image completion:(id (^)(id))completionBlock { NSString* url = [self uploadUrl]; NSData* imageData = UIImageJPEGRepresentation(image, 1.0); OMGMultipartFormData *multipartFormData = [OMGMultipartFormData new]; [multipartFormData addFile:imageData parameterName:@"file" filename:@"someFileName" contentType:@"multipart/form-data"]; NSMutableURLRequest* request = [OMGHTTPURLRQ POST:url :multipartFormData error:nil]; // 可在此处配置验证信息 if (completionBlock) { return [NSURLConnection promise:request].then(completionBlock); } else { return [NSURLConnection promise:request]; } }
这里能够看到 promise 的.then
语法。它是一个 C 函数,传进的参数是这项 promise 完成以后下一步须要执行的 block,返回值仍然是AnyPromise
,因此能够一直.then().then()……
这样链式调用下去。咱们在这里让它上传完单张图片以后执行单张图片的回调,把回调 block『附身』在上传的 promise 以后。
上面就是建立 promise 的过程。那么执行 promise 的代码怎么写呢?
- (IBAction)runPromiseTest:(id)sender { // 须要上传的数据 NSArray* images = [self images]; NSMutableArray* promises = [NSMutableArray array]; for (NSInteger i = 0; i < images.count; i++) { UIImage* image = images[i]; [promises addObject:[self uploadPromiseWithImage:image completion:^(id resultImageUrl){ NSLog(@"第 %d 张图片上传成功: %@", (int)i + 1, resultImageUrl); return resultImageUrl; }]]; } PMKWhen(promises).then(^(NSArray *results) { NSLog(@"上传完成!"); NSLog(@"%@", results); }).catch(^{ NSLog(@"图片上传失败"); }); }
能够看到代码很是简洁,可读性又好,比前两种方法都省去很多代码,这是 promise 的一大优点。咱们只需把针对每张图片建立一个 promise ,放进一个 promises 数组,而后PMKWhen(promises).then()
就能帮咱们搞定一切了——是否是很神奇呢?每一个任务单开线程、等待所有任务执行完、结果正确排序等诸多工序,全都由这一行代码搞定了。看看测试结果:
2016-05-13 15:30:45.447 HAMAFNetworkingBatchRequestDemo[23093:5713564] 第 2 张图片上传成功: { imageBucket = test; imageKey = "5d50cdd3-2272-4d3b-bbb1-054d1d08e682"; imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/5d50cdd3-2272-4d3b-bbb1-054d1d08e682?imageMogr2/thumbnail/640x"; } 2016-05-13 15:30:45.595 HAMAFNetworkingBatchRequestDemo[23093:5713564] 第 1 张图片上传成功: { imageBucket = test; imageKey = "ff3874d2-8477-4ceb-a49f-1938168b0456"; imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/ff3874d2-8477-4ceb-a49f-1938168b0456?imageMogr2/thumbnail/640x"; } 2016-05-13 15:30:46.127 HAMAFNetworkingBatchRequestDemo[23093:5713564] 第 3 张图片上传成功: { imageBucket = test; imageKey = "2b8b0175-1274-4de9-b809-7d88809ef606"; imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/2b8b0175-1274-4de9-b809-7d88809ef606?imageMogr2/thumbnail/640x"; } 2016-05-13 15:30:46.130 HAMAFNetworkingBatchRequestDemo[23093:5713564] 上传完成! 2016-05-13 15:30:46.130 HAMAFNetworkingBatchRequestDemo[23093:5713564] ( { imageBucket = test; imageKey = "ff3874d2-8477-4ceb-a49f-1938168b0456"; imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/ff3874d2-8477-4ceb-a49f-1938168b0456?imageMogr2/thumbnail/640x"; }, { imageBucket = test; imageKey = "5d50cdd3-2272-4d3b-bbb1-054d1d08e682"; imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/5d50cdd3-2272-4d3b-bbb1-054d1d08e682?imageMogr2/thumbnail/640x"; }, { imageBucket = test; imageKey = "2b8b0175-1274-4de9-b809-7d88809ef606"; imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/2b8b0175-1274-4de9-b809-7d88809ef606?imageMogr2/thumbnail/640x"; } )
一样是正确的。
因此看起来用 promise 仍是很是方便的。不过这是我第一次尝试用它,还不知道在工程中实际应用会有什么缺点。