WANetworkRouting 1.0.1

WANetworkRouting 1.0.1

测试测试
语言语言 Obj-CObjective C
许可证 MIT
发布上次发布2017年8月

Marian Paul 维护。



 
依赖项
AFNetworking~> 3.1
WAMapping~> 0.0.8
 

  • 作者
  • Marian Paul

ipodishima 开发和维护,他在 Wasappli Inc 担任创始人和 CTO。

Wisembly 赞助

一个用于从 API 获取对象并将其映射到您的应用程序的路由库

  • [x] 高度可定制:网络请求、身份验证和映射层是分开的,您可以创建自己的。
  • [x] 默认网络请求层基于 AFNetworking 3.0 构建
  • [x] 默认映射层基于 WAMapping 构建
  • [x] 带内对象路由器
  • [x] 带内批量管理器
  • [x] 提供不同配置
  • [x] 支持 NSProgress
  • [x] 已在真实项目中测试和使用

访问 wiki 了解有关 WANetworkRouting 高级用法的更多信息。

WANetworkRouting 是一个库,它将简单的配置步骤将 GET enterprises/:itemID 转换为从 API 获取的 Enterprise : NSManagedObject

安装和使用

WANetworkRoutingManager

WANetworkRoutingManagerWANetworkRouting 的核心组件。它会为您处理所有请求。

初始化应包含至少一个请求管理器。 WANetworkRouting 有一个内置管理器 WAAFNetworkingRequestManager,它基于 AFNetworking 3.0 构建。

// Create a request manager
WAAFNetworkingRequestManager *requestManager = [WAAFNetworkingRequestManager new];

// Create the network routing manager 
WANetworkRoutingManager *routingManager = [WANetworkRoutingManager managerWithBaseURL:[NSURL URLWithString:@"http://baseURL.com"]
                                                                       requestManager:requestManager
                                                                       mappingManager:nil
                                                                authenticationManager:nil];

** 注意 ** 此 routingManager 将没有映射层或身份验证层。此示例演示在配置映射之前测试 API 是多么简单。

您可以使用它执行任何操作,例如 GET|POST|PUT|PATCH|DELETE|HEAD。例如

  • 获取所有企业
[routingManager getObjectsAtPath:@"enterprises"
                      parameters:nil
                         success:^(WAObjectRequest *objectRequest, WAObjectResponse *response, NSArray *mappedObjects) {
                             NSDictionary *json = response.responseObject;
                             // Do something with the JSON
                         }
                         failure:^(WAObjectRequest *objectRequest, WAObjectResponse *response, id<WANRErrorProtocol> error) {
                         }];
  • 更新企业
[routingManager putObject:nil
                     path:@"enterprises/1"
               parameters:@{@"key": @"newValue"}
                  success:^(WAObjectRequest *objectRequest, WAObjectResponse *response, NSArray *mappedObjects) {
                  }
                  failure:^(WAObjectRequest *objectRequest, WAObjectResponse *response, id<WANRErrorProtocol> error) {
                  }];

添加映射层

从这里开始变得有趣了

想法是添加一个映射层,以便您的 json 可以映射为对象。这是通过使用默认的映射服务完成的,该映射服务基于 WAMapping。我强烈推荐您阅读有关如何配置映射的文档。

有 3 个步骤

  • 配置映射,
  • 配置响应描述符,
  • 可选:配置请求描述符。

步骤 1:配置映射

有关映射的详细信息,请参阅 WAMapping

// Specify a store to use between WAMemoryStore, WANSCodingStore, WACoreDataStore, your own store.
WAMemoryStore *memoryStore = [[WAMemoryStore alloc] init];
           
// Create the mapping description for `Enterprise`
WAEntityMapping *enterpriseMapping = [WAEntityMapping mappingForEntityName:@"Enterprise"];
enterpriseMapping.identificationAttribute = @"itemID";
[enterpriseMapping addAttributeMappingsFromDictionary:@{
                                                        @"id": @"itemID",
                                                        @"name": @"name",
                                                        @"address.street_number": @"streetNumber"}];
// Create the mapping manager
WAMappingManager *mappingManager = [WAMappingManager mappingManagerWithStore:memoryStore];
                                                                
WAAFNetworkingRequestManager *requestManager = [WAAFNetworkingRequestManager new];

// Create the network routing manager 
WANetworkRoutingManager *routingManager = [WANetworkRoutingManager managerWithBaseURL:[NSURL URLWithString:@"http://baseURL.com"]
                                                                       requestManager:requestManager
                                                                       mappingManager:mappingManager
                                                                authenticationManager:nil];
                                                                
