WebApiClient
WebApiClient 是一个应用框架,旨在以标准化的方式执行 RESTful 网络服务请求。它通过为每个网络服务端点配置命名路由,从而使您的应用程序专注于消费逻辑网络服务 API,而不是 HTTP 实现细节。
项目分为模块,从核心模块开始,然后分支到各种支持模块,这些模块添加了如 对象映射、缓存 和 UI 支持 等功能。
模块:核心
“核心”模块提供了一个基于路由的 HTTP 客户端框架,支持将请求和响应在本地对象和序列化形式之间进行转换,如 JSON。此模块仅提供基于协议的 API 和一些支持 API 的骨架类,但本身不提供实际的全实现,因此可以使用不同需要的 HTTP 后端。`AFNetworking 模块` 提供了 API 的完整实现。
WebApiClient 协议定义了应用程序使用的 HTTP 客户端主入口点。API 故意简单,基于异步块回调。
- (void)requestAPI:(NSString *)name
withPathVariables:(id)pathVariables
parameters:(id)parameters
data:(id<WebApiResource>)data
finished:(void (^)(id<WebApiResponse> response, NSError *error))callback;
此 API 的一个示例调用可能如下所示
// make a GET request to /documents/123
[client requestAPI:@"doc" withPathVariables:@{@"uniqueId" : @123 } parameters:nil data:nil
finished:^(id<WebApiResponse> response, NSError *error) {
if ( !error ) {
MyDocument *doc = response.responseObject;
} else if ( response.statusCode == 422 ) {
// handle 422 (validation) errors here...
}
}];
后台回调支持
默认情况下,回调块在主线程(队列)上调用。如果您希望回调在特定队列上,可以使用接受分派队列作为参数的另一种方法。在这种情况下,将使用传入的队列来执行回调
- (void)requestAPI:(NSString *)name
withPathVariables:(id)pathVariables
parameters:(id)parameters
data:(id<WebApiResource>)data
queue:(dispatch_queue_t)callbackQueue
progress:(nullable WebApiClientRequestProgressBlock)progressCallback
finished:(void (^)(id<WebApiResponse> response, NSError *error))callback;
进度回调支持
上一部分中展示的接受显式回调块的相同方法还接受一个可选的 WebApiClientRequestProgressBlock
,其定义为以下内容
typedef void (^WebApiClientRequestProgressBlock)(NSString *routeName,
NSProgress * _Nullable uploadProgress,
NSProgress * _Nullable downloadProgress);
通过将此类型块传递给 progress
参数,您可以监控 HTTP 请求的上传和下载进度。此外,还可以使用 WebApiClientRequestDidProgressNotification
和 WebApiClientResponseDidProgressNotification
通知来监听进度更新。用户信息键 WebApiClientProgressNotificationKey
将包含相关的 NSProgress
对象。
同步请求支持
有时执行一个阻塞的、同步的 HTTP 资源请求可能很有用。WebApiClient 也支持这一点
- (id<WebApiResponse>)blockingRequestAPI:(NSString *)name
withPathVariables:(id)pathVariables
parameters:(id)parameters
data:(id<WebApiResource>)data
maximumWait:(NSTimeInterval)maximumWait
error:(NSError **)error;
调用此方法将阻塞调用线程,直到响应可用或 maximumWait
秒已过。
路由
WebApiRoute 协议定义了一个单个 API 端点定义,分配一个唯一名称。路由通常在应用程序启动时配置。每个路由定义了一些标准属性,例如 HTTP 的 method
和 URL 的 path
。为了方便,路由支持通过 Objective-C 的键子索引支持进行任意属性访问,因此以下是可能的
id<WebApiRoute> myRoute = ...;
// access the path property
NSString *path1 = myRoute.path;
// access the path property using keyed subscript notation
NSString *path2 = myRoute[@"path"];
// access some arbitrary property not defined in WebApiRoute specifically
id something = myRoute[@"extendedProperty"];
为了进一步提高便利性,WebApiRoute 为 NSDictionary
和 NSMutableDictionary
提供了 扩展,以便它们分别符合 WebApiRoute
和 MutableWebApiRoute
。这意味着您可以直接使用字典作为路由,如下所示
// define a route
id<WebApiRoute> myRoute = @{ @"name" : "login", @"path" : @"user/login", @"method" : @"POST" };
// create a mutable copy and extend
id<MutableWebApiRoute> mutableRoute = [myRoute mutableCopy];
mutableRoute[@"extendedProperty"] = @"special";
对象映射
WebApiDataMapper协议定义了一个API,用于将原生对象编码到HTTP请求中,以及将HTTP响应映射到原生对象中。可以通过配置dataMapper
属性来配置路由,以支持这一功能。该API也非常简单。
@protocol WebApiDataMapper <NSObject>
// Map a source data object into some domain object.
- (id)performMappingWithSourceObject:(id)sourceObject route:(id<WebApiRoute>)route error:(NSError *__autoreleasing *)error;
// Encode a domain object into an encoded form, such as @c NSDictionary or @c NSData.
- (id)performEncodingWithObject:(id)domainObject route:(id<WebApiRoute>)route error:(NSError *__autoreleasing *)error;
@end
模块:AFNetworking
AFNetworking模块提供了基于AFNetworking和NSURLSession
的WebApiClient
API的完整实现。
路由配置
可以通过registerRoute:forName:
方法在代码中配置路由,但更方便的是通过BREnvironment来配置。默认情况下,将会检查webservice.api
键,它可以为应用程序注册的所有路由表示一个字典。例如,以下JSON将会注册三个路由:login、register和absolute。
{
"App_webservice_protocol" : "https",
"App_webservice_host" : "example.com",
"App_webservice_port" : 443,
"webservice" : {
"api" : {
"register" : {
"method" : "POST",
"path" : "user/register",
},
"login" : {
"method" : "POST",
"path" : "user/login",
},
"absolute" : {
"method" : "GET",
"path" : "https://example.com/something"
}
}
}
}
您会注意到register和login路由有相对路径。所有web服务URL都相对于可配置的baseApiURL
属性构建,默认情况下是通过之前示例JSON中显示的各种App_webservice_* BREnvironment键配置的。
GZip压缩支持
支持在路由上启用gzip属性。当设置为true时,任何数据都会被压缩,并添加一个请求HTTP头Content-Encoding: gzip
。
为了支持压缩的响应数据(强烈推荐!),您只需配置一个Accept-Encoding: gzip
HTTP头,通过requestHeaders
属性在路由上或通过在AFNetworkingWebApiClient
上可用的globalHTTPRequestHeaders
属性来进行。
以下是一个配置请求和响应压缩的示例路由。
{
"webservice" : {
"api" : {
"trim" : {
"method" : "POST",
"path" : "upload/jumbo",
"gzip" : true,
"requestHeaders" : {
"Accept-Encoding" : "gzip"
}
}
}
}
}
上传原始数据
原始数据可以直接上传到HTTP请求体中。这适用于需要上传图片或其他类型的数据的情况,因为URL中包含了足够的信息来识别内容。要执行原始数据上传,请将WebApiResource实例传递到WebApiClient API中的data
参数。WebApiClient提供了两种WebApiResource
实现:用于内存数据的DataWebApiResource
和用于文件数据的FileWebApiResource
。您传递的WebApiResource
实例将直接发送到请求体中,并包含适当的Content-Type
和Content-MD5
HTTP头信息。这意味着parameters
对象将被忽略。如果需要同时上传参数和文件,请使用下一节中描述的multipart/form-data
上传方法。
使用multipart/form-data方式上传数据
除了直接上传原始数据外,还可以通过将一个WebApiResource实例传递到WebApiClient API中的data
参数来实现multipart/form-data
附件编码上传,并将路由配置为具有form
类型的序列化。
{
"webservice" : {
"api" : {
"trim" : {
"method" : "POST",
"path" : "upload/image",
"serializationName" : "form"
}
}
}
}
如果提供了WebApiClient API的parameters
对象,它也将包含在请求中,并按照请求体的其他部分序列化。
下载原始数据
您可以通过添加一个具有真值的saveAsResource
属性,将响应数据保存到文件中,而不是默认将响应加载到RAM中。
{
"webservice" : {
"api" : {
"download" : {
"method" : "GET",
"path" : "download/image",
"saveAsResource" : true
}
}
}
}
然后,在WebApiResponse
中返回的responseObject
将是一个WebApiResource,您可以将其移动到所需的位置,例如
[client requestAPI:@"download" withPathVariables:nil parameters:nil data:nil
queue:dispatch_get_main_queue()
progress:nil
finished:^(id<WebApiResponse> response, NSError *error) {
if ( !error ) {
id<WebApiResource> resource = response.responseObject;
NSURL *dest = [NSURL fileURLWithPath:@"/some/path"];
[[NSFileManager defaultManager] moveItemAtURL:[resource URLValue] toURL:dest error:nil];
}
}];
模块:缓存
缓存模块通过提供可以使用PINCache缓存结果对象的PINCacheWebApiClient
代理,为WebApiClient
API提供响应缓存支持。通过在路由上配置cacheTTL
属性来启用缓存支持,例如
{
"webservice" : {
"api" : {
"info" : {
"method" : "GET",
"path" : "infrequentlyupdated/info",
"cacheTTL" : 3600
}
}
}
}
有关路由缓存的更多详细信息,请参阅CachingWebApiRoute协议。
会使得其他路由的缓存数据失效的路由
您还可以配置路由,使得它失效任何针对 其他 路由的缓存数据。一个很好的例子是,当您定义一个返回对象列表的 列表 路由,还有一个添加对象到该列表的 添加 路由时。我们可以使后者路由失效前者路由的缓存数据,如下所示
{
"webservice" : {
"api" : {
"list" : {
"method" : "GET",
"path" : "stuff/list",
"cacheTTL" : 3600
},
"add" : {
"method" : "PUT",
"path" : "stuff/:thingId",
"invalidatesCachedRouteNames" : [ "list" ]
}
}
}
}
invalidatesCachedRouteNames
被配置为一个数组,当这些路由成功调用时,应失效的路线名称。
忽略路由URL查询参数
默认情况下,在计算每个路由的缓存密钥时,会包括URL查询参数。有时忽略查询参数可能会有用。例如,Amazon S3资源的预签名URL包含每次请求相同的资源时都会改变的授权查询参数。通过使用cacheIgnoreQueryParameters = YES
配置该路由,那么对该路由的查询参数将不包含在请求的缓存键中。
{
"webservice" : {
"api" : {
"info" : {
"method" : "GET",
"path" : "https://s3.amazon.com/info/foo.txt",
"cacheTTL" : 3600,
"cacheIgnoreQueryParameters" : true
}
}
}
}
支持多用户的缓存组
PINCacheWebApiClient
支持一个可以在运行时改变的keyDiscriminator
属性,可以将所有缓存路由数据隔离到组中。这个功能的主要使用场景是支持多用户应用,其中路由URL不包含识别用户的参数,因此当不同用户登录时,他们不会看到其他用户的缓存数据。您可以将活动用户的唯一标识符分配给keyDiscriminator
属性,然后所有缓存数据都变为了特定用户。当用户注销并登录不同用户时,更改keyDiscriminator
为新的用户标识符。
检查缓存数据
CachingWebApiClient
API增加了一个方法,可以用来测试缓存数据是否可用。
- (void)requestCachedAPI:(NSString *)name
withPathVariables:(nullable id)pathVariables
parameters:(nullable id)parameters
queue:(dispatch_queue_t)callbackQueue
finished:(void (^)(id<WebApiResponse> _Nullable response, NSError * _Nullable error))callback;
只有当数据已经在缓存中时,回调才会传递回响应。有时知道一些内容已经被下载是有用的!
模块:RestKit
RestKit 模块为基于 RestKit 的 WebApiClient
API 提供了 对象映射 实现方式。该模块能够将本地对象转换为 JSON 格式,反之亦然。这个模块仅使用 RestKit/ObjectMapping
模块,因此不会与 AFNetworking 2 冲突。实际上,WebApiClient 的创建部分动机是能够使用 AFNetworking 2 和 RestKit 的对象映射支持,因为 RestKit 的网络层是基于 AFNetworking 1 的。在某种程度上,WebApiClient API 提供的功能与完整 RestKit 项目提供的功能相似。
映射配置
RestKitWebApiDataMapper
类支持一个共享的单例模式,应用程序可以在启动时配置任何必需的 RKObjectMapping
对象。您可以这样配置它
RestKitWebApiDataMapper *dataMapper = [RestKitWebApiDataMapper sharedDataMapper];
// get RestKit mapper for user objects
RKObjectMapper *userObjectMapper = ...;
// register user mapper for requests and responses
[dataMapper registerRequestObjectMapping:[userObjectMapper inverseMapping] forRouteName:@"login"];
[dataMapper registerResponseObjectMapping:userObjectMapper forRouteName:@"login"];
基于块的编码和映射
RestKitWebApiDataMapper
类还支持基于块的映射钩子,既可以用于请求编码,也可以用于响应映射。这些块执行 在 已配置的 RKObjectMapper
对请求或响应数据进行处理之后。
此支持的一个有用示例是连接隐含由数据指示的父子关系属性。想象一个具有子 Person
对象数组的 Person
类,每个子对象都有一个指向其父 Person
实例的 parent
属性
@interface Person : NSObject
@property (strong) NSString *name;
@property (strong) NSArray<Person *> *children;
@property (weak) Person *parent;
@end
服务器返回的 JSON 如下所示
{
"name" : "John Doe",
"children" : [
{ "name" : "Johnny Doe" },
{ "name" : "Jane Doe" }
]
}
要使用新分配的 John Doe 对象填充每个子对象的 parent
属性,可以配置响应映射块如下
[dataMapper registerResponseMappingBlock:^(id sourceObject, id<WebApiRoute> route, NSError * __autoreleasing *error) {
if ( [sourceObject isKindOfClass:[Person class]] ) {
// populate the Child -> Parent relationship
Person *parent = sourceObject;
for ( Person *child in parent.children ) {
child.parent = parent;
}
}
return sourceObject;
} forRouteName:@"parent-child-tree"];
路由配置
要使用基于RestKit的对象映射与路由,你需要将路由的dataMapper
属性配置为RestKitWebApiDataMapper
,如下所示
{
"webservice" : {
"api" : {
"login" : {
"method" : "POST",
"path" : "user/login",
"dataMapper" : "RestKitWebApiDataMapper"
}
}
}
}
有时请求或响应的JSON需要嵌套在某个顶级对象中。例如,想象一下,注册端点期望将用户对象以如下JSON格式发布
{
"user" : { "email" : "[email protected]", "name" : "Joe" }
}
可以通过添加一个dataMapperRequestRootKeyPath
属性(或用于映射响应的dataMapperResponseRootKeyPath
)来实现,如下所示
{
"webservice" : {
"api" : {
"login" : {
"method" : "POST",
"path" : "user/login",
"dataMapper" : "RestKitWebApiDataMapper",
"dataMapperRequestRootKeyPath" : "user"
}
}
}
}
模块:UI
UI模块提供了一些UI实用工具,例如WebApiClientActivitySupport
类,它可以监听路由请求,对于那些指定了具有布尔值的preventUserInteraction
的路由,会抛出一个全屏的“请求时间过长”视窗,让应用程序的用户知道正在等待响应。例如,可以配置如下路由
{
"webservice" : {
"api" : {
"login" : {
"method" : "POST",
"path" : "user/login",
"preventUserInteraction" : true
}
}
}
}