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

完整代码示例下载: github

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

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

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

/* 要实现的逻辑很简单:
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:jsonArray
options:NSJSONWritingPrettyPrinted
error:NULL]; NSString * jsonString = [[NSString alloc] initWithData:jsonData
encoding: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. List多个字段标识过滤 IIS发布.net core mvc web站点 ASP.NET Core 实战:构建带有版本控制的 API 接口 ASP.NET Core 实战:使用 ASP.NET Core Web API 和 Vue.js 搭建前后端分离项目 Using AutoFac

    List多个字段标识过滤 class Program{  public static void Main(string[] args) { List<T> list = new List& ...

  2. 快速零配置迁移 API 适配 iOS 对 IPv6 以及 HTTPS 的要求

    本文快速分享一下快速零配置迁移 API 适配 iOS 对 IPv6 以及 HTTPS 的要求的方法,供大家参考. 原文发表于我的技术博客 零配置方案 最新的苹果审核政策对 API 的 IPv6 以及 ...

  3. 使用ASP.NET Web Api构建基于REST风格的服务实战系列教程【九】——API变了,客户端怎么办?

    系列导航地址http://www.cnblogs.com/fzrain/p/3490137.html 前言 一旦我们将API发布之后,消费者就会开始使用并和其他的一些数据混在一起.然而,当新的需求出现 ...

  4. 个性化推荐调优:重写spark推荐api

    最近用spark的mlib模块中的协同过滤库做个性化推荐.spark里面用的是als算法,本质上是矩阵分解svd降维,把一个M*N的用户商品评分矩阵分解为M*K的userFeature(用户特征矩阵) ...

  5. ASP.NET Core 实战:构建带有版本控制的 API 接口

    一.前言 在上一篇的文章中,主要是搭建了我们的开发环境,同时创建了我们的项目模板框架.在整个前后端分离的项目中,后端的 API 接口至关重要,它是前端与后端之间进行沟通的媒介,如何构建一个 “好用” ...

  6. 「小程序JAVA实战」小程序和后台api通信(28)

    转自:https://idig8.com/2018/08/19/xiaochengxujavashizhanxiaochengxuhehoutaiapitongxin28/ 开发最重要的就是实操! 小 ...

  7. Go实战--通过gin-gonic框架搭建restful api服务(github.com/gin-gonic/gin)

    生命不止,继续 go go go !!! 先插播一条广告,给你坚持学习golang的理由: <2017 软件开发薪酬调查:Go 和 Scala 是最赚钱的语言> 言归正传! 之前写过使用g ...

  8. SpringBoot实战(二)Restful风格API接口

    在上一篇SpringBoot实战(一)HelloWorld的基础上,编写一个Restful风格的API接口: 1.根据MVC原则,创建一个简单的目录结构,包括controller和entity,分别创 ...

  9. 《ElasticSearch6.x实战教程》之简单的API

    第三章-简单的API 万丈高楼平地起 ES提供了多种操作数据的方式,其中较为常见的方式就是RESTful风格的API. 简单的体验 利用Postman发起HTTP请求(当然也可以在命令行中使用curl ...

随机推荐

  1. Broken Keyboard (a.k.a. Beiju Text) UVA - 11988 (链表)

    题目链接:https://vjudge.net/problem/UVA-11988 题目大意:输入一个字符串,输出在原本应该是怎么样的?  具体方法是 碰到' [ ' 回到最前面  碰到‘ ]’  回 ...

  2. 用spring的 InitializingBean 的 afterPropertiesSet 来初始化

    void afterPropertiesSet() throws Exception; 这个方法将在所有的属性被初始化后调用. 但是会在init前调用. 但是主要的是如果是延迟加载的话,则马上执行. ...

  3. java 多线程 yield方法的意义

    Thread.yield( )方法: 使当前线程从执行状态(运行状态)变为可执行态(就绪状态).cpu会从众多的可执行态里选择,也就是说,当前也就是刚刚的那个线程还是有可能会被再次执行到的,并不是说一 ...

  4. java连接数据库驱动代码综合共享

    1.Oracle8/8i/9i数据库(thin模式)Class.forName("oracle.jdbc.driver.OracleDriver").newInstance();S ...

  5. POJ 2112—— Optimal Milking——————【多重匹配、二分枚举答案、floyd预处理】

    Optimal Milking Time Limit:2000MS     Memory Limit:30000KB     64bit IO Format:%I64d & %I64u Sub ...

  6. 部署Cube报错,用户登录失败;280000

    在创建SSAS项目过程中,创建数据源.数据源视图.多维数据集.纬度等一切都没有问题.但是在“进程”这一步的时候,发现总是报错,提示如下. OLE DB 错误: OLE DB 或 ODBC 错误 : 用 ...

  7. DIV三列同行

    <!doctype html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  8. Spring文件下载

    package com.smbea.demo.controller; import java.io.BufferedInputStream; import java.io.BufferedOutput ...

  9. oracle 递归查询(来源于网络)

    比如 a   b a   c   a   e b   b1 b   b2 c   c1 e   e1 e   e3 d   d1 指定parent=a,选出 a   b a   c   a   e b ...

  10. window系统安装jdk,jre

    java开发少不了安装jdk.当然如果只是想运行其他人的java项目,只需要安装jre就行了,不需要安装jdk,jdk是编译用的.jdk可以同时安装多个 版本,只需要在项目部署时注意切换版本选择.在这 ...