美文网首页
开发中接口mock数据

开发中接口mock数据

作者: 福哥_ | 来源:发表于2018-04-12 19:01 被阅读489次

    一、啰嗦一下

    实际开发中,如果网络接口后端还没有提供,就需要我们前端人员自己mock数据,当项目代码量不多
    和逻辑复杂程度比较低的时候,mock数据很好处理。但项目很大很大的时候,改接口方法,代码写死
    数据等方法就显得不合适了,而且这样风险较大,尤其是多人项目的时候,很容易出现带着debug的
    代码上线,这样被fire也是有可能的,那么有没有可以不对业务代码进行太多侵入,又能解决mock数据
    的问题,我花了3天(第一天思考、第二天写核心代码、第三天完善代码),做了个小工具,看看能否
    解决这个问题。
    

    二、核心代码

    #import <Foundation/Foundation.h>
    
    // 按照标准命名了,就不使用这个宏
    #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 <objc/runtime.h>
    #import <objc/message.h>
    
    @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> 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> 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<methodsArray.count; j++) {
            NSString *methodName = (NSString *)methodsArray[j];
        
            //如果存在sendXXXRequest方法
            if (([methodName hasPrefix:@"__send"] || [methodName hasPrefix:@"send"]) && [methodName hasSuffix:@"Request"]) {
                //先取出sendXXXRequest方法中的XXX
                NSString *originalMethodName = [self originalMethodName:methodName];
                NSString *originalMethodNameLowercase = [self stringFirstCharLowercase:originalMethodName];
                NSString *originalMethodNameUppercase = [self stringFirstCharUppercase:originalMethodName];
                
                //过滤ignore方法
                BOOL isIgnore = NO;
                NSString *classString = NSStringFromClass(theClass);
                NSDictionary *classDic = [[[MockManager shareInstance].mcDataDic objectForKey:@"MockData"] objectForKey:classString];
                NSString *ingoreReqName = nil;
                for (NSString *reqName in [classDic allKeys]) {
                    NSString *ignore = [[classDic objectForKey:reqName] objectForKey:@"ignore"];
                    if ([ignore isEqualToString:@"#"]) {
                        NSString *theOriginal = [self originalMethodName:reqName];
                        if ([[originalMethodName lowercaseString] isEqualToString:[theOriginal lowercaseString]]) { //表明此方法忽略
                            isIgnore = YES;
                            ingoreReqName = reqName;
                            continue;
                        }
                    }
                }
                if (isIgnore) {
                    NSLog(@"%@ 正在使用MOCK,%@ 方法被忽略", vc, ingoreReqName);
                    continue;
                }
                
                //如果类中存在XXX方法,说明使用了MCRequest注解
                if ([methodsArray containsObject:originalMethodNameLowercase]) {
                    //如果调用了网络请求方法,这里截获调用,实际上去调用MCRequest生成的sendXXXRequest方法
                    [theClass aspect_hookSelector:NSSelectorFromString(originalMethodNameLowercase) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> 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> 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> 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<methodCount; i++) {
            Method temp = methodList[i];
            const char* name_s =sel_getName(method_getName(temp));
            [methodsArray addObject:[NSString stringWithUTF8String:name_s]];
        }
        free(methodList);
        return methodsArray;
    }
    
    void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector)   {
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        BOOL didAddMethod =
        class_addMethod(class,
                        originalSelector,
                        method_getImplementation(swizzledMethod),
                        method_getTypeEncoding(swizzledMethod));
        
        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    }
    
    + (instancetype)shareInstance {
        static MockManager *sharedInstance = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            sharedInstance = [[self alloc] init];
        });
        return sharedInstance;
    }
    
    - (instancetype) init {
        if (self = [super init]) {
            NSString *filePath = [[NSBundle mainBundle] pathForResource:@"MCDataPlist" ofType:@"plist"];
            self.mcDataDic = [[NSDictionary alloc] initWithContentsOfFile:filePath];
            NSLog(@"data2:%@", self.mcDataDic);
        }
        return self;
    }
    
    - (id)copy {
        return self;
    }
    
    - (id)mutableCopy {
        return self;
    }
    
    @end
    

    三、使用方法

    //使用方法:
    //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配置

    屏幕快照 2018-04-12 下午7.14.03.png

    五:代码实际应用


    ssss.png

    相关文章

      网友评论

          本文标题:开发中接口mock数据

          本文链接:https://www.haomeiwen.com/subject/moefkftx.html