前言
在读这篇文章之前,大家先看一下作者Casa的文章,以便对CTNetwokring的整体设计以及使用方式有个大体的了解。
其次这次改造,主要由需求驱动,因为没有以下这些问题,我也不会做这样的尝试:
- 公司的网络库使用虽然很方便,依赖性太强,不方便测试,属于集约型的做法,导致公司不太可能写单元测试,所以没有写单元测试,又由于公司后台经常私底下改接口,导致线上版本经常出现问题。
- CTNetworking刚出来的时候我就关注了,苦于放出来的版本,适合学习,并不合适即插即用,我一直在等有人来改造,结果两年过去了。
- 由于那是早年安居客的脱敏版,所以设计上面就或多或少有一些依赖业务的东西,建立在它们业务上,的确是最合适方便的,但也由于太合适自身业务,那么也导致无法通用化,其他公司想拿来用就必须改,对于这一点,我在这里要建议强烈批评一下Casa,开源这么好的东西出来,怎么就不弄得更方便一点呢,像CTPersistance(安利一下)那样子插上就能用多好。
- 我想要开始走向开源社区(不要脸地说,我想涨点粉),在写这个文章前,由于公司项目API一两百个,真是没有余力做短期替换,所以我所提供的那部分代码还没有真正的得到真实项目的考量,另一方面基于我自己不会写后端接口,公司内容涉密问题也导致了没法给出更多完整的使用方式,本文给出的将是我的一套YY方案,欢迎评论区拍砖。
改造篇
如上面提到的,原来的CTNetworking之所以不能做到即插即用,主要有以下几点:
- 有旧业务的依赖,比如token的拼接,url组装啊。
- 涉及面略广,作为一个网络封装层,提供了CTUDIDGenerator等UUID生成器,CTAPPContext致使引入一些不必要的业务。
- ServiceFactory需要硬编码,他需要知道详细的业务Service,而业务的Service硬编码无疑就使得他变得不通用。
谈到要解决上面的问题,那么就有必要来讲解一下CTNetworking的基本组成设计了,这是Casa没有在他文章里面讲的东西,打个比方,比如有这么一个接口,使用CTNetworking的视角来说,它是下面这样的:
1 2 3 4 5 6 7 8 9
| https: 拆解: https: /method => CTAPIBaseManager的子类提供 username=corotata => CTAPIBaseManager的子类所对应的CTAPIManagerParamSource
|
所以CTNetworking所做的事情,就是将上面的内容进行拆解工作,将完整的API拼接拆解开成为一个个小部件,再通过CTApiProxy、CTRequestGenerator以及CTServiceFactory做为胶水,将他们组合起来,而通过CTURLResponse做为回调输出。
首先是CTService,它主要是以域名头的形式存在,很多网络库封装都会直接忽略掉这一层,相信有不少朋友也是有过以下的经验吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| static NSString *const kBaseUrl = @"https://abc.com"; NetworkRequestDataModel *dataModel = [[NetworkRequestDataModel alloc] init]; dataModel.interfaceFlag = kLoginUrl; dataModel.url = [NSString stringWithFormat:@"%@%@",kBaseUrl,kLoginUrl]; dataModel.params = loginDic; dataModel.isRequireLogin = NO; dataModel.success = ^(id responseObject) { DLogInfo(@"登录成功"); }; dataModel.failure = ^(NSError *error,NSInteger httpStatusCode) { DDLogInfo(@"登录失败"); }; [LGHttpManager dataTaskWithDataModel:dataModel];
|
问题1 :
基本拼接的方式,渗透到每一个调用者身上,让调用者来提供URL的拼接方式,而往往同一服务器团队,提供的API接口都会是同样的规格,以后如果接口拼接方式改动了,那无疑将会是噩梦,可能有的小伙伴会说,你不知道可以全局替换吗?可以的,无疑有点丑了点,但如果一个APP里面同时有几个服务器团队给你提供的接口呢,而且忽然说要改了拼接方式,全局改起来还是有一点难度吧,又或者是有需求加通用参数或者通用的HTTPHeader啊,那时侯我们该怎么办?程序维护,少不了就是变动。所以为了应对这样的问题,我做了Service层的改造,主要提供了以下接口,交由子类去处理:
1 2 3 4 5 6 7 8 9 10 11 12 13
| - (NSDictionary *)extraParmas; - (NSDictionary *)extraHttpHeadParmasWithMethodName:(NSString *)method; - (NSString *)urlGeneratingRuleByMethodName:(NSString *)method; - (BOOL)shouldCallBackByFailedOnCallingAPI:(CTAPIBaseManager *)apiManager;
|
看源码时,就具体看下CTRequestGenerator,他主要做请求的组装加工操作,以NSURLRequest返回给CTApiProxy进行网络请求,我贴下核心的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| - (NSURLRequest *)generateRequestWithServiceIdentifier:(NSString *)serviceIdentifier requestParams:(NSDictionary *)requestParams methodName:(NSString *)methodName requestWithMethod:(NSString *)method { CTService *service = [[CTServiceFactory sharedInstance] serviceWithIdentifier:serviceIdentifier]; NSString *urlString = [service urlGeneratingRuleByMethodName:methodName]; NSDictionary *totalRequestParams = [self totalRequestParamsByService:service requestParams:requestParams]; NSMutableURLRequest *request = [self.httpRequestSerializer requestWithMethod:method URLString:urlString parameters:totalRequestParams error:NULL]; if (![method isEqualToString:@"GET"] && [CTNetworkingConfigurationManager sharedInstance].shouldSetParamsInHTTPBodyButGET) { request.HTTPBody = [NSJSONSerialization dataWithJSONObject:requestParams options:0 error:NULL]; } if ([service.child respondsToSelector:@selector(extraHttpHeadParmasWithMethodName:)]) { NSDictionary *dict = [service.child extraHttpHeadParmasWithMethodName:methodName]; if (dict) { [dict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { [request setValue:obj forHTTPHeaderField:key]; }]; } } request.requestParams = totalRequestParams; return request; } #pragma mark - private method - (NSDictionary *)totalRequestParamsByService:(CTService *)service requestParams:(NSDictionary *)requestParams { NSMutableDictionary *totalRequestParams = [NSMutableDictionary dictionaryWithDictionary:requestParams]; if ([service.child respondsToSelector:@selector(extraParmas)]) { if ([service.child extraParmas]) { [[service.child extraParmas] enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { [totalRequestParams setObject:obj forKey:key]; }]; } } return [totalRequestParams copy]; }
|
内容也比较简单,方法名有相关注释我也就不知道怎么解释了,至此问题1,提到的内容得到解决。
问题2 :
关于问题2,我本来是不想写的,但由于CTNetworking的Star有快1000了,所以对于删文件这种的重大举措,就有必要提一下了,否则下次人家下了个包,就会怒喷上个版本还提供,怎么这个版本就没了,虽然是我干的,但大家@Casa吧,下面就简单聊下我删除内容的想法以及原本CTNetworking的变动内容:
- 作为网络组件,涉及的内容应该在这个范围内,否则它应该是一个APP,而不是组件了,对于过多的功能,最简单的方式就是删,将不是必要的内容删除就可以了。
- CTAPPContext提供太多不是CTNetworking应该提供的功能,砍掉了。
- CTUDIDGenerator作为一个唯一ID生成器,我觉得它应该以一个通用的组件出现,原生主要是CTAPPContext在提供服务,由于CTAPPContext已经被砍掉了,所以它和框架就完全没有依赖了,所以也可以砍掉了。
- 原来的一个叫CTNetworkingConfiguration.h被砍掉了,转而增加了CTNetworkingConfigurationManager单例,提供一些默认的配置以及原生CTAPPContext提供的部分必要服务
- 将来可能还需要砍掉的如CTSignatureGenerator,CTCommonParamsGenerator,CTLocationManager等,由于有些是不是为了后期预留的暂不知道,所以就没有动了。
- 调整了大部分分类名称及方法,统一前缀CT_。
问题3 :
针对问题3,由于CTServiceFactory是单例,而CTServiceFactory与CTService之间的关系其实就只差一个映射字典,所以只要为它提供@{identifier1: @”GDMapService1”,identifier2: @”GDMapService2”},在这里GDMapService1即为相应的Service的字符串,CTServiceFactory使用时,则通过反映直接生成对象,这样就不用去添加不必要的引用了。
在这边我是让CTServiceFactory提供一个数据源,即CTServiceFactoryDataSource,其实也可以直接提供一个属性的,但不晓得当时为什么这么做的,忘记了,可能是脑抽了吧,所以使用起来,大致是这样的:
1 2 3 4 5 6 7 8 9 10 11 12
| @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [CTServiceFactory sharedInstance].dataSource = self; return YES; } #pragma mark - CTServiceFactoryDataSource - (NSDictionary<NSString *,NSString *> *)servicesKindsOfServiceFactory { return @{kCTServiceGDMapV3: @"GDMapService"}; }
|
至此,所有改造的内容已经讲完了。
扩展篇
现市面上,大部分APP都采用OAUTH授权登录的方式,登录完成后,会获得相应的accessToken、expireTime及refreshToken,基本上调用其他接口使用,都会在请求头里面拼接相应的accessToken,这部分上面有提到应该交由Service去处理,让Service负责拼接的工作。
网络请求时,token失效是会收到类型expired_access_token,而token非法时,会收到illegal_access_token,当然还有更多的情况,由于错误信息error description可能各种公司的会不一致,所以在Service层预留的shouldCallBackByFailedOnCallingAPI就是为了做这样的事情,当网络请求失败时,到service决定是否拦截并做相应处理。