// Add a default date formatter on mapper
id(^toDateMappingBlock)(id ) = ^id(id value) {
    if ([value isKindOfClass:[NSString class]]) {
        return [dateFormatter dateFromString:value];
    }

    return value;
};

[mappingManager addDefaultMappingBlock:toDateMappingBlock
                   forDestinationClass:[NSDate class]];

步骤 2: 配置响应描述符

这将为路径映射一个关系。例如,当获取 /enterprises 时,你会得到一个 JSON,如下

{
	"array_of_enterprises": [{
		"name": "Enterprise 1"
	}, {
		"name": "Enterprise 2"
	}]
}

请求描述符将如下所示

WAResponseDescriptor *enterprisesResponseDescriptor =
[WAResponseDescriptor responseDescriptorWithMapping:enterpriseMapping
                                             method:WAObjectRequestMethodGET
                                        pathPattern:@"enterprises" // The path on the URL
                                            keyPath:@"array_of_enterprises"]; // The key path to access the enterprises on JSON

对于企业的 GETPUT 请求,将返回如下

{
	"name": "Enterprise 1"
}
WAResponseDescriptor *singleEnterpriseResponseDescriptor =
[WAResponseDescriptor responseDescriptorWithMapping:enterpriseMapping
                                             method:WAObjectRequestMethodGET | WAObjectRequestMethodPUT // Specify multiple methods
                                        pathPattern:@"enterprises/:itemID" // The path
                                            keyPath:nil]; // The response would directly returns the object

请注意语法 enterprises/:itemID:`:` 表示该值将会动态替换,已知 itemID 是客户端上的属性名。

最后,将响应描述符添加到映射管理器

[mappingManager addResponseDescriptor:enterprisesResponseDescriptor];
[mappingManager addResponseDescriptor:singleEnterpriseResponseDescriptor];

你现在可以用 WANetworkRouting 像这样使用

[routingManager getObjectsAtPath:@"enterprises"
                      parameters:nil
                         success:^(WAObjectRequest *objectRequest, WAObjectResponse *response, NSArray *mappedObjects) {
                             NSArray<Enterprise *> *enterprises = mappedObjects;
                             // Do something with the enterprises
                         }
                         failure:^(WAObjectRequest *objectRequest, WAObjectResponse *response, id<WANRErrorProtocol> error) {
                         }];

步骤 3: 配置请求描述符

这一步是可选的,但它允许你将一个对象反向映射以发送到服务器。例如,不必自己创建一个用于 POST 的参数字典,你可以编写

WARequestDescriptor *enterpriseRequestDescriptor =
[WARequestDescriptor requestDescriptorWithMethod:WAObjectRequestMethodPOST
                                     pathPattern:@"enterprises" // The path on the URL
                                         mapping:enterpriseMapping
                                  shouldMapBlock:nil // On optional block which let you configure rather you want to reverse map a relation ship or not
                                  requestKeyPath:nil]; // The key path for the final dictionary
                                  
[mappingManager addRequestDescriptor:enterpriseRequestDescriptor];

请注意,该 requestKeyPath 需要与你对象包装在参数中的方式相匹配。如果为空,则字典是对象本身的字典。

{
	"requestKeyPathValue": {
		"name": "Enterprise 1"
	}
}

这样,你就可以发布如下对象

Enterprise *enterprise = [Enterprise new];
enterprise.name = @"Test";
        
[routingManager postObject:enterprise
                      path:@"enterprises"
                parameters:nil
                   success:^(WAObjectRequest *objectRequest, WAObjectResponse *response, NSArray *mappedObjects) {
                   }
                   failure:^(WAObjectRequest *objectRequest, WAObjectResponse *response, id<WANRErrorProtocol> error) {
                   }];

这将反向映射 enterprise 到一个字典,并以参数形式在 POST enterprise 上发送。所有的操作都神奇地完成了!

注意:如果 enterpriseidentificationAttribute 没有值(请参阅映射),则企业对象将从存储中自动删除,因为服务器返回的对象将是一个带有 identificationAttribute 的新对象。换句话说:你不必处理潜在的重复。

添加路由层

我们刚刚看到这个调用

[routingManager postObject:enterprise
                      path:@"enterprises"
                parameters:nil
                   success:^(WAObjectRequest *objectRequest, WAObjectResponse *response, NSArray *mappedObjects) {
                   }
                   failure:^(WAObjectRequest *objectRequest, WAObjectResponse *response, id<WANRErrorProtocol> error) {
                   }];

