作为客户端开发者,我们的大部分工作是与各种网络服务进行交互,通常是 RESTful 类型。AFNetworking 是 Cocoa 生态圈中可能最流行的第三方库,这可能是因为它使与网络服务的交互变得容易得多,以至于我几乎没有记得我最后一次做的不使用它的项目。
然而,网络服务集成在客户端开发中仍然存在一些难以逾越的难点。
虽然我们认为所有客户端项目都是从完成了的 API 开始的,但往往并不是这样。你有多少次在一个客户项目中与正在重开发的 API 有依赖关系?当然,一般对象数据结构可能已知,但你很难一上来就有完全稳定的 API。
此外,一旦你开始编写与该 API 交互的代码,你就会注意到使该 API 更好的某些设计变更。无论后端工程团队有多好,他们在客户开始集成之前基本上是在真空中设计 API,但遗憾的是,到那时通常已经太晚进行实质性的更改了。
如果您像我一样,那么您可能有很多优秀的 Objective-C 技能,但您在后台代码方面的经验可能有些不令人满意。当我需要原型一个 API,因为我想要尝试一个客户端想法时,我通常会使用 rails-api 或 sinatra 来组合一些简单的内容,尽管它们工作得很不错,但这显然不是我在尝试原型客户端应用程序时所特别喜欢的转变,因为它迫使我在开发环境之间不断切换,而这些框架更多地面向构建实际的 PRODUCTION 应用程序,因此它们的复杂性超出了简单的原型工作。
我尝试了几种用于此目的的其他框架,如 Deployd,甚至使用一个小的 sinatra 实例提供静态 JSON,但没有一个是为那些真正希望在 Objective-C 项目中包含它们的人而设计的。
编写与API正确交互的健壮测试是很有难度的。这有几个原因。
遗憾的是,大多数人要么最终编写了非常脆弱的集成测试,然后对所有那些虚假失败的测试感到沮丧(有时在这个过程中甚至错过了他们的代码中的实际错误),要么完全避免对那些Web服务进行测试。这可能就是为什么(或者至少是几个原因之一)iOS上的单元测试并未像许多服务器端语言那样取得很大的进步。
RESTEasy是我针对这些问题的解决方案。想要快速设置RESTful服务器?只要这样做?
#import <RESTEasy/RESTEasy.h>
[[TGRESTServer sharedServer] startServerWithOptions:nil];
现在,你的Objective-C客户端项目中已经运行了一个RESTful API服务器!它可以通过HTTP调用来访问,就像任何其他服务器一样,并且它已经准备好开始接收请求了。但这只是这个能力解锁的开端...
在我深入探讨RESTEasy所能做的所有事情之前,让我们先从几个基本示例开始,然后我会向你展示如何将这个库按照你的意愿来修改。
TGRESTResource *people = [TGRESTResource newResourceWithName:@"people" model:@{
@"name": [NSNumber numberWithInteger:TGPropertyTypeString],
@"numberOfKids": [NSNumber numberWithInteger:TGPropertyTypeInteger],
@"kilometersWalked": [NSNumber numberWithInteger:TGPropertyTypeFloatingPoint],
@"avatar": [NSNumber numberWithInteger:TGPropertyTypeBlob]
}];
[[TGRESTServer sharedServer] addResource:people];
[[TGRESTServer sharedServer] startServerWithOptions:nil];
我们的服务器现在已经准备好接受请求了!
如果你愿意,你可以使用curl检查服务器是否工作,方法如下
curl https://127.0.0.1:8888/people
[]
一个空数组?这没有什么令人兴奋,但是这是一个起点。那么我们如何在其中加载数据呢?我们可以从正常的REST方式开始
curl \
-X POST \
-H "Content-Type: application/json" \
-d '{"name":"john","numberOfKids":1}' \
http://10.0.1.66:8888/people
{"numberOfKids":1,"id":1,"kilometersWalked":null,"name":"john","avatar":null}
太好了!我们的第一个对象成功返回了。我们也可以重复我们的第一个请求并得到这个结果
[{"numberOfKids":1,"id":1,"kilometersWalked":null,"name":"john","avatar":null}]
好的,让我们尝试更新它,让名字变成"jeff"。
curl \
-X PUT \
-H "Content-Type: application/json" \
-d '{"name":"jeff"}' \
http://10.0.1.66:8888/people/1
{"numberOfKids":1,"id":1,"kilometersWalked":null,"name":"jeff","avatar":null}
还不错。那么删除呢?
curl \
-X DELETE \
http://10.0.1.66:8888/people/1
它的工作方式就像你预期的那样。
当然我们不想用API调用就加载整个数据集。幸运的是,《RESTEasy》为你准备了一些非常简单的方法来加载你的样本数据。
假设你的数据是以JSON格式存储的,具有与您为该示例定义的资源模型相同的属性名称--我们可以通过以下方式加载它:
// Let's assume this was assigned to the resource we created before
TGRESTResource *people = ...
// First lets get the JSON from disk and parse it
NSString *pathToJSON = [[NSBundle mainBundle] pathForResource:@"testdata" ofType:@"json"];
NSData *rawJSON = [NSData dataWithContentsOfFile:pathToJSON];
NSError *parsingError;
id json = [NSJSONSerialization JSONObjectWithData:rawJSON options:kNilOptions error:&parsingError];
if (!error) {
// Now pass it to the server
[[TGRESTServer sharedServer] addData:json forResource:people];
}
你传递的对象首先将被清理,并从中提取任何匹配的参数,并且只要至少有一个属性名称相匹配,它就会摄取整个JSON对象并为每个创建数据。是不是很简单?
您可以通过非常简单的方式设置资源之间的一对多关系。您只需做类似于以下这样的事情
TGRESTResource *people = [TGRESTResource newResourceWithName:@"people"
model:@{
@"name": [NSNumber numberWithInteger:TGPropertyTypeString],
@"numberOfKids": [NSNumber numberWithInteger:TGPropertyTypeInteger],
@"kilometersWalked": [NSNumber numberWithInteger:TGPropertyTypeFloatingPoint],
@"avatar": [NSNumber numberWithInteger:TGPropertyTypeBlob]
}];
TGRESTResource *cars = [TGRESTResource newResourceWithName:@"cars"
model:@{
@"name": [NSNumber numberWithInteger:TGPropertyTypeString],
@"color": [NSNumber numberWithInteger:TGPropertyTypeString]
}
actions:TGResourceRESTActionsDELETE | TGResourceRESTActionsGET | TGResourceRESTActionsPOST | TGResourceRESTActionsPUT
primaryKey:nil
parentResources:@[people]];
[[TGRESTServer sharedServer] addResource:people];
[[TGRESTServer sharedServer] addResource:cars];
这样做会创建以下默认路由
资源 | 动词 | URI 模式 | 结果 |
---|---|---|---|
人员 | GET | /人员 | 所有人员的列表 |
人员 | GET | /人员/:id | 根据 id 获取单个人员 |
人员 | POST | /人员 | 创建新的人员 |
人员 | PUT | /人员/:id | 根据 id 更新人员 |
人员 | DELETE | /人员/:id | 根据 id 删除人员 |
汽车 | GET | /汽车 | 所有汽车的列表 |
汽车 | GET | /汽车/:id | 根据 id 获取单个汽车 |
汽车 | GET | /人员/:id/汽车 | 根据 id 获取人员的所有汽车 |
汽车 | POST | /汽车 | 创建新的汽车 |
汽车 | POST | /人员/:id/汽车 | 根据 id 为人员创建新的汽车 |
汽车 | PUT | /汽车/:id | 根据 id 更新汽车 |
汽车 | DELETE | /汽车/:id | 根据 id 删除汽车 |
这遵循了类似于这里所述的浅嵌套概念,但有一些小的不同(这些主要涉及限制嵌套资源,而这并不是这个库的主要目的)。
上面的内容应该会给你一个如何快速开始的好主意。但关于定制化呢?
RESTEasy本应该是关于模拟网络服务而不是创建精确复制品的,但在某些方面,你可能需要比基础知识更多。为了使这一点尽可能简单,而又不损害其易于接近和易于设置及开始使用的核心使命,RESTEasy进行了架构设计。
如果你的数据与服务器提供的 JSON 表示形式不完全匹配?这可能是几个原因造成的
你可以在导入之前对数据进行规范化(这当然是一个选项),但如果你想要更多的控制,那就找找
这个协议非常简单,它定义了三个类方法
+ (id)dataWithSingularObject:(NSDictionary *)object resource:(TGRESTResource *)resource;
+ (id)dataWithCollection:(NSArray *)collection resource:(TGRESTResource *)resource;
+ (NSDictionary *)requestParametersWithBody:(NSDictionary *)body resource:(TGRESTResource *)resource;
前两个是在从数据存储库检索对象或对象集合后生成响应的,第三个方法提供了传入请求的参数,你可以按照自己的喜好对其进行规范化。实际上,这不叫序列化,因为其实并没有什么序列化和反序列化(尽管当考虑到事件序列时,这些方法会在“真正的”序列化之前或之后直接调用),它实际上是在数据进入或离开服务器资源控制器之前将其转换为所需的任何形式的方法。
你可以更改属性名称、格式、注入默认或静态值,你喜欢做的一切。这里的理念是,如果你需要做丑陋的修改以使你的数据表示看起来正确,那就让我们做正确的丑陋修改,并将它从框架的其他部分中隔离出来。
有一个符合
#import "TGRESTDefaultSerializer.h"
@implementation TGRESTDefaultSerializer
+ (id)dataWithSingularObject:(NSDictionary *)object resource:(TGRESTResource *)resource
{
NSParameterAssert(object);
NSParameterAssert(resource);
return object;
}
+ (id)dataWithCollection:(NSArray *)collection resource:(TGRESTResource *)resource
{
NSParameterAssert(collection);
NSParameterAssert(resource);
return collection;
}
+ (NSDictionary *)requestParametersWithBody:(NSDictionary *)body resource:(TGRESTResource *)resource
{
NSParameterAssert(resource);
return body;
}
@end
它绝对不做任何事情,这正是重点。如果你想自定义请求或响应使其与现有API兼容,你可以这样做,而且你不需要修改你的数据表示,因为它们将对此一无所知。
一旦你创建了一个实现了 TGRESTSerializer
协议的类,你需要将它加载到服务器上。有两种方法来实现这一点。
如果没有指定资源序列化程序,将使用此序列化程序。如果你想将所有内容都在一个类中,使用一组规则进行封装,这是一个简单的方法,只是要记住,与资源序列化程序不同,你可以只在启动服务器时设置它。
要设置默认序列化程序,请这样做
NSDictionary *options = @{TGRESTServerDefaultSerializerClassOptionKey: [MyCustomSerializer class]};
[[TGRESTServer sharedServer] startServerWithOptions:options];
注意,你传递的是自定义序列化程序的 类 而不是一个实例。
你可以随时设置资源序列化程序,甚至在服务器运行时也可以。它的工作原理是资源控制器会首先查找资源的序列化程序,如果找不到,则只使用默认序列化程序。因此,这允许你在需要的地方分配自定义序列化程序,但如果你没有定义,你始终可以依靠默认的序列化程序。
为资源设置序列化程序就像这样做
[[TGRESTServer sharedServer] setSerializerClass:[MyCustomSerializer class] forResource:people];
然后可以移除它
[[TGRESTServer sharedServer] removeCustomSerializerForResource:people];
为了正确进行网络测试的模拟,通常需要使用像链路条件器这样的工具。这虽然足够,但由于它在客户端进行节流,因此需要远程服务的网络连接。当然,你可以仅仅使用 RESTEasy 的链路条件器,但有一个更直接的方法。
服务器配置包括设置延迟的最小值和最大值的功能,如下所示
NSDictionary *serverOptions = @{
TGLatencyRangeMinimumOptionKey: @1.0,
TGLatencyRangeMaximumOptionKey: @2.0
};
[[TGRESTServer sharedServer] startServerWithOptions:serverOptions];
这将使得响应通过人工节流,从而使响应在至少指定的范围内以随机响应时间返回(但要明确的是,如果范围低且由于某种原因请求花费了很长时间,则它可以更高)。将此设置为模拟真实网络请求是很好的,因为如果本地调用没有模拟延迟,它们往往会返回10毫秒时间范围内。
真的想在 RESTEasy 上进行破解?嗯,还有一些其他的事情你可以做。
你想要在资源上执行超出基本 CRUD 的操作?虽然 RESTEasy 既不是也不是永远不会是 Rails,但它确实有能力用另一个控制器替换默认控制器。这不会改变默认的路由,但它会提供对控制器操作(索引、显示、创建、更新、删除)的完全可定制性。检查 TGRESTController
协议或 TGRESTDefaultController
类,看看你可以走到哪里,因为你可以通过从 TGRESTDefaultController
继承来保留超级 CRUD 方法,或者创建一个符合规范的自己的控制器。
一旦你有了它,你就可以像加载自定义序列化程序一样将其加载到服务器上。
NSDictionary *options = @{TGRESTServerControllerClassOptionKey: [MyCustomController class]};
[[TGRESTServer sharedServer] startServerWithOptions:options];
只是确保这真的是你想要的,因为这个特性是高级特性,大多数人可能不需要或想纠结。在使用此功能之前,你应该已经用自定义的 TGRESTSerializer
做到了你能做的。
默认情况下有两种存储类型:内存和sqlite,它们都有各自的用途。但你会注意到,这两种都是名为 TGRESTStore
的抽象超类的子类。如果你想将你的REST API连接到其他东西(从JSON文件到远程Web服务等),你可以通过创建一个自定义存储来实现 TGRESTStore
上的所有方法。
- (NSUInteger)countOfObjectsForResource:(TGRESTResource *)resource;
- (NSDictionary *)getDataForObjectOfResource:(TGRESTResource *)resource
withPrimaryKey:(NSString *)primaryKey
error:(NSError * __autoreleasing *)error;
- (NSArray *)getDataForObjectsOfResource:(TGRESTResource *)resource
withParent:(TGRESTResource *)parent
parentPrimaryKey:(NSString *)key
error:(NSError * __autoreleasing *)error;
- (NSArray *)getAllObjectsForResource:(TGRESTResource *)resource
error:(NSError * __autoreleasing *)error;
- (NSDictionary *)createNewObjectForResource:(TGRESTResource *)resource
withProperties:(NSDictionary *)properties
error:(NSError * __autoreleasing *)error;
- (NSDictionary *)modifyObjectOfResource:(TGRESTResource *)resource
withPrimaryKey:(NSString *)primaryKey
withProperties:(NSDictionary *)properties
error:(NSError * __autoreleasing *)error;
- (BOOL)deleteObjectOfResource:(TGRESTResource *)resource
withPrimaryKey:(NSString *)primaryKey
error:(NSError * __autoreleasing *)error;
- (void)addResource:(TGRESTResource *)resource;
- (void)dropResource:(TGRESTResource *)resource;
如果你想了解更多关于实现你自己的具体存储类,请查看 TGRESTStore
的文档以及现有的两个实现 TGRESTInMemoryStore
和 TGRESTSqliteStore
。
如果你想尝试示例应用,运行测试套件或提交一个拉取请求,请首先克隆仓库并从根目录运行 pod install
,然后打开 RESTEasy.xcworkspace
。
作为Cocoadocs散集提供或你可以下载仓库并运行 'rake docs:generate'。所有类都有完全的文档。
请查看 /Example 文件夹。
适用于OSX 10.8和iOS 6.0及其以上版本。
不需要或不希望持久性?你可以这样使用 'core' 子规范安装仅依赖于内存存储的-core 文件,而不依赖于FMDB或sqlite:
pod "RESTEasy/core"
没有几个出色的子项目,这个库将无法实现。
John Tumminaro,[email protected]
RESTEasy 在 MIT 许可证下可用。有关更多信息,请参阅 LICENSE 文件。