一、啰嗦一下
实际开发中,如果网络接口后端还没有提供,就需要我们前端人员自己mock数据,当项目代码量不多
和逻辑复杂程度比较低的时候,mock数据很好处理。但项目很大很大的时候,改接口方法,代码写死
数据等方法就显得不合适了,而且这样风险较大,尤其是多人项目的时候,很容易出现带着debug的
代码上线,这样被fire也是有可能的,那么有没有可以不对业务代码进行太多侵入,又能解决mock数据
的问题,我花了3天(第一天思考、第二天写核心代码、第三天完善代码),做了个小工具,看看能否
解决这个问题。
二、核心代码
#import
// 按照标准命名了,就不使用这个宏
#define MCRequest(Req) \
- (void) __send##Req##Request {}
// 设置Mock的url地址
#define MCRequestURL(Req, URL) \
- (NSString *) __##Req##MockURLString { return URL; }
// 设置处理网络请求
#define MCHandle(Req, HandleSEL) \
- (void) __handle##Req##Request:(id) data {} \
- (NSString *) __originalHandle##Req { return NSStringFromSelector(HandleSEL); }
@interface MockManager : NSObject
+ (instancetype)shareInstance;
@end
#import "MockManager.h"
#import "Aspects.h"
#import
#import
@interface MockManager ()
@property (nonatomic, strong) NSDictionary *mcDataDic;
@end
@implementation MockManager
#ifdef DEBUG
+ (void) load {
NSLog(@"MockManager load");
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//XIB、SB
[UIViewController aspect_hookSelector:@selector(initWithCoder:) withOptions:AspectPositionBefore usingBlock:^(id aspectInfo)
{
NSString *mockOpenState = [[[MockManager shareInstance].mcDataDic objectForKey:@"Config"] objectForKey:@"MOCKOPENSTATE"];
if (![mockOpenState isEqualToString:@"#"]) {
[self magicMethods:aspectInfo.instance];
}
} error:NULL];
//纯代码
[UIViewController aspect_hookSelector:@selector(init) withOptions:AspectPositionBefore usingBlock:^(id aspectInfo)
{
NSString *mockOpenState = [[[MockManager shareInstance].mcDataDic objectForKey:@"Config"] objectForKey:@"MOCKOPENSTATE"];
if (![mockOpenState isEqualToString:@"#"]) {
[self magicMethods:aspectInfo.instance];
}
} error:NULL];
});
}
#endif
/* 替换VC的Request方法 */
+ (void) magicMethods:(UIViewController *) vc {
NSString *mockClass = [[[MockManager shareInstance].mcDataDic objectForKey:@"Config"] objectForKey:@"MOCK"];
if (![mockClass containsString:NSStringFromClass([vc class])]) { //过滤不在Mock列表中的类
return;
} else {
NSLog(@"%@ 正在使用MOCK", vc);
}
NSMutableArray *methodsArray = [self methodsArray:vc];
NSLog(@"magicMethods:%@", methodsArray);
Class theClass = [vc class];
for (NSInteger j=0; j aspectInfo) {
SEL methodNameSEL = NSSelectorFromString(methodName);
NSString *assertMsg = [NSString stringWithFormat:@"MockManager:%@找不到%@", vc, methodName];
NSAssert([vc respondsToSelector:methodNameSEL], assertMsg);
objc_msgSend(vc, methodNameSEL, nil, nil);
} error:NULL];
} else if ([methodsArray containsObject:originalMethodNameUppercase]) {
[theClass aspect_hookSelector:NSSelectorFromString(originalMethodNameUppercase) withOptions:AspectPositionInstead usingBlock:^(id aspectInfo) {
SEL methodNameSEL = NSSelectorFromString(methodName);
NSString *assertMsg = [NSString stringWithFormat:@"MockManager:%@找不到%@", vc, methodName];
NSAssert([vc respondsToSelector:methodNameSEL], assertMsg);
objc_msgSend(vc, methodNameSEL, nil, nil);
} error:NULL];
}
else {
//网络请求方法遵循了sendXXXRequest方法,不需要MCRequest注解。不管是否遵循了sendXXXRequest方法,网络请求方法,接下来都会被handleXXXRequest方法截获
}
//原始处理网络数据方法,查看是否定义了originalHandleXXX,如果定义了,说明处理数据方法被改写,不然说明方法存在
NSString *originalHandle = [NSString stringWithFormat:@"__originalHandle%@", originalMethodName];
NSString *theNewHandleSELString = nil;
if ([methodsArray containsObject:originalHandle]) {
SEL originalHandleSEL = NSSelectorFromString(originalHandle);
NSString *assertMsg = [NSString stringWithFormat:@"MockManager:%@找不到%@", vc, originalHandle];
NSAssert([vc respondsToSelector:originalHandleSEL], assertMsg);
NSString *originalHandleSELString = objc_msgSend(vc, originalHandleSEL, nil, nil);
//将handleXXXRequest方法获取originalHandleXXX的实现
theNewHandleSELString = [NSString stringWithFormat:@"__handle%@Request:", originalMethodName];
swizzleMethod(theClass, NSSelectorFromString(originalHandleSELString), NSSelectorFromString(theNewHandleSELString));
} else {
//不然的话,说明handleXXXRequest:方法存在
theNewHandleSELString = [NSString stringWithFormat:@"handle%@Request:", originalMethodName];
}
//如果调用sendXXXRequest,就会被handleXXXRequest:截获 methodName=sendTestRequest,加注解后methodName=__sendGetNetDataRequest
[theClass aspect_hookSelector:NSSelectorFromString(methodName) withOptions:AspectPositionInstead usingBlock:^(id aspectInfo) {
//如果配置了MCRequestURL,就读取这个URL。否则去读取本地配置文件
SEL sendReqURLSELUppercase = NSSelectorFromString([NSString stringWithFormat:@"__%@MockURLString", [self stringFirstCharUppercase:methodName]]);
SEL sendReqURLSELLowercase = NSSelectorFromString([NSString stringWithFormat:@"__%@MockURLString", [self stringFirstCharLowercase:methodName]]);
SEL originalReqURLSELUppercase = NSSelectorFromString([NSString stringWithFormat:@"__%@MockURLString", [self stringFirstCharUppercase:originalMethodName]]);
SEL originalReqURLSELLowercase = NSSelectorFromString([NSString stringWithFormat:@"__%@MockURLString", [self stringFirstCharLowercase:originalMethodName]]);
id returnValue = nil;
NSString *rapUrlString = nil;
if ([vc respondsToSelector:sendReqURLSELUppercase]) {
rapUrlString = objc_msgSend(vc, sendReqURLSELUppercase, nil, nil);
} else if([vc respondsToSelector:sendReqURLSELLowercase]) {
rapUrlString = objc_msgSend(vc, sendReqURLSELLowercase, nil, nil);
} else if([vc respondsToSelector:originalReqURLSELUppercase]) {
rapUrlString = objc_msgSend(vc, originalReqURLSELUppercase, nil, nil);
} else if([vc respondsToSelector:originalReqURLSELLowercase]) {
rapUrlString = objc_msgSend(vc, originalReqURLSELLowercase, nil, nil);
} else {
NSString *classString = NSStringFromClass(theClass);
NSDictionary *classDic = [[[MockManager shareInstance].mcDataDic objectForKey:@"MockData"] objectForKey:classString];
NSDictionary *reqMethodDic = nil;
if ([methodName hasPrefix:@"__send"]) {
reqMethodDic = [classDic objectForKey:originalMethodNameLowercase];
} else {
reqMethodDic = [classDic objectForKey:methodName];
}
if (reqMethodDic != nil) {
NSString *serviceString = [reqMethodDic objectForKey:@"service"];
if ([serviceString hasPrefix:@"#"] || (serviceString.length == 0)) {
NSString *localJsonString = [reqMethodDic objectForKey:@"local"];
if (localJsonString == nil || (localJsonString.length == 0)) {
returnValue = [[[MockManager shareInstance].mcDataDic objectForKey:@"MockData"] objectForKey:@"RequestError"];
} else {
NSError *error = nil;
NSData *jsonData = [localJsonString dataUsingEncoding:NSUTF8StringEncoding];
id jsonObj = [NSJSONSerialization JSONObjectWithData:jsonData
options:NSJSONReadingAllowFragments
error:&error];
if (!error && (jsonObj != nil)) {
returnValue = jsonObj;
} else {
returnValue = [[[MockManager shareInstance].mcDataDic objectForKey:@"MockData"] objectForKey:@"RequestError"];
}
}
} else {
rapUrlString = serviceString;
}
} else {
returnValue = [[[MockManager shareInstance].mcDataDic objectForKey:@"MockData"] objectForKey:@"RequestError"];
}
}
NSError *error = nil;
if (rapUrlString != nil) {
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:rapUrlString]];
returnValue = [NSJSONSerialization JSONObjectWithData:data
options:NSJSONReadingAllowFragments
error:&error];
}
if (error) {
returnValue = [[[MockManager shareInstance].mcDataDic objectForKey:@"MockData"] objectForKey:@"RequestError"];
} else {
//XXXMockURLString
//这里根据URL(returnValue)做网络请求,然后回调原来的数据处理逻辑
SEL theNewHandleSELStringSEL = NSSelectorFromString(theNewHandleSELString);
NSString *assertMsg = [NSString stringWithFormat:@"MockManager:%@找不到%@", vc, theNewHandleSELString];
NSAssert([vc respondsToSelector:theNewHandleSELStringSEL], assertMsg);
objc_msgSend(vc, theNewHandleSELStringSEL, returnValue, nil);
}
} error:NULL];
}
}
// NSLog(@"%@:%@", vc, methodsArray);
}
+ (NSString *) stringFirstCharUppercase:(NSString *) string {
NSString *firstString = [string substringToIndex:1];
NSString *lastString = [string substringFromIndex:1];
NSString *theString = [NSString stringWithFormat:@"%@%@", [firstString uppercaseString], lastString];
return theString;
}
+ (NSString *) stringFirstCharLowercase:(NSString *) string {
NSString *firstString = [string substringToIndex:1];
NSString *lastString = [string substringFromIndex:1];
NSString *theString = [NSString stringWithFormat:@"%@%@", [firstString lowercaseString], lastString];
return theString;
}
+ (NSString *) originalMethodName:(NSString *) methodName {
NSString *originalMethodName = [methodName stringByReplacingOccurrencesOfString:@"send" withString:@""];
originalMethodName = [originalMethodName stringByReplacingOccurrencesOfString:@"Request" withString:@""];
originalMethodName = [originalMethodName stringByReplacingOccurrencesOfString:@"_" withString:@""];
return originalMethodName;
}
+ (NSMutableArray *) methodsArray:(id) object {
unsigned int methodCount =0;
Method* methodList = class_copyMethodList([object class], &methodCount);
NSMutableArray *methodsArray = [NSMutableArray arrayWithCapacity:methodCount];
for(int i=0; i
三、使用方法
//使用方法:
//1、开启和关闭mock
//MCDataPlist.plist -> Config -> MOCKOPENSTATE,配置成#则关闭mock,否则开启mock。
//如果项目上线设置,则mock默认关闭,主要通过DEBUG实现关闭
//2、配置mock哪些类
//MCDataPlist.plist -> Config -> MOCK,配置需要mock扫描的类,多个类用#分割开,例如AController#BController。
//3、接口方法规范
//要求发起网络请求方法要写成sendXXXRequest,接收网络请求方法返回数据的方法写成handleXXXRequest:
//4、配置网络请求的mock地址
//使用宏MCRequestURL(Req, URL)配置请求的mock地址,其中Req是请求的名字,要求首字母大写,URL是请求地址字符串,例如MCRequestURL(GetNetData, @"http://10.141.4.93:8080/mockjs/14/common/omissionInfoSwitch")
//5、接口方法不规范解决方法
//使用宏MCRequest(Req)定义发起网络请求方法,Req为方法名字,首字母要大写,例如- (void) getNetData,要写成MCRequest(GetNetData)
//使用宏MCHandle(Req, HandleSEL)定义接受网络请求返回数据方法,Req为网络请求方法名字,首字母大写。HandleSEL为接受网络请求方法的SEL,例如MCHandle(GetNetData, @selector(useNetData:))
//6、配置本地mock数据
//如果没有MCRequestURL给网络请求方法,也可以MCDataPlist.plist配置本地数据,配置规范是在MCDataPlist.plist -> MockData -> 类名,类名下边配置网络请求方法名字,具体见MCDataPlist.plist
//如果配置了MCRequestURL(Req, URL),则本地mock数据的接口mock数据会被忽略,如果使用本地mock数据,本地数据中service字段配置的url优先于local的json数据
//7、配置忽略
//项目上线,则mock自动失效
//可以在MCDataPlist.plist -> Config -> MOCKOPENSTATE,配置成#,则mock完全关闭
//可以MCDataPlist.plist -> Config -> MOCK不配置任何类名,则mock默认不做任何扫描
//可以MCDataPlist.plist -> MockData -> 类名 -> 网络请求方法名 -> ignore,配置成#,则忽略本网络请求方法
//可以MCDataPlist.plist -> MockData -> 类名 -> 网络请求方法名 -> service,配置成#http:XXX,则忽略此mock的网络请求url配置
四、MCDataPlist.plist配置
五:代码实际应用