难道不是写这个会更好吗?

[routingManager postObject:enterprise
                      path:nil // No value for path
                parameters:nil
                   success:^(WAObjectRequest *objectRequest, WAObjectResponse *response, NSArray *mappedObjects) {
                   }
                   failure:^(WAObjectRequest *objectRequest, WAObjectResponse *response, id<WANRErrorProtocol> error) {
                   }];

这就是路由器!

WANetworkRoute *postEnterpriseRoute =
[WANetworkRoute routeWithObjectClass:[Enterprise class]
                         pathPattern:@"enterprises"
                              method:WAObjectRequestMethodPOST];

[routingManager.router postEnterpriseRoute];

每次你尝试使用 POST 一个 Enterprise 类的对象时,它都会为你提供 enterprises 路径。

这里有一个使用 GETPUT 路由的另一个示例

WANetworkRoute *enterpriseRoute =
[WANetworkRoute routeWithObjectClass:[Enterprise class]
                         pathPattern:@"enterprises/:itemID"
                              method:WAObjectRequestMethodGET | WAObjectRequestMethodPUT];

[routingManager.router addRoute:enterpriseRoute];

添加认证层

如果你的 API 有一些认证,这个层就很重要。基本来说,你应该有一个登录/注册端点,该端点返回某种可以刷新(如果请求得到一个错误如 401: token过期)的令牌。通过实现简单的 WARequestAuthenticationManagerProtocol 协议,并将你的类的一个实例传递给路由管理器,它会

  • 请求对 NSMutableURLRequest 进行认证。应该在 Authorization HTTP 头字段中有一个令牌。
  • 询问是否需要重新播放请求:例如收到了 401: token过期,那么应该回答是。
  • 要求你进行认证(以某种方式更新授权)并将请求重新发送([routingManager enqueueRequest:])(该请求将自动为你重新授权)。

这样,路由管理器就可以在没有惊喜的情况下运行每个请求,即使认证已过期!

添加批量操作

批量管理器回答了两个需求

1/ 假设你希望在同时执行多个调用时最小化对服务器的影响,比如 GET /meGET /meetingsGET /configuration 2/ 你想要添加离线支持,例如,添加会议笔记,并在你回到在线状态时自动同步它们!

那么,WABatchManager 就是为你准备的

步骤 1: 创建批量 API

该库内置了以下规范的批量管理器。您可以选择创建一个符合规范的API,或者从WABatchManager新建一个适合您需求的批量管理器。

规范: (此规范紧随Facebook批量API请求 Facebook批量API

  • 设置每会话限制
  • 发送的数据具有以下格式
{
	"batch": [{
		"uri": "\/meetings\/Ufd8f4e\/notes",
		"method": "POST",
		"body": "{\"note\":{\"hash\":\"817ebd76b6a89eff47bbd8e53f633c93eeadf23e\",\"title\":\"New first note\",\"type\":\"item\",\"position\":0}}",
		"headers": {
          "Content-Type": "application/json"
        }
	}, {
		"uri": "\/meetings\/Ufd8f4e\/notes",
		"method": "POST",
		"body": "{\"note\":{\"hash\":\"f4e893ab9a754c273707b264359e7ec12262959d\",\"title\":\"New second note\",\"type\":\"note\",\"position\":1}}"
	}]
}

请注意,uri相对基本URL,而body是编码的字符串

  • 响应:
[
  {
    "body": "[{\"note\":\"...\"}]
    "code": 200
  },
  {
    "body": "[{\"note\":\"...\"}]
    "code": 200
  }
 ]

步骤2:创建批量管理器

WABatchManager *batchManager = [[WABatchManager alloc] initWithBatchPath:@"/batch" limit:20];

routingManager = [WANetworkRoutingManager managerWithBaseURL:[NSURL URLWithString:kBaseURL]
                                              requestManager:requestManager
                                              mappingManager:mappingManager
                                       authenticationManager:authManager
                                                batchManager:batchManager];

手动批量请求数据

// Create a batch session
WABatchSession *batchSession = [WABatchSession new];

// Enqueue the requests you want to perform
[batchSession addRequest:[WAObjectRequest requestWithHTTPMethod:WAObjectRequestMethodGET
                                                           path:@"/me"
                                                     parameters:nil
                                                optionalHeaders:nil]];
[batchSession addRequest:[WAObjectRequest requestWithHTTPMethod:WAObjectRequestMethodGET
                                                           path:@"/meetings"
                                                     parameters:nil
                                                optionalHeaders:nil]];
[batchSession addRequest:[WAObjectRequest requestWithHTTPMethod:WAObjectRequestMethodGET
                                                           path:@"/configuration"
                                                     parameters:nil
                                                optionalHeaders:nil]];
                                                
// Send the session
[batchManager sendBatchSession:batchSession
                  successBlock:^(id<WABatchManagerProtocol> batchManager, NSArray<WABatchResponse *> *batchResponses) {
                      
                  }
                  failureBlock:^(id<WABatchManagerProtocol> batchManager, WAObjectRequest *request, WAObjectResponse *response, id<WANRErrorProtocol> error) {
                      
                  }];

添加离线模式

离线模式出盒即用。基本上,您注册描述请求的路径,当出现网络问题(请勿包含GET)时,可以使用队列排队,然后让库做它的工作!它还无需任何进一步的操作即可与映射和身份验证集成。

  • 创建路径以描述要排队的请求
// Meetings
WANetworkRoute *modifyMeeting = [WANetworkRoute routeWithObjectClass:nil
                                                         pathPattern:@"meetings/:itemID"
                                                              method:WAObjectRequestMethodPUT | WAObjectRequestMethodeDELETE];

// Action items
WANetworkRoute *postActionItem = [WANetworkRoute routeWithObjectClass:nil
                                                          pathPattern:@"meetings/:itemID/notes"
                                                               method:WAObjectRequestMethodPOST];
  • 将其添加到批量管理器中
[batchManager addRouteToBatchIfOffline:modifyMeeting];
[batchManager addRouteToBatchIfOffline:postActionItem];
  • 错误响应
[self.apiManager putObject:meeting
                      path:nil
                parameters:nil
                   success:...
                   failure:^(WAObjectRequest *objectRequest, WAObjectResponse *response, id<WANRErrorProtocol> error) {
                       SLDMeeting *meetingReturned = nil;
                       // Check if the error is a batch one
                       if ([[error.originalError.domain isEqualToString:WANetworkRoutingManagerErrorDomain] && error.originalError.code == WANetworkRoutingManagerErrorRequestBatched) {
                           // Return the meeting for any UI update
                           meetingReturned = meeting;
                           // Store a "has offline changes" flag
                           meeting.hasOfflineModifications = @YES;
                           // Save the store
                           [store save];
                       }
                       
                       if (completion) {
                           completion(meetingReturned, error);
                       }
                   }];
  • 等待刷新。库会自动在在线时刷新离线更改。您还可以通过以下方式手动触发刷新
if ([batchManager needsFlushing]) {
  [batchManager flushDataWithCompletion:completion];
}
  • 刷新后的操作
[batchManager setOfflineFlushSuccessBlock:^(id <WABatchManagerProtocol> batchManager, NSArray<WABatchResponse *> *batchResponses) {
    if (![batchManager needsFlushing]) {
        // Consider all meetings synchronized. You could also iterate trough batch responses
        NSArray *notSyncedMeetings = [SLDMeeting findAll];
        for (SLDMeeting *meeting in notSyncedMeetings) {
            meeting.hasOfflineModifications = @NO;
        }
        
        [store save];
    }
}];

进度

您可以跟踪请求的进度。该库使用了NSProgress类,这是一个处理进度的绝佳工具。如果您的应用支持iOS 9+,则具有显式的子级功能。您然后可以编写

(请注意,我们正在使用NSProgress的子类,您可以在样本中找到。这是因为NSProgress不允许您检查进度是否有子进程 :/)

WAProgress *mainProgress = [[WAProgress alloc] initWithParent:nil userInfo:nil];
mainProgress.totalUnitCount = 100; // 100%

// Add an observer to track the fraction completed
[mainProgress addObserver:self forKeyPath:NSStringFromSelector(@selector(fractionCompleted)) options:NSKeyValueObservingOptionNew context:nil];

                                     }];

[routingManager getObjectsAtPath:@"posts"
                      parameters:nil
                        progress:^(WAObjectRequest *objectRequest, NSProgress *uploadProgress, NSProgress *downloadProgress, NSProgress *mappingProgress) {
                            // Add the progress as a child
                            [mainProgress addChildOnce:downloadProgress withPendingUnitCount:80]; // Network counts as 80% of the time
                            [mainProgress addChildOnce:mappingProgress withPendingUnitCount:20]; // Mapping counts as 20% of the time. This is arbitrary
                        }
                         success:^(WAObjectRequest *objectRequest, WAObjectResponse *response, NSArray *mappedObjects) {
                             self.posts = mappedObjects;
                             [self.tableView reloadData];
                             
                             // Remove the observer!
                             [mainProgress removeObserver:self forKeyPath:NSStringFromSelector(@selector(fractionCompleted))];
                         }
                         failure:^(WAObjectRequest *objectRequest, WAObjectResponse *response, id<WANRErrorProtocol> error) {
                             [mainProgress removeObserver:self forKeyPath:NSStringFromSelector(@selector(fractionCompleted))];
                         }];

...

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
    if ([object isKindOfClass:[NSProgress class]]) {
        NSLog(@"New progress is %f", [change[NSKeyValueChangeNewKey] floatValue]);
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

如果您应用的iOS目标版本小于9,建议您自行计算进度,这是同步的,很简单。

取消

由于NSProgress,您可以轻松取消请求:您需要调用[downloadProgress cancel][mappingProgress cancel],这将取消映射或服务器获取任务,具体取决于您处于过程中的哪个位置。

错误

每个API都有一种方式来描述除HTTP代码(404、403、401等)之外的错误详细信息。例如,您可能得到这样的响应:

{
	"error": {
		"error_code": 4,
		"error_description": "The token is expired"
	}
}

WANetworkRouting 允许您自定义错误处理并检索代码和错误描述。让我们看看它是如何做的:这是一个协议(再次强调):WANRErrorProtocol

创建您的自定义类

路由管理器提供了一个默认的错误类,除了占用内存之外,什么都不做。

@interface MyAPIError : NSObject <WANRErrorProtocol>
@end

@implementation MyAPIError 

- (instancetype)initWithOriginalError:(NSError *)error response:(WAObjectResponse *)response {
    self = [super init];
    
    if (self) {
        self->_originalError = error;
        self->_response      = response;
        self->_finalError    = error;
        
        NSDictionary *errorDescription = response.responseObject[@"error"];
        NSInteger errorCode = [errorDescription[@"error_code"] integerValue];
        NSString *errorDesc = errorDescription[@"error_description"];
        
        self->_finalError = [NSError errorWithDomain:MyDomain
                                                code:errorCode
                                            userInfo:@{
                                                       NSLocalizedDescriptionKey: errorDesc
                                                       }];
    }
    
    return self;
}

@end

注册类

WAAFNetworkingRequestManager *requestManager = [WAAFNetworkingRequestManager new];
requestManager.errorClass = [MyAPIError class];

使用类

[routingManager postObject:enterprise
                      path:nil
                parameters:nil
                   success:^(WAObjectRequest *objectRequest, WAObjectResponse *response, NSArray *mappedObjects) {
                   }
                   failure:^(WAObjectRequest *objectRequest, WAObjectResponse *response, MyAPIError *error) {
                   		if (error.finalError.errorCode == 4) {
                   			// Token has expired :/
                   		}
                   }];

删除资源

关于删除的一点说明:

如果您这样写:

[routingManager deleteObject:enterprise
                        path:nil
                  parameters:nil
                     success:^(WAObjectRequest *objectRequest, WAObjectResponse *response, NSArray *mappedObjects) {
                   }
                     failure:^(WAObjectRequest *objectRequest, WAObjectResponse *response, MyAPIError *error) {
                   }];

在成功的情况下,这将自动从商店删除企业!请注意,如果您仅从资源中删除对象,则必须您负责从您的商店中删除它([routingManager deleteObject:nil path:@"enterprises/1"...])。

从响应中获取HTTP代码和头字段

// WAObjectResponse *response

NSInteger responseStatusCode = response.urlResponse.statusCode;
NSDictionary *httpHeaderFields = response.urlResponse.httpHeaderFields;

启发

让我们说实话,你会发现一些看起来像 Restkit 的东西。我曾在一个非常大的项目中使用了一段时间的RestKit,但它通常涉及到太多的魔法,难以维护,并不符合我们保持简单的需求。例如:在RestKit上升级 AFNetworking 就是我的一个月份期待的事情!

通过分层划分,我希望解决这个问题!

## 贡献:问题、建议、Pull Requests?

如果你遇到特定于 WANetworkRouting 的问题,请在这里创建新问题

鼓励发送新的功能pull requests,并非常感激!请尽量保持与现有代码风格的一致性。如果你正在考虑对项目进行重大更改或添加,请在创建新问题之前询问我,以便有机会合并。请也在运行测试之前;)

## 那就完了!

  • 如果你很满意,请不要犹豫发我一条推文 @ipodishima
  • 在MIT许可下分发。
  • facebook 上关注Wasappli。