YTKNetwork介紹
YTKNetwork 是猿題庫 iOS 研發(fā)團隊基于 AFNetworking 封裝的 iOS 網(wǎng)絡(luò)庫匣吊,其實現(xiàn)了一套 High Level 的 API,提供了更高層次的網(wǎng)絡(luò)訪問抽象蛋辈。目前在 GitHub 上已有 3600+ star 桶错,是 Network 中的新星航唆。
YTKNetwork提供的主要功能
- 支持按時間緩存和版本號緩存網(wǎng)絡(luò)請求內(nèi)容
- 支持統(tǒng)一設(shè)置服務(wù)器和 CDN 的地址
- 支持檢查返回 JSON 內(nèi)容的合法性
- 支持
block
和delegate
兩種模式的回調(diào)方式 - 支持批量的網(wǎng)絡(luò)請求發(fā)送,并統(tǒng)一設(shè)置它們的回調(diào)(實現(xiàn)在
YTKBatchRequest
類中) - 支持方便地設(shè)置有相互依賴的網(wǎng)絡(luò)請求的發(fā)送院刁,例如:發(fā)送請求 A糯钙,根據(jù)請求 A 的結(jié)果,選擇性的發(fā)送請求 B 和 C退腥,再根據(jù) B 和 C 的結(jié)果任岸,選擇性的發(fā)送請求 D。(實現(xiàn)在
YTKChainRequest
類中) - 支持網(wǎng)絡(luò)請求 URL 的 filter狡刘,可以統(tǒng)一為網(wǎng)絡(luò)請求加上一些參數(shù)享潜,或者修改一些路徑。
- 定義了一套插件機制嗅蔬,可以很方便地為 YTKNetwork 增加功能剑按。猿題庫官方現(xiàn)在提供了一個插件,可以在某些網(wǎng)絡(luò)請求發(fā)起時澜术,在界面上顯示“正在加載”的 HUD艺蝴。
YTKNetwork 的基本思想
YTKNetwork 的基本的思想是把每一個網(wǎng)絡(luò)請求封裝成對象。所以使用 YTKNetwork鸟废,你的每一個請求都需要繼承 YTKRequest 類猜敢,通過覆蓋父類的一些方法來構(gòu)造指定的網(wǎng)絡(luò)請求。
把每一個網(wǎng)絡(luò)請求封裝成對象其實是使用了設(shè)計模式中的 Command 模式,它有以下好處:
- 將網(wǎng)絡(luò)請求與具體的第三方庫依賴隔離锣枝,方便以后更換底層的網(wǎng)絡(luò)庫厢拭。
- 方便在基類中處理公共邏輯,例如猿題庫的數(shù)據(jù)版本號信息就統(tǒng)一在基類中處理撇叁。
- 方便在基類中處理緩存邏輯供鸠,以及其它一些公共邏輯。
方便做對象的持久化陨闹。
當然楞捂,如果說它有什么不好,那就是如果你的工程非常簡單趋厉,這么寫會顯得沒有直接用 AFNetworking
將請求邏輯寫在 Controller 中方便寨闹,所以 YTKNetwork 并不合適特別簡單的項目。
關(guān)于集約式和離散式
集約式
介紹:即項目中的每個請求都會走統(tǒng)一的入口君账,對外暴露了請求的 URL 和 Param 以及請求方式繁堡,入口一般都是通過單例
來實現(xiàn),AFNetworking 的官方 demo 就是采用的集約式的方式對網(wǎng)絡(luò)請求進行的封裝乡数,也是目前比較流行的網(wǎng)絡(luò)請求方式椭蹄。
優(yōu)點:
- 使用便捷,能實現(xiàn)快速開發(fā)
缺點:
- 對每個請求的定制型不夠強
- 不方便后期業(yè)務(wù)拓展
離散式
介紹:即每個網(wǎng)絡(luò)請求類都是一個對象净赴,它的 URL 以及請求方式和響應(yīng)方式 均不暴露給外部調(diào)用绳矩。只能內(nèi)部通過 重載或?qū)崿F(xiàn)協(xié)議
的方式來指定,外部調(diào)用只需要傳 Param 即可玖翅,YTKNetwork就是采用的這種網(wǎng)絡(luò)請求方式翼馆。
優(yōu)點:
- URL 以及請求和響應(yīng)方式不暴露給外部,避免外部調(diào)用的時候?qū)戝e
- 業(yè)務(wù)方使用起來較簡單金度,業(yè)務(wù)使用者不需要去關(guān)心它的內(nèi)部實現(xiàn)
- 可定制性強应媚,可以為每個請求指定請求的超時時間以及緩存的周期
缺點:
- 網(wǎng)絡(luò)層需要業(yè)務(wù)實現(xiàn)方去寫,變相的增加了部分工作量
- 文件增多审姓,程序包會變大[倒也不是特別大]
在微脈的iOS客戶端珍特,由于最初人員較少,且業(yè)務(wù)變更較頻繁魔吐。故使用的就是集約式請求扎筒。不過考慮到為實現(xiàn)業(yè)務(wù)便捷性以及可拓展性,故增加了 RequestHeader
請求頭酬姆,以及 WMHttpHelper
網(wǎng)絡(luò)操作工具類嗜桌。基本上已滿足于目前的開發(fā)模式
不過長遠來看辞色,轉(zhuǎn)成離散式的網(wǎng)絡(luò)請求也是有必要的骨宠。
安裝
你可以在 Podfile 中加入下面一行代碼來使用 YTKNetwork
pod 'YTKNetwork'
集成至項目
項目文件介紹
YTKBaseRequest:為請求的基類,內(nèi)部聲明了請求的常用 API :
比如請求方式,請求解析方式层亿,響應(yīng)解析方式桦卒,請求參數(shù)等等。它的用意是讓子類去實現(xiàn)的匿又,本身不做實現(xiàn)方灾。
YTKRequest:是 YTKBaseRequest
的子類,在其基礎(chǔ)上支持了緩存碌更,并且提供了豐富的緩存策略裕偿。基本上項目中使用都是繼承于 YTKRequest
去寫業(yè)務(wù)的 Request痛单。
YTKNetworkAgent:真正做網(wǎng)絡(luò)請求的類嘿棘,在內(nèi)部跟 AFNetworking
直接交互,調(diào)用了 AFNetworking
提供的各種請求旭绒,當然鸟妙,如果底層想切換其他第三方,在這個類中替換掉就行了挥吵。
YTKNetworkConfig:該文件為網(wǎng)絡(luò)請求的統(tǒng)一配置類圆仔,提供了設(shè)置 baseUrl
cdnUrl
等基礎(chǔ)請求路徑,可以給所有的請求增加參數(shù)等等蔫劣。
YTKBatchRequest:為批量進行網(wǎng)絡(luò)請求而生,提供了代理和 block 兩種方式給外部使用
YTKChainRequest:當多個請求之間有關(guān)聯(lián)的時候采用此類去實現(xiàn)非常方便个从,即下一個請求可能要根據(jù)上個請求返回的數(shù)據(jù)進行請求脉幢。
YTKBatchRequestAgent,YTKChainRequestAgent:分別是 YTKBatchRequest
嗦锐,YTKChainRequest
的操作類嫌松,不需要也無妨主動調(diào)用
集成文件介紹
這是 Demo 工程的我新增的文件,一般情況下奕污,不建議直接繼承于 YTKRequest
類去寫業(yè)務(wù)萎羔,需要自己寫請求的基類,具體業(yè)務(wù)請求再繼承于改項目基類碳默,避免因新版本 YTKRequest 中修改了部分實現(xiàn)的默認值導(dǎo)致的程序需要做大量的修改贾陷。其中:ZCBaseRequest
,ZCBatchRequest
嘱根,ZCChainRequest
就是 demo 項目的基類髓废。ZCJSONModel
是 JSON 轉(zhuǎn) Model 的基類,而 ZCHTTPError
是用于自定義錯誤信息的
這是 Demo 工程中具體某個請求的實例该抒。這種展現(xiàn)方式很清晰慌洪,ZCGetInfoParam
是請求的入?yún)㈩悾?code>ZCMeGetInfoManger是具體的請求操作類, ZCGetInfoModel
是出參類。不過如果入?yún)⒑统鰠⒑苌俑缘梢灾挥幸粋€manger類
相關(guān)問題思考
我這里不想介紹 YTKNetwork
的基礎(chǔ)和高級使用教程涌攻。如果想了解基礎(chǔ)以及高級使用教程可以看這里
YTKNetwork 使用基礎(chǔ)教程
YTKNetwork 使用高級教程
這篇文章重在介紹集成以及使用過程中遇到一些問題以及解決方案
1>JSON轉(zhuǎn)Model的問題
對于稍微復(fù)雜的項目,可能某些接口返回數(shù)據(jù)有十多個频伤,使用的時候不可能從字典中一個一個讀取出來恳谎,然后再做 <null>
空處理,一般都是采用轉(zhuǎn) Model 的方式 轉(zhuǎn)換成具體的業(yè)務(wù)模型剂买,從業(yè)務(wù)模型中獲取具體數(shù)據(jù)惠爽,常見的有 JSONModel
,Mantle
瞬哼,MJExtension
等第三方庫婚肆,本文以JSONModel
為例,來實現(xiàn)框架內(nèi)部解析成 Model
- 在
YTKBaseRequest
新增 JSONModel 屬性
/// JsonModel類
@property (nonatomic, strong, readonly, nullable) id responseJSONModel;
- 在
YTKBaseRequest
新增 modelClass 函數(shù)坐慰,用于子類去實現(xiàn)较性,表明要轉(zhuǎn)換的具體 model 類的類名
YTKBaseRequest.h
/// model對應(yīng)的類,子類實現(xiàn)的話會直接映射到該model類并進行初始化操作
- (Class)modelClass;
YTKBaseRequest.m
- (Class)modelClass
{
return nil;
}
- 查看源碼不難發(fā)現(xiàn)结胀,真正處理網(wǎng)絡(luò)請求成功和失敗的地方是
YTKNetworkAgent
類赞咙,在- (void)handleRequestResult:(NSURLSessionTask *)task responseObject:(id)responseObject error:(NSError *)error
和- (void)requestDidSucceedWithRequest:(YTKBaseRequest *)request
,- (void)requestDidFailWithRequest:(YTKBaseRequest *)request error:(NSError *)error
這三個方法糟港。
具體操作為
- (void)requestDidSucceedWithRequest:(YTKBaseRequest *)request {
@autoreleasepool {
[request requestCompletePreprocessor];
[self JSONConvertModel:request];
}
dispatch_async(dispatch_get_main_queue(), ^{
[request toggleAccessoriesWillStopCallBack];
[request requestCompleteFilter];
if (request.delegate != nil) {
[request.delegate requestFinished:request];
}
if (request.successCompletionBlock) {
request.successCompletionBlock(request);
}
[request toggleAccessoriesDidStopCallBack];
});
}
///json轉(zhuǎn)model的具體方法
- (void)JSONConvertModel:(YTKBaseRequest*)request
{
Class modelClass = [request modelClass];
if (!modelClass) {
return;
}
NSError * error = nil;
if ([request.responseJSONObject isKindOfClass:[NSDictionary class]]) {
request.responseJSONModel = [[modelClass alloc] initWithDictionary:request.responseJSONObject error:&error];
}else if ([request.responseJSONObject isKindOfClass:[NSArray class]]){
request.responseJSONModel = [modelClass arrayOfModelsFromDictionaries:request.responseJSONObject error:&error];
}else if {
//這里不做處理攀操,因為AFNetworking如果返回的數(shù)據(jù)為null的時候會調(diào)用失敗的回調(diào)
}
if (error) {
YTKLog(@"Request JSON---JSONModel Failed =%@",error);
}
}
- 在
YTKRequest
類中也需要新增緩存類的model,具體代碼為
YTKRequest.m
@property (nonatomic, strong) id cacheJSONModel;///TTT
- (id)responseJSONModel {
if (_cacheJSONModel) {
return _cacheJSONModel;
}
return [super responseJSONModel];
}
- (BOOL)loadCacheData {
NSString *path = [self cacheFilePath];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *error = nil;
if ([fileManager fileExistsAtPath:path isDirectory:nil]) {
NSData *data = [NSData dataWithContentsOfFile:path];
_cacheData = data;
_cacheString = [[NSString alloc] initWithData:_cacheData encoding:self.cacheMetadata.stringEncoding];
switch (self.responseSerializerType) {
case YTKResponseSerializerTypeHTTP:
// Do nothing.
return YES;
case YTKResponseSerializerTypeJSON:
_cacheJSON = [NSJSONSerialization JSONObjectWithData:_cacheData options:(NSJSONReadingOptions)0 error:&error];
if (!error) {
[self JSONConvertModel:_cacheJSON];
}
return error == nil;
case YTKResponseSerializerTypeXMLParser:
_cacheXML = [[NSXMLParser alloc] initWithData:_cacheData];
return YES;
}
}
return NO;
}
- (void)JSONConvertModel:(YTKBaseRequest*)request
{
///跟第二步的實現(xiàn)方式一樣
}
- 到這里基本上已經(jīng)實現(xiàn)了 json-model秸抚,具體的業(yè)務(wù)代碼為:
- (void)loadCacheData {
NSString *userId = @"1";
GetUserInfoApi *api = [[GetUserInfoApi alloc] initWithUserId:userId];
if ([api loadCacheWithError:nil]) {
NSDictionary *json = [api responseJSONObject];
NSLog(@"json = %@", json);
// show cached data
YTKJSONModel * model = [api responseJSONModel];
NSLog(@"jsonmodelllll=%@---%@",model.nick,model.level);
}
api.animatingText = @"正在加載";
api.animatingView = self.view;
[api startWithCompletionBlockWithSuccess:^(YTKBaseRequest *request) {
NSLog(@"update ui=%@",[api responseJSONModel]);
} failure:^(YTKBaseRequest *request) {
NSLog(@"failed");
}];
}
大致步驟如此速和,只是這樣實現(xiàn)的話需要修改源代碼,細節(jié)可以參考 demo剥汤,地址:https://github.com/albertjson/YTKNetwork
2>token引發(fā)的問題
一般情況下颠放,網(wǎng)絡(luò)請求客戶端都要帶 token,用于服務(wù)端驗證用戶的登陸有效性吭敢。那么 token 失效可能需要做一些處理碰凶,在 demo 中這部分驗證是寫在 ZCBaseRequest
類中實現(xiàn)的。這樣避免業(yè)務(wù)代碼在各處進行處理 token 失效的情況
- (void)requestFailedFilter
{
[super requestFailedFilter];
if (error.code==TokenTimeOut) {
......
}
}
當然鹿驼,這樣處理之后欲低,如果子類需要在錯誤的時候做特殊處理,那么在重寫 requestFailedFilter
方法的時候一定要調(diào)用 [super requestFailedFilter]
3>錯誤解析
YTKNetwork 調(diào)用 HTTP 返回錯誤的類為 NSError蠢沿。而自己的項目一般都需要定制錯誤信息伸头,或者根據(jù)某一類型的錯誤進行特殊的操作。這一步可以在自己定義的請求基類的錯誤回調(diào)中處理舷蟀。我們先來看一段 YTKNetwork 的源碼:
- (void)handleRequestResult:(NSURLSessionTask *)task responseObject:(id)responseObject error:(NSError *)error {
//這里只留下關(guān)鍵性代碼
NSError * __autoreleasing serializationError = nil;
NSError * __autoreleasing validationError = nil;
NSError *requestError = nil;
BOOL succeed = NO;
request.responseObject = responseObject;
if ([request.responseObject isKindOfClass:[NSData class]]) {
request.responseData = responseObject;
request.responseString = [[NSString alloc] initWithData:responseObject encoding:[YTKNetworkUtils stringEncodingWithRequest:request]];
switch (request.responseSerializerType) {
case YTKResponseSerializerTypeHTTP:
// Default serializer. Do nothing.
break;
case YTKResponseSerializerTypeJSON:
request.responseObject = [self.jsonResponseSerializer responseObjectForResponse:task.response data:request.responseData error:&serializationError];
request.responseJSONObject = request.responseObject;
break;
case YTKResponseSerializerTypeXMLParser:
request.responseObject = [self.xmlParserResponseSerialzier responseObjectForResponse:task.response data:request.responseData error:&serializationError];
break;
}
}
if (error) {
succeed = NO;
requestError = error;
} else if (serializationError) {
succeed = NO;
requestError = serializationError;
} else {
succeed = [self validateResult:request error:&validationError];
requestError = validationError;
}
//只留關(guān)鍵性代碼
}
不難發(fā)現(xiàn)恤磷,這里的錯誤其實分三類
- requestError:請求錯誤面哼,為 AFNetworking 進行網(wǎng)絡(luò)請求的請求錯誤,比如說沒網(wǎng)絡(luò)扫步。
- serializationError:響應(yīng)錯誤魔策,為 AFNetworking 響應(yīng)錯誤,比如返回的json數(shù)據(jù)你卻用了xml解析河胎,還有很多情況等等闯袒。
-
validationError:校驗 json 錯誤,這里包括
[request statusCodeValidator]
和[request jsonValidator
兩種類型的錯誤游岳,前者為返回的 statusCode 不在你指定的成功請求區(qū)間內(nèi)政敢,后者為返回的 json 數(shù)據(jù) 跟你重載的 jsonValidator 函數(shù)中存在字段不一致的情況。 - [可選] 如果你用 JSONModel 還會有 JSONModel 解析錯誤產(chǎn)生的錯誤胚迫。
處理方式如下:
//這里暫不考慮JSONModel解析錯誤的問題
- (void)requestFailedPreprocessor
{
//note:子類如需繼承喷户,必須必須調(diào)用 [super requestFailedPreprocessor];
[super requestFailedPreprocessor];
NSError * error = self.error;
if ([error.domain isEqualToString:AFURLResponseSerializationErrorDomain])
{
//AFNetworking處理過的錯誤
}else if ([error.domain isEqualToString:YTKRequestValidationErrorDomain])
{
//猿題庫處理過的錯誤
}else{
//系統(tǒng)級別的domain錯誤,無網(wǎng)絡(luò)等[NSURLErrorDomain]
//根據(jù)error的code去定義顯示的信息访锻,保證顯示的內(nèi)容可以便捷的控制
}
}
這里還有一種特殊情況褪尝,就是服務(wù)端返回的錯誤不一定是以 錯誤
的方式給你∑谌可能請求狀態(tài)碼依然是200OK河哑,那么這個時候需要重寫 YTK 提供的成功和失敗的block和重寫代理
4>loading動畫以及錯誤彈出機制
YTK自帶了一套插件機制,用于處理 YTKBaseRequest
龟虎,YTKBatchRequest
璃谨,YTKChainRequest
這幾種請求的loading展示機制,只需要傳入 animatingView
和 animatingText
即可鲤妥。對于彈出統(tǒng)一的錯誤提示睬罗,可以在 ZCBaseRequest
的失敗主線程回調(diào)中進行。即:
/// Called on the main thread when request failed.
- (void)requestFailedFilter
{
[super requestFailedFilter];
if (![self isHideErrorToast]) {
UIWindow * window = [[UIApplication sharedApplication] keyWindow];
UIViewController * controller = [self findBestViewController:window.rootViewController];
[WMHUDUntil showFailWithMessage:self.error.localizedDescription toView:controller.view];
}
}
其中 [self isHideErrorToast]
用于表示是否隱藏錯誤提示旭斥。該方法由具體的子類去實現(xiàn)。
5>網(wǎng)絡(luò)請求的終止
YTK給出網(wǎng)絡(luò)請求關(guān)閉方案:在dealloc中調(diào)用:
Remove self from request queue and cancel the request.
- (void)stop;
所以古涧,建議每個網(wǎng)絡(luò)請求都在controller寫成全局的變量垂券。
下面展示一下具體某個請求的代碼:
ZCTYKTestViewController.m
@interface ZCTYKTestViewController ()
@property (nonatomic,strong) ZCMeGetInfoManger * infoManger;
@end
@implementation ZCTYKTestViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self setupData];
}
- (void)setupData
{
self.infoManger = [[ZCMeGetInfoManger alloc] init];
self.infoManger.animatingView = self.view;
}
- (IBAction)buttonAction:(UIButton*)sender
{
[self clearTextView];
ZCGetInfoParam * param = [[ZCGetInfoParam alloc] init];
param.userId = @"0";
param.token = @"222222";
_infoManger.param = param;
[_infoManger startWithCompletionBlockWithSuccess:^(__kindof YTKBaseRequest * _Nonnull request) {
NSLog(@"responseJSONObject=%@",_infoManger.responseJSONObject);
ZCGetInfoModel * infoModel = [[ZCGetInfoModel alloc] initWithDictionary:_infoManger.responseJSONObject error:nil];
[self updateTextViewWithLog:[NSString stringWithFormat:@"讀取數(shù)據(jù):\n%@",infoModel]];
} failure:^(__kindof YTKBaseRequest * _Nonnull request) {
[weakself updateTextViewWithLog:[NSString stringWithFormat:@"讀取失敗:\n%@",weakself.infoManger.error]];
}];
}
這是我個人總結(jié)的這幾點,如果有更好的方案也可以跟我一起探討