这一次我们将要讨论的是移动开发中比较重要的一环–网络请求的封装.鉴于个人经验有限,本文将在一定程度上参考 基于AFNetworking2.0和ReactiveCocoa2.1的iOS REST Client,来以LeanCloud的Rest Api来练手.前两节的示例,我们都是使用自定义的PHP接口来作为测试服务器,但是真实的服务器接口是涉及到许多细节的,比如一个基本的权限控制机制,用户登录登出等.为了能更真实快速的开始网络请求类的重构,本节选取一个国内较为常用的后端开发平台LeanCloud. 本文将实现一个拥有真实数据的博客App的Demo,数据源取自博客主站:ios122.com.

完整代码示例下载: github

将WP导出的XML数据转换成JSON文件,导入LeanCloud.

首先,你是肯定要先去它们官网注册一个账号,然后添加一个应用.这是我是添加了应用iOS122.然后新建一个名为Post的Class,字段信息如下:

iOS122是一个wordpress搭建的博客站点,导出的文章为xml格式,需要处理成 LeanCloud 需要的JSON格式才能导入,主站文章不多,几十篇,一个一个手动输,也是可以的.我将试着写一小段代码,来自动解析wp导出的文件,并根据需要生成对应的 JSON 文件.感兴趣的,可以自己试着弄下!

  • 这是原始的从wp中导出的主站的所有文章: http://ios122.bj.bcebos.com/Post.xml.
  • 这是通过iOS代码解析处理后,生成的可直接导入进LeanCloud的JSON文件. http://ios122.bj.bcebos.com/Post.json
  • 这是XML转JSON核心代码,完整代码见文首github链接,XML解析用了一个第三方库Ono:
/* 要实现的逻辑很简单: 1.读取XML文件;2.解析为JSON,并显示;3.将JSON输出为json文件.*//* 1.读取并解析XML. */
NSMutableArray * jsonArray = [NSMutableArray arrayWithCapacity: 42];NSString *XMLFilePath = [[NSBundle mainBundle] pathForResource:@"Post" ofType:@"xml"];
ONOXMLDocument *document = [ONOXMLDocument XMLDocumentWithData:[NSData dataWithContentsOfFile:XMLFilePath] error: NULL];NSString *XPath = @"//channel/item";[document enumerateElementsWithXPath:XPath usingBlock:^(ONOXMLElement *element, __unused NSUInteger idx, __unused BOOL *stop) {ONOXMLElement * titleElement = [element firstChildWithTag:@"title"];ONOXMLElement * descElement = [element firstChildWithTag: @"encoded" inNamespace: @"excerpt"];ONOXMLElement * contentElement = [element firstChildWithTag: @"encoded" inNamespace:@"content"];NSDictionary * jsonDict = @{@"title": [titleElement stringValue],@"desc": [descElement stringValue],@"body": [contentElement stringValue]};[jsonArray addObject: jsonDict];
}];/* 2.显示JSON字符串. */
NSData * jsonData = [NSJSONSerialization dataWithJSONObject:jsonArrayoptions:NSJSONWritingPrettyPrintederror:NULL];NSString * jsonString = [[NSString alloc] initWithData:jsonDataencoding:NSUTF8StringEncoding];self.textView.text = jsonString;/*3.存储到文件中.真机下,暂无法找到Documents目录下的东西,可以通过模拟器运行此段代码,并通过finder-->前往文件夹,输入此处jsonPath对应的文件路径来获取 Post.json 文件.*/
NSArray *paths=NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString * path=[paths objectAtIndex:0];
NSString * jsonPath=[path stringByAppendingPathComponent:@"Post.json"];[jsonData writeToFile: jsonPath atomically:YES];
  • 导入后,LeanCloud控制台显示是这样的:

模仿 “花瓣”,重写 LeanCloud Rest Api的iOS REST Client.

接下来的文字,思路上将在很大程度上参考 @limboy的文章,但是会相对更加完整.另外,其实 LeanCloud 其实是有自己的iOS API的,但是是一个抽象的封装,和实际应用中使用的网络请求API有很大不同.两种方式的差别,有点类似于是使用 字典等基本类型存储数据,还是使用 自定义的Model来存储数据.两种方式,不过多置评,个人倾向于后一种,方便后续的代码重构.

// TODO:Models Group包含了所有跟服务端API对应的Model,比如HBPComment

基本结构

使用时,直接引用 YFAPI.h 即可,里面包含了所有的Class:

|- YFAPI.h
|- Classes|- YFAPIManager.h|- YFAPIManager.m|- Models|- YFPostModel.h|- YFPostModel.h...

YFAPIManager包含了所有的跟服务端通信的方法,通过Category来区分:

//
//  YFAPIManager.h
//  iOS122
//
//  Created by 颜风 on 15/10/28.
//  Copyright © 2015年 iOS122. All rights reserved.
//#import <Foundation/Foundation.h>
#import <AFNetworking.h>@class RACSignal, YFUserModel;@interface YFAPIManager : AFHTTPRequestOperationManager@property (nonatomic, nonatomic) YFUserModel * user; //!< 当前登录的用户,可能为nil./***  一个单例.**  @return 共享的实例对象.*/
+ (instancetype) sharedInstance;@end/***  私有扩展,其他网路请求的基础.*/
@interface YFAPIManager (Private)/***  内部统一使用这个方法来向服务端发送请求**  @param method       请求方式.*  @param relativePath 相对路径.*  @param parameters   参数.*  @param resultClass  从服务端获取到JSON数据后,使用哪个Class来将JSON转换为OC的Model.**  @return RACSignal 信号对象.*/
- (RACSignal *)requestWithMethod:(NSString *)method relativePath:(NSString *)relativePath parameters:(NSDictionary *)parameters resultClass:(Class)resultClass;@end/***  用户信息相关的操作.*/
@interface YFAPIManager (User)/***  用户登录.**  获取到用户数据后,会自动更新User属性,所以仅需要在必要的地方观察user属性即可.**  @param username 用户名.*  @param password 用户密码.**  @return RACSingal对象,sendNext的是此类的的单例实例.*/
- (RACSignal *)signInUsingUsername:(NSString *)username passowrd:(NSString *)password;/***  登出.**  登出,其实就是把 user 属性设为nil.**  @return sendNext为此类的单例实例.*/
- (RACSignal *) logout;@end/***  文章相关操作.*/
@interface YFAPIManager (Post)
//....@end

Models Group包含了所有跟服务端API对应的Model,比如 YFPostModel:

//
//  YFPostModel.h
//  iOS122
//
//  Created by 颜风 on 15/10/28.
//  Copyright © 2015年 iOS122. All rights reserved.
//#import <Foundation/Foundation.h>
#import <Mantle.h>/***  文章.*/
@interface YFPostModel : MTLModel <MTLJSONSerializing>@property (strong, nonatomic) NSString * postId; //!< 文章唯一标识.
@property (copy, nonatomic) NSString * title; //!< 文章标题.
@property (copy, nonatomic) NSString * desc; //!< 文章简介.
@property (copy, nonatomic) NSString * body; //!< 文章详情.@end
//
//  YFPostModel.m
//  iOS122
//
//  Created by 颜风 on 15/10/28.
//  Copyright © 2015年 iOS122. All rights reserved.
//#import "YFPostModel.h"@implementation YFPostModel/***  用于指定模型属性与JSON数据字段的对应关系.**  @return 模型属性与JSON数据字段的对应关系:以模型属性为键,JSON字段为值.*/+ (NSDictionary *)JSONKeyPathsByPropertyKey {NSDictionary * dictMap = @{@"postId": @"objectId",@"title": @"title",@"desc": @"desc",@"body": @"body"};return dictMap;
}@end

可以使用类似下面的语句,来将JSON转换为Model:

YFPostModel * model = [MTLJSONAdapter modelOfClass:[YFPostModel class] fromJSONDictionary:@{@"title": @"标题", @"desc": @"简介", @"body": @"内容", @"objectId": @"id"} error: NULL];

Archive / UnArchive / Copy

每一个Model都要支持Archive / UnArchive / Copy,也就是要实现和协议,这两个协议的内容其实就是对Object的Property做些处理,所以如果可以在基类里把这些事都统一处理,就会方便许多。考虑到设计的稳定性和后期的可扩展性,我们使用比较著名的第三方库–Mantle 来处理.你可以使用CocoaPods安装这个库,然后引入头文件 #import <Mantle.h> 到自定义的Model中即可.

pod 'Mantle' # JSON <==> Model

用户的登录与登出

先来说说登录,由于使用RAC,在构造API时,就不需要传入Block了,随之而来的一个问题就是需要在注释中说明sendNext时会发送什么内容.LeanCloud用户登录接口会返回完整的用户信息:

+ (RACSignal *)signInUsingUsername:(NSString *)username passowrd:(NSString *)password
{NSDictionary *parameters = @{@"username": username,@"password": password,};YFAPIManager *manager = [self sharedInstance];// 需要配对使用@weakify 与 @strongify 宏,以防止block内的可能的循环引用问题.@weakify(manager);return [[[[manager rac_GET:@"login" parameters:parameters]// reduceEach的作用是传入多个参数,返回单个参数,是基于`map`的一种实现reduceEach:^id(NSDictionary *response, AFHTTPRequestOperation *operation){@strongify(manager);YFUserModel * user = [MTLJSONAdapter modelOfClass:[YFUserModel class] fromJSONDictionary: response error: NULL];manager.user = user;return manager;}]// 避免side effect,有点类似于 "懒加载".replayLazily]setNameWithFormat:@"+signInUsingUsername:%@ password:%@", username, password];
}

用户的登出就简单了,直接设置user为nil就行了:

+ (RACSignal *)logout
{YFAPIManager * manager = [YFAPIManager sharedInstance];@weakify(manager);return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {@strongify(manager);manager.user = nil;[subscriber sendNext: manager];[subscriber sendCompleted];return nil;}];
}

设置超时时间和缓存策略

“花瓣”采取的是重新定义 AFHTTPRequestSerializer 子类的方式,但其实用AOP,几行代码就够了:

// 设置超时和缓存策略.
[self.requestSerializer aspect_hookSelector:@selector(requestWithMethod:URLString:parameters:error:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo>info){/* 在方法调用后,来获取返回值,然后更改其属性. */// __autoreleasing 关键字是必须的,默认的 __strong,会引起后续代码的野指针崩溃.__autoreleasing NSMutableURLRequest *  request = nil;NSInvocation *invocation = info.originalInvocation;[invocation getReturnValue: &request];if (nil != request) {request.timeoutInterval = 30;request.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;[invocation setReturnValue: &request];}
}error: NULL];

使用了一个AOP库,感兴趣的戳这里: Aspects.

权限验证

这个比较简单些,直接在方法里面加上判断属性self.isAuthenticated 即可:

if (!self.isAuthenticated)
{....
}

其中 isAuthenticated 为基于self.user的推导属性,其实现如下:


RAC(self, isAuthenticated) = [RACSignal combineLatest:@[RACObserve(self, user)] reduce:^id{@strongify(self);BOOL isLogin = YES;if (nil == self.user || nil == self.user.token) {isLogin = NO;}return [NSNumber numberWithBool: isLogin];
}];

实现博客数据的访问.

这里我们要实现访问某个具体的博客数据,以验证上述各种基础构件的可用性.为了使示例更具有典型性,我手动将博客数据设为仅指定测试用户(测试用户可以在LeanCloud后台添加和指定)可以访问:

需要先实现- (RACSignal *)requestWithMethod:(YFAPIManagerMethod)method relativePath:(NSString *)relativePath parameters:(NSDictionary *)parameters resultClass:(Class)resultClass;方法,这是所有网络访问的基础,如下:

/***  内部统一使用这个方法来向服务端发送请求**  @param method       请求方式.*  @param relativePath 相对路径.*  @param parameters   参数.*  @param resultClass  从服务端获取到JSON数据后,使用哪个Class来将JSON转换为OC的Model.**  @return RACSignal 信号对象.sendNext返回的是转换后的Model.*/- (RACSignal *)requestWithMethod:(YFAPIManagerMethod)method relativePath:(NSString *)relativePath parameters:(NSDictionary *)parameters resultClass:(Class)resultClass
{RACSignal * signal = nil;if (method == YFAPIManagerMethodGet) {signal = [self rac_GET:relativePath parameters:parameters];}if (method == YFAPIManagerMethodPut) {signal = [self rac_PUT:relativePath parameters:parameters];}if (method == YFAPIManagerMethodPost) {signal = [self rac_POST:relativePath parameters:parameters];}if (method == YFAPIManagerMethodPatch) {signal = [self rac_PATCH:relativePath parameters:parameters];}if (method == YFAPIManagerMethodDelete) {signal = [self rac_DELETE:relativePath parameters:parameters];}return [[signal reduceEach:^id(NSDictionary *response){id responseModel = [MTLJSONAdapter modelOfClass:resultClass fromJSONDictionary:response error:NULL];return responseModel;}]replayLazily];
}

然后添加一个用户博客详情访问的方法即可:

/***  获取文章详情.**  @param postId 文章id.**  @return sendNext为获取到的文章数据模型.*/- (RACSignal *)fetchPostDetail:(NSString *)postId
{return [[self requestWithMethod:YFAPIManagerMethodGet relativePath:[NSString stringWithFormat:@"classes/Post/%@", postId] parameters:nil resultClass: [YFPostModel class]] setNameWithFormat: @"%@ -fetchPostDetail: %@", self.class, postId];
}

然后你就可以用类似下面的代码访问博客详情了:

[[[YFAPIManager sharedInstance] fetchPostDetail: @"56308138e4b0feb4c8ba2a34"] subscribeNext:^(YFPostModel * x) {NSLog(@"%@", x.body);[self.webView loadHTMLString:x.body baseURL:nil];
}];

一些你可能需要知道的技术细节

md5 加密

LeanClodu Rest API 需要在本地对masterKey在本地做一次md5加密,我封装了一个方法,可以直接用:

/***  将字符串md5加密,并返回加密后的结果.**  @param originalStr 原始字符串.*  @param lower       是否返回小写形式: YES,返回全小写形式;NO,返回全大写形式.**  @return md5 加密后的结果.*/
- (NSString *) md5Str: (NSString *) originalStr isLower: (BOOL) lower
{const char *original = [originalStr UTF8String];unsigned char result[CC_MD5_DIGEST_LENGTH];CC_MD5(original, (CC_LONG)strlen(original), result);NSMutableString *hash = [NSMutableString string];for (int i = 0; i < 16; i++){[hash appendFormat:@"%02X", result[i]];}NSString * md5Result = [hash lowercaseString];if (NO == lower) {md5Result = [md5Result uppercaseString];}return md5Result;
}

动态设置请求头

因为LeanCloud的请求签权和时间戳有挂,所以每次请求都需要重置部分请求头,此处可以每个请求都手动设置,但是我是使用AOP,直接hook了一下(PS:强烈建议不知道AOP为何物的童鞋,学习下,真的很爽用起来):

// 每次发送请求前,都需要更新一下 请求头中的 apiClientSecret,因为它是时间戳相关的.
[self aspect_hookSelector:NSSelectorFromString(@"rac_requestPath:parameters:method:") withOptions:AspectPositionBefore usingBlock:^{@strongify(self);[self.requestSerializer setValue: self.apiClientSecret forHTTPHeaderField: @"X-LC-Sign"];} error:NULL];

token值自动设置

这个其实算是RAC的基础,让token和user的变化绑定起来就行了,如果你想重写user的setter方法,然后出发请求头中token的变化,也是可以的(但我更喜欢RAC的写法了):

// 每次用户数据更新时,都需要重新设置下请求头中的token值.
[RACObserve(self, user) subscribeNext:^(YFUserModel * user) {@strongify(self);[self.requestSerializer setValue:user.token forHTTPHeaderField: @"X-LC-Session"];
}];

“推导属性”的实现

所谓”推导属性”,就是那些附属的,是依据其他属性推断出来的属性,本身应该随着核心属性的变化而自动变化.实现方式有很多,可以重写此属性的getter方法,也可以像下面这样:

// 设置isAuthenticated.
RAC(self, isAuthenticated) = [RACSignal combineLatest:@[RACObserve(self, user)] reduce:^id{@strongify(self);BOOL isLogin = YES;if (nil == self.user || nil == self.user.token) {isLogin = NO;}return [NSNumber numberWithBool: isLogin];
}];

小结与预告

因为我们的服务器,是传统的PHP服务器,所以本文对LeanCloud的分析,仅供大家作为技术实现上的一个参考.具体到自己的业务细节,可能有些地方,需要特殊处理.关于以上技术讨论的问题,欢迎跟帖讨论!

下一篇主题,会对单元测试的一些细节做一分析.边摸索边学习,总算真到了一个合适的重构我们已有工程的策略了.重构量不小,最核心的一点是必须保证原有的代码不受影响.也就是说,接下来两周我要边写单元测试用例,边重构代码.期间遇到的关于测试的问题与坑,会及时记录下来,汇总交流.

ReactiveCocoa实战: 模仿 花瓣,重写 LeanCloud Rest Api的iOS REST Client.相关推荐

  1. 视频教程-从零开发一个iOS企业级项目实战之我的云音乐视频 教程-iOS

    从零开发一个iOS企业级项目实战之我的云音乐视频 教程 任苹蜻,爱学啊创始人 & CEO,曾就职于某二车手公司担任Android工程师后离职创办爱学啊,我们的宗旨是:人生苦短,我们只做好课!熟 ...

  2. 全球地区资料json 含中英文 经纬度_爬虫实战(三)使用百度API获取经纬度/地址...

    点击上方"蓝字"关注我们百度API获取经纬度/地址Mar 28, 2020 本期介绍给定地址/经纬度,使用百度API来获取经纬度/地址. 本文约3k字,预计阅读18分钟. 本次是第 ...

  3. 【Ids4实战】分模块保护资源API

    (毕竟西湖六月中) 书接上文,上回书咱们说到了IdentityServer4(下文统称Ids4)官方已经从v3更新升级到了v4版本,我的Blog.Idp项目也做了同步更新,主要是针对快速启动UI做的对 ...

  4. java search 不能使用方法_ElasticSearch实战系列三: ElasticSearch的JAVA API使用教程

    前言 在上一篇中介绍了ElasticSearch实战系列二: ElasticSearch的DSL语句使用教程---图文详解,本篇文章就来讲解下 ElasticSearch 6.x官方Java API的 ...

  5. Zabbix分布式监控实战(2)—— Zabbix的API接口的使用方法

    本实验是在<Zabbix分布式监控实战(1)--Zabbix简介及Zabbix监控平台的搭建>实验基础上进行的,已经配置好了zabbix-server和zabbix-agent主机,并在z ...

  6. Canvas实战---模仿GOOGLE浮动小球效果

    看到基于Canvas动画的Google浮动小球效果,非常炫,决定自己尝试模仿着做一个. Demo:http://qs20199.github.io/SuspendingBall/ 这个Demo并不难, ...

  7. 深度学习实战(七)——目标检测API训练自己的数据集(R-FCN数据集制作+训练+测试)

    TensorFlow提供的网络结构的预训练权重:https://cloud.tencent.com/developer/article/1006123 将voc数据集转换成.tfrecord格式供te ...

  8. 【Web API系列教程】1.3 — 实战:用ASP.NET Web API和Angular.js创建单页面应用程序(上)

    前言 在传统的web应用程序中,客户端(浏览器)通过请求页面来启动与服务器的通信.然后服务器处理该请求,并发送HTML页面到客户端.在随后页面上的操作中--例如,用户导航到一个链接或提交一个包含数据的 ...

  9. 实战基于Docker部署FLASK后端api并使用云托管服务

    上篇文章我们讲了怎么用docker部署nginx应用网站,这篇文章我们就来部署我们的后端api.我们这次尝试在服务器和微信云托管都进行部署,部署于云托管也是现在个人开发者的另一种选择(个人觉得啊)不用 ...

最新文章

  1. python中关于sqlite3数据库插入数据的使用
  2. 大型高并发高负载网站的系统架构(转)
  3. 化整为零,一步一步教你搭建Prometheus监控报警系统
  4. 编程题走迷宫_C++程序算法题----迷宫(一)
  5. xml转换为json格式时,如何将指定节点转换成数组 Json.NET
  6. Keil(MDK-ARM-STM32)系列教程(三)工程目标选项配置(Ⅰ)
  7. 鼠标在某个控件上按下,然后离开后弹起,如何捕获这个鼠标弹起事件
  8. eclipse-Tomcat运行项目笔记
  9. URL带中文参数的解决方法FR.cjkEncode()
  10. 烟花散尽漫说无(参考资料)
  11. GCP Marker生成的刺点文件导入Pix4D教程
  12. 解决This application failed to start because no Qt platform plugin could be initialized的问题
  13. 阿里云手机验证码注册(可以使用阿里云提供的测试模板,不用个人申请)
  14. 2022 年面向初学者的 10 大免费 3D 建模软件
  15. VS 报错“无法解析的外部符号” 情况1
  16. java csvwriter 追加_CSV文件每行的末尾追加写数据
  17. python读取歌词文本,并显示在图片上,制作视频
  18. python1到100奇数和_python计算1~100的和,1~100奇数的和,1~100偶数的和,一条代码求1~100的和...
  19. 多台服务器连一个显示器如何切换,多台主机一台显示器怎么弄
  20. 德州学院计算机专业怎样,2017德州学院各专业录取分数线

热门文章

  1. 存储论及经济订货批量模型(EOQ)
  2. EOS系列 - 超级节点(BP)列表更新流程
  3. 2022年监理工程师质量/投资/进度控制考试每日一练及答案
  4. 如何将视频生成gif?学会这招视频转gif快速实现
  5. Linux(强大的yum命令)
  6. 操作系统的发展和分类
  7. 【C语言编程】青蛙爬井问题
  8. iRedmail搭建 集成AD以进行身份验证和地址簿
  9. 消费级|工业级|军工级 IMX6Q 核心板
  10. 百度AI识别图片文字