ReactiveCocoa实战: 模仿 花瓣,重写 LeanCloud Rest Api的iOS REST Client.
这一次我们将要讨论的是移动开发中比较重要的一环–网络请求的封装.鉴于个人经验有限,本文将在一定程度上参考 基于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.相关推荐
- 视频教程-从零开发一个iOS企业级项目实战之我的云音乐视频 教程-iOS
从零开发一个iOS企业级项目实战之我的云音乐视频 教程 任苹蜻,爱学啊创始人 & CEO,曾就职于某二车手公司担任Android工程师后离职创办爱学啊,我们的宗旨是:人生苦短,我们只做好课!熟 ...
- 全球地区资料json 含中英文 经纬度_爬虫实战(三)使用百度API获取经纬度/地址...
点击上方"蓝字"关注我们百度API获取经纬度/地址Mar 28, 2020 本期介绍给定地址/经纬度,使用百度API来获取经纬度/地址. 本文约3k字,预计阅读18分钟. 本次是第 ...
- 【Ids4实战】分模块保护资源API
(毕竟西湖六月中) 书接上文,上回书咱们说到了IdentityServer4(下文统称Ids4)官方已经从v3更新升级到了v4版本,我的Blog.Idp项目也做了同步更新,主要是针对快速启动UI做的对 ...
- java search 不能使用方法_ElasticSearch实战系列三: ElasticSearch的JAVA API使用教程
前言 在上一篇中介绍了ElasticSearch实战系列二: ElasticSearch的DSL语句使用教程---图文详解,本篇文章就来讲解下 ElasticSearch 6.x官方Java API的 ...
- Zabbix分布式监控实战(2)—— Zabbix的API接口的使用方法
本实验是在<Zabbix分布式监控实战(1)--Zabbix简介及Zabbix监控平台的搭建>实验基础上进行的,已经配置好了zabbix-server和zabbix-agent主机,并在z ...
- Canvas实战---模仿GOOGLE浮动小球效果
看到基于Canvas动画的Google浮动小球效果,非常炫,决定自己尝试模仿着做一个. Demo:http://qs20199.github.io/SuspendingBall/ 这个Demo并不难, ...
- 深度学习实战(七)——目标检测API训练自己的数据集(R-FCN数据集制作+训练+测试)
TensorFlow提供的网络结构的预训练权重:https://cloud.tencent.com/developer/article/1006123 将voc数据集转换成.tfrecord格式供te ...
- 【Web API系列教程】1.3 — 实战:用ASP.NET Web API和Angular.js创建单页面应用程序(上)
前言 在传统的web应用程序中,客户端(浏览器)通过请求页面来启动与服务器的通信.然后服务器处理该请求,并发送HTML页面到客户端.在随后页面上的操作中--例如,用户导航到一个链接或提交一个包含数据的 ...
- 实战基于Docker部署FLASK后端api并使用云托管服务
上篇文章我们讲了怎么用docker部署nginx应用网站,这篇文章我们就来部署我们的后端api.我们这次尝试在服务器和微信云托管都进行部署,部署于云托管也是现在个人开发者的另一种选择(个人觉得啊)不用 ...
最新文章
- python中关于sqlite3数据库插入数据的使用
- 大型高并发高负载网站的系统架构(转)
- 化整为零,一步一步教你搭建Prometheus监控报警系统
- 编程题走迷宫_C++程序算法题----迷宫(一)
- xml转换为json格式时,如何将指定节点转换成数组 Json.NET
- Keil(MDK-ARM-STM32)系列教程(三)工程目标选项配置(Ⅰ)
- 鼠标在某个控件上按下,然后离开后弹起,如何捕获这个鼠标弹起事件
- eclipse-Tomcat运行项目笔记
- URL带中文参数的解决方法FR.cjkEncode()
- 烟花散尽漫说无(参考资料)
- GCP Marker生成的刺点文件导入Pix4D教程
- 解决This application failed to start because no Qt platform plugin could be initialized的问题
- 阿里云手机验证码注册(可以使用阿里云提供的测试模板,不用个人申请)
- 2022 年面向初学者的 10 大免费 3D 建模软件
- VS 报错“无法解析的外部符号” 情况1
- java csvwriter 追加_CSV文件每行的末尾追加写数据
- python读取歌词文本,并显示在图片上,制作视频
- python1到100奇数和_python计算1~100的和,1~100奇数的和,1~100偶数的和,一条代码求1~100的和...
- 多台服务器连一个显示器如何切换,多台主机一台显示器怎么弄
- 德州学院计算机专业怎样,2017德州学院各专业录取分数线