国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學院 > 開發設計 > 正文

ReactiveCocoa實戰:模仿"花瓣",重寫LeanCloudRestApi的iOSRESTClient.

2019-11-14 18:02:23
字體:
來源:轉載
供稿:網友

效果圖

這一次我們將要討論的是移動開發中比較重要的一環--網絡請求的封裝.鑒于個人經驗有限,本文將在一定程度上參考 基于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后臺

模仿 "花瓣",重寫 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的分析,僅供大家作為技術實現上的一個參考.具體到自己的業務細節,可能有些地方,需要特殊處理.關于以上技術討論的問題,歡迎跟帖討論!

下一篇主題,會對單元測試的一些細節做一分析.邊摸索邊學習,總算真到了一個合適的重構我們已有工程的策略了.重構量不小,最核心的一點是必須保證原有的代碼不受影響.也就是說,接下來兩周我要邊寫單元測試用例,邊重構代碼.期間遇到的關于測試的問題與坑,會及時記錄下來,匯總交流.


發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 米脂县| 明光市| 万年县| 新兴县| 鄂托克前旗| 汉源县| 蒙山县| 卢湾区| 黎平县| 依安县| 策勒县| 新巴尔虎左旗| 灌云县| 文安县| 盘山县| 鄂温| 芦山县| 大连市| 隆回县| 晋中市| 淮阳县| 鄂伦春自治旗| 永嘉县| 瓮安县| 临夏县| 长子县| 九台市| 浦江县| 东明县| 泸水县| 金山区| 手机| 治多县| 英超| 佛冈县| 吴江市| 冷水江市| 平度市| 田阳县| 蒙阴县| 永善县|