关于CTNetwokring的改造记录

前言

在读这篇文章之前,大家先看一下作者Casa的文章,以便对CTNetwokring的整体设计以及使用方式有个大体的了解。

其次这次改造,主要由需求驱动,因为没有以下这些问题,我也不会做这样的尝试:

  • 公司的网络库使用虽然很方便,依赖性太强,不方便测试,属于集约型的做法,导致公司不太可能写单元测试,所以没有写单元测试,又由于公司后台经常私底下改接口,导致线上版本经常出现问题。
  • CTNetworking刚出来的时候我就关注了,苦于放出来的版本,适合学习,并不合适即插即用,我一直在等有人来改造,结果两年过去了。
  • 由于那是早年安居客的脱敏版,所以设计上面就或多或少有一些依赖业务的东西,建立在它们业务上,的确是最合适方便的,但也由于太合适自身业务,那么也导致无法通用化,其他公司想拿来用就必须改,对于这一点,我在这里要建议强烈批评一下Casa,开源这么好的东西出来,怎么就不弄得更方便一点呢,像CTPersistance(安利一下)那样子插上就能用多好。
  • 我想要开始走向开源社区(不要脸地说,我想涨点粉),在写这个文章前,由于公司项目API一两百个,真是没有余力做短期替换,所以我所提供的那部分代码还没有真正的得到真实项目的考量,另一方面基于我自己不会写后端接口,公司内容涉密问题也导致了没法给出更多完整的使用方式,本文给出的将是我的一套YY方案,欢迎评论区拍砖。

改造篇

如上面提到的,原来的CTNetworking之所以不能做到即插即用,主要有以下几点:

  1. 有旧业务的依赖,比如token的拼接,url组装啊。
  2. 涉及面略广,作为一个网络封装层,提供了CTUDIDGenerator等UUID生成器,CTAPPContext致使引入一些不必要的业务。
  3. ServiceFactory需要硬编码,他需要知道详细的业务Service,而业务的Service硬编码无疑就使得他变得不通用。

谈到要解决上面的问题,那么就有必要来讲解一下CTNetworking的基本组成设计了,这是Casa没有在他文章里面讲的东西,打个比方,比如有这么一个接口,使用CTNetworking的视角来说,它是下面这样的:

1
2
3
4
5
6
7
8
9
https://xxx.com/method?username=corotata => 完整接口
拆解:
https://xxx.com => CTNetworking中的CTService
/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
//CTServiceProtocol
//为某些Service需要拼凑额外字段到URL处
- (NSDictionary *)extraParmas;
//为某些Service需要拼凑额外的HTTPToken,如accessToken
- (NSDictionary *)extraHttpHeadParmasWithMethodName:(NSString *)method;
//提供URL的拼接方式
- (NSString *)urlGeneratingRuleByMethodName:(NSString *)method;
//提供拦截器集中处理Service错误问题,比如token失效要抛通知等,返回值用来做拦截特殊错误时通知上层是否还要继续回调
- (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
//根据Service拼接额外参数
- (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的变动内容:

  1. 作为网络组件,涉及的内容应该在这个范围内,否则它应该是一个APP,而不是组件了,对于过多的功能,最简单的方式就是删,将不是必要的内容删除就可以了。
  2. CTAPPContext提供太多不是CTNetworking应该提供的功能,砍掉了。
  3. CTUDIDGenerator作为一个唯一ID生成器,我觉得它应该以一个通用的组件出现,原生主要是CTAPPContext在提供服务,由于CTAPPContext已经被砍掉了,所以它和框架就完全没有依赖了,所以也可以砍掉了。
  4. 原来的一个叫CTNetworkingConfiguration.h被砍掉了,转而增加了CTNetworkingConfigurationManager单例,提供一些默认的配置以及原生CTAPPContext提供的部分必要服务
  5. 将来可能还需要砍掉的如CTSignatureGenerator,CTCommonParamsGenerator,CTLocationManager等,由于有些是不是为了后期预留的暂不知道,所以就没有动了。
  6. 调整了大部分分类名称及方法,统一前缀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决定是否拦截并做相应处理。