测试测试 | ✗ |
语言语言 | Obj-CObjective C |
许可证 | MIT |
发布上次发布 | 2017年8月 |
由 Marian Paul 维护。
依赖项 | |
AFNetworking | ~> 3.1 |
WAMapping | ~> 0.0.8 |
由 ipodishima 开发和维护,他在 Wasappli Inc 担任创始人和 CTO。
由 Wisembly 赞助
一个用于从 API 获取对象并将其映射到您的应用程序的路由库
NSProgress
访问 wiki 了解有关 WANetworkRouting
高级用法的更多信息。
WANetworkRouting
是一个库,它将简单的配置步骤将 GET enterprises/:itemID
转换为从 API 获取的 Enterprise : NSManagedObject
。
WANetworkRoutingManager
WANetworkRoutingManager
是 WANetworkRouting
的核心组件。它会为您处理所有请求。
初始化应包含至少一个请求管理器。 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 个步骤
有关映射的详细信息,请参阅 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]];
这将为路径映射一个关系。例如,当获取 /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
对于企业的 GET
或 PUT
请求,将返回如下
{
"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) {
}];
这一步是可选的,但它允许你将一个对象反向映射以发送到服务器。例如,不必自己创建一个用于 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
上发送。所有的操作都神奇地完成了!
注意:如果 enterprise
的 identificationAttribute
没有值(请参阅映射),则企业对象将从存储中自动删除,因为服务器返回的对象将是一个带有 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
路径。
这里有一个使用 GET
和 PUT
路由的另一个示例
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 /me
、GET /meetings
、GET /configuration
2/ 你想要添加离线支持,例如,添加会议笔记,并在你回到在线状态时自动同步它们!
那么,WABatchManager
就是为你准备的
该库内置了以下规范的批量管理器。您可以选择创建一个符合规范的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
}
]
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"...]
)。
// WAObjectResponse *response
NSInteger responseStatusCode = response.urlResponse.statusCode;
NSDictionary *httpHeaderFields = response.urlResponse.httpHeaderFields;
让我们说实话,你会发现一些看起来像 Restkit 的东西。我曾在一个非常大的项目中使用了一段时间的RestKit,但它通常涉及到太多的魔法,难以维护,并不符合我们保持简单的需求。例如:在RestKit上升级 AFNetworking
就是我的一个月份期待的事情!
通过分层划分,我希望解决这个问题!
## 贡献:问题、建议、Pull Requests?
如果你遇到特定于 WANetworkRouting 的问题,请在这里创建新问题。
鼓励发送新的功能pull requests,并非常感激!请尽量保持与现有代码风格的一致性。如果你正在考虑对项目进行重大更改或添加,请在创建新问题之前询问我,以便有机会合并。请也在运行测试之前;)
## 那就完了!