本文是投稿文章,作者:wujunyang
jiaModuleDemo項目是為了解決關於項目中如何進行模塊化開發而編寫的實例,包含如何進行路由式、本地模塊間交互的實現;目前還是在頁面層級進行抽離,對於項目中各個模塊共有的基礎功能也進行提取,可以結合私有Pods進行管理;
項目中存在的問題
問題一:頁面耦合嚴重
上面這張圖中左邊體現了目前項目中存在的問題,對於頁面之間相互耦合,而頁面之間的傳參也各不相同,由於不同的開發人員或者簡便方式等原因,傳參的類型都有差異,包含如實體、簡單基本類型等,先前項目對於路由方式也不支持,導致要實現收到消息推送進行不同的頁面跳轉存在硬編碼情況,對於功能擴展存在相當大的問題;而右邊則是模塊化後頁面之間的交互方式;頁面之間也不存在耦合關系,都只跟JiaMediator這個中介者相依賴;而傳參都統一成以字典的形式;雖然可能犧牲一些方便跟隨意,卻可以解耦模塊化;並且加入對路由方式的處理;約定好相關的協議進行交互;用這種路由方式代替那些第三方的路由插件則是因為它的靈活性,最主要還是省去了第三方路由插件在啟動時要注冊路由的問題;
問題二:相同模塊重復開發
當公司裡面有多個項目同時進行,並且有可能是多個人分別不同項目時,就會存在如上圖出現的情況,其實每個APP中都是有很多共同的模塊,當然有可能你會把相同功能模塊代碼復制一份在新項目中,但這其實並不是最好的方式,在後期不斷迭代過程中,不同的人會往裡面增加很多帶有個人色彩的代碼;這樣就像相同的模塊項目後期對於多個項目統一管理也是災難性,有可能會失控,哪怕項目轉移別人接手也會無形中浪費很多時間,增加維護成本,所以實例中更注重對於一些相同模塊進行提取,求同存異;而模塊化結合私有Pods進行管理,對於常用功能的封裝,只要開放出一些簡單開關配置方式,就可以實現一個功能,比如日志記錄、網絡請求模塊、網絡狀態變化提示等;
模塊化解決方案
頁面交互解耦
實現調用代碼:
NSDictionary *curParams=@{kDesignerModuleActionsDictionaryKeyName:@"wujunyang",kDesignerModuleActionsDictionaryKeyID:@"1001",kDesignerModuleActionsDictionaryKeyImage:@"designerImage"}; switch (indexPath.row) { case 0: { UIViewController *viewController=[[JiaMediator sharedInstance]JiaMediator_Designer_viewControllerForDetail:curParams]; [self presentViewController:viewController animated:YES completion:nil]; break; } case 1: { UIViewController *viewController=[[JiaMediator sharedInstance]JiaMediator_Designer_viewControllerForDetail:curParams]; [self.navigationController pushViewController:viewController animated:YES]; break; } case 2: { NSString *curRoue=@"jiaScheme://Designer/nativeFetchDetailViewController?name=wujunyang&ID=1001&image=designerImage"; UIViewController *viewController=[[JiaMediator sharedInstance]performActionWithUrl:[NSURL URLWithString:curRoue] completion:^(NSDictionary *info) { }]; [self.navigationController pushViewController:viewController animated:YES]; break; } default: break; }
上面針對本地模塊調用及路由方式調用的跳轉
1:JiaMediator起到一個中介的作用,所有的模塊間響應交互都是通過它進行,每個模塊都會對它進行擴展分類(例如:JiaMediator+模塊A),分類主要是為了用於本地間調用而又不想用路由的方式,若要用路由的方式則要注意關於路由約束准確編寫,它將會直接影響到能否正確響應到目標;
2:JiaMediator是每個模塊都要用到的內容,可以把它放在公共的模塊中,因為關於各個模塊的JiaMediator由每個模塊自個負責,開放給要調用的模塊使用;
3:為了解耦對於頁面間的傳參都采用字典形式,項目中所有的頁面都繼承於一個基頁面jiaBaseViewController,裡面已經有對初始化對於字典參數的接收並賦值,每個模塊的子頁面只要調用parameterDictionary屬性,就可以獲取關於參數的內容;同樣jiaBaseViewController也是每個模塊都要使用,所以也被提取在公共裡面,其還包括一些導欄條的封裝及關於網絡狀態變化的提示等;
//頁面接收參數 @property(nonatomic,strong)NSDictionary *parameterDictionary; //初始化參數 - (id)initWithRouterParams:(NSDictionary *)params; - (id)initWithRouterParams:(NSDictionary *)params { self = [super init]; if (self) { _parameterDictionary=params; NSLog(@"當前參數:%@",params); } return self; }
4:當響應某一個模塊目標後,將會把相應的viewController進行返回,而對於具體如何操作則是在獲得當前控制器自行處理,比如是跳轉還是彈出展現;
5:為了減少對於字典參數key拼寫錯誤問題,每個模塊都有一個對應key值的常量配置文件,已經把對應的key值都定義成的常量,方便調用;
#ifndef HeaderDesignerConfig_h #define HeaderDesignerConfig_h //鍵值 static NSString * const kDesignerModuleActionsDictionaryKeyName=@"name"; static NSString * const kDesignerModuleActionsDictionaryKeyID=@"ID"; static NSString * const kDesignerModuleActionsDictionaryKeyImage=@"image"; #endif /* HeaderDesignerConfig_h */ NSDictionary *curParams=@{kDesignerModuleActionsDictionaryKeyName:@"wujunyang",kDesignerModuleActionsDictionaryKeyID:@"1
JiaCore(基礎功能封裝)
JiaCore是整個APP最基礎模塊,所有的模塊化都要依賴,主要包含一些全局的功能模塊,比如JiaBaseViewController、JiaAppDelegate等;目前已經把一些默認的功能進行集成在裡面,包含網絡狀態變化判斷及提示、日志記錄功能等;並把一些相關配置的內容用JiaCoreConfigManager這個管理類進行統一設置,比如是否打開日志記錄功能;JiaCoreConfigManager類則是開放給具體APP設置全局的相關配置;下面就以其中一個日志記錄功能進行講解:
//JiaCore基礎模塊相關配置 JiaCoreConfigManager *jiaCoreConfig=[JiaCoreConfigManager sharedInstance]; jiaCoreConfig.recordlogger=YES;
然後具體APP的PrefixHeader.pch引入命名空間並進行設置記錄日志的等級:
#import "JiaCocoaLumberjack.h" //DDLog等級 static const int ddLogLevel = DDLogLevelVerbose;
這樣就完成的一個APP對於日志記錄模塊的引入,JiaCore已經幫你完成的關於日志記錄的相關配置,並且錯誤內容以一種可讀性較好的格式記錄到file文件中,而且這些file文件生成規則也都定義好了,當然如何時你要是在Xcode控制台顯示不同等級色彩,只要安裝XcodeColors插件並簡單進行設置就可以了,對於不同等級不同色彩都已經在JiaCore配置完成;
在JiaCore裡面也默認集成了熱更新的功能,只要傳入簡單的對象數組就會啟動熱更新;其中JiaPathchModel已經是定義好的模型,在APP中把接口請求轉化成模型數組,其中patchId是唯一值名稱、md5則是JS文件的MD5值、url是JS的下載路徑、ver則是對哪個版本起作用;因為一般我們在外面的APP都是多版本共存,熱更新也要進行版本區分,只下載與本版本相對應的熱更新JS文件加載;而MD5值則是為了增加安全性,避免JS文件被別人進行修改而影響APP的運行,在JiaCore會對下載後的JS文件進行MD5計算並比較;對於沒有在jSPatchMutableArray以前的JS文件會被刪除;
//熱更新內容 JiaPathchModel *sample=[[JiaPathchModel alloc]init]; sample.patchId = @"patchId_sample1"; sample.md5 = @"2cf1c6f6c5632dc21224bf42c698706b"; sample.url = @"http://test.qshmall.net:9090/demo1.js"; sample.ver = @"1"; JiaPathchModel *sample1=[[JiaPathchModel alloc]init]; sample1.patchId = @"patchId_sample2"; sample1.md5 = @"e8a4eaeadce5a4598fb9a868e09c75fd"; sample1.url = @"http://test.qshmall.net:9090/demo2.js"; sample1.ver = @"1"; //JiaCore基礎模塊相關配置 JiaCoreConfigManager *jiaCoreConfig=[JiaCoreConfigManager sharedInstance]; jiaCoreConfig.jSPatchMutableArray=[@[sample,sample1] mutableCopy];
JiaNetWork(網絡交互封裝)
對於網絡請求模塊則采用YTKNetwork,底層還是以AFNetworking進行網絡通信交互,定義一個繼承於YTKBaseRequest的JiaBaseRequest,針對JiaBaseRequest則是為了後期各個APP可以對它進行分類擴展,對於一些超時、請求頭部等進行統一個性化設置,畢竟這些是每個APP都不相同;而針對模塊中關於請求網絡的前綴設置,則在每個模塊中都有一個單例的配置類,此配置類是為了針對該模塊對不同APP變化而定義;相應的配置內容開放給APP,由具體APP來定義,例如現在項目中的JiaBaseRequest+App.h類,裡面有簡單設置超時跟頭部;當然記得把這個分類引入到APP中,比如AppPrefixHeader這個APP的全局頭部;
#import "JiaBaseRequest+App.h" @implementation JiaBaseRequest (App) - (NSTimeInterval)requestTimeoutInterval { return 15; } //公共頭部設置 - (NSDictionary *)requestHeaderFieldValueDictionary { NSDictionary *headerDictionary=@{@"platform":@"ios"}; return headerDictionary; } @end
網絡層整體實現如下:
JiaGT模塊(個推封裝)
消息推送對於一個APP是相當重要性,一般是采用第三方的SDK進行集成,其實大部分的SDK處理代碼都是差不多,在這實例中對差異化的內容進行提取,實例中將以個推進行模塊化,因為消息推送的大部分代碼都集中在AppDelegate中,造成的一大堆雜亂代碼,當然也有一部分人對AppDelegate進行擴展分類進行移除代碼,實例中將采用另外一種解決方案進行抽取,可以達到完全解耦,在具體的APP裡面將不會再出現個推SDK相關內容,只要簡單進行配置跟處理消息就可以,下面只是簡單的列出部分代碼,其它封裝代碼見源代碼;
//設置個推模塊的配置 jiaGTConfigManager *gtConfig=[jiaGTConfigManager sharedInstance]; gtConfig.jiaGTAppId=@"0uuwznWonIANoK07JeRWgAs"; gtConfig.jiaGTAppKey=@"26LeO4stbrA7TeyMUJdXlx3"; gtConfig.jiaGTAppSecret=@"2282vl0IwZd9KL3ZpDyoUL7"; #pragma mark 消息推送相關處理 /** * @author wujunyang, 16-07-07 16:07:25 * * @brief 處理個推消息 * * @param NotificationMessage */ -(void)gtNotification:(NSDictionary *)NotificationMessage { NSLog(@"%@",NotificationMessage[@"payload"]); NSLog(@"-----接收到個推通知------"); } /** * @author wujunyang, 16-07-07 16:07:40 * * @brief 處理遠程蘋果通知 * * @param RemoteNotificationMessage */ -(void)receiveRemoteNotification:(NSDictionary *)RemoteNotificationMessage { NSLog(@"%@",RemoteNotificationMessage[@"message"]); NSLog(@"-----接收到蘋果通知------"); } /** * @author wujunyang, 16-09-21 14:09:33 * * @brief 獲得注冊成功時的deviceToken 可以在裡面做一些綁定操作 * * @param deviceToken */ -(void)receiveDeviceToken:(NSString *)deviceToken { NSLog(@"-----當前deviceToken:%@------",deviceToken); }
上面能夠對個推進行完全的解耦不得不提一個第三方的插件XAspect,如果想對它進行了解可以在github進行查找;它的主要作用如下圖,可以用它進行其它第三方SDK的抽離
JiaAnalytics模塊(友盟統計封裝)
JiaAnalytics模塊是在友盟統計SDK跟Aspect相結合基礎上完成,對於頁面的進出統計采用Aop切面方式進行,把原本應該在每個頁面生命周期的統計代碼移除,App運用只要簡單配置友盟相對應的信息,也可以設置要統計頁面的過濾條件,目前已經有三種如要統計的開頭頁面的前綴字符串數組、要統計的頁面名稱字符串數組、不統計的頁面名稱字符串數組;可以結合使用,達到精確統計頁面的目的;而且把統計的代碼放在異步線程進行,不會影響主線程的響應;
JiaShare模塊(友盟分享及第三方登錄封裝)
JiaShare模塊運用友盟分享最新版的SDK進行封裝,並把一些其它不是很常用的去除,目前只支持新浪、微信聊天、微信朋友圈、QQ聊天頁面、qq空間、騰訊微博;分享包括純文本、圖文、URL、視頻地址、音樂地址,並在裡面已經運用JavaScriptCore.framework封裝好關於H5頁面調用分享並傳參的功能;可以直接在UIWebView裡面進行調用;本模塊只是對功能進行封裝,對於友盟在info.plist的配置還是要自行手動,如不明白可以直接到友盟分享官網查看,項目到也有配置好一份可以直接參考;JiaPlatformHelper類裡面有一個判斷當前手機是否有安裝相應平台軟件的方法,可以用它進行隱藏相應的操作功能,避免上架時審核不過;下面的代碼是在項目的AppDelegate裡面進行配置.
//友盟分享 JiaShareConfigManager *jiaShareConfig=[JiaShareConfigManager sharedInstance]; jiaShareConfig.shareAppKey=@"57e3f1cbe0f55a42080011ec"; jiaShareConfig.shareLogEnabled=YES; //設置平台 [jiaShareConfig setPlaform:JiaSocialPlatConfigType_Tencent appKey:@"100424468" appSecret:@"c7394704798a158208a74ab60104f0ba" redirectURL:@"http://www.umeng.com/social"]; [jiaShareConfig setPlaform:JiaSocialPlatConfigType_Wechat appKey:@"wxdc1e388c3822c80b" appSecret:@"3baf1193c85774b3fd9d18447d76cab0" redirectURL:@"http://www.umeng.com/social"]; [jiaShareConfig setPlaform:JiaSocialPlatConfigType_Sina appKey:@"3921700954" appSecret:@"04b48b094faeb16683c32669824ebdad" redirectURL:@"http://sns.whalecloud.com/sina2/callback"];
然後就可以進行分享,在ViewController裡面進行調用,JiaSocialPlatformType是分享平台的枚舉,shareUrlDataWithPlatform為URL分享方式,其它可以直接見源代碼JiaShareHelper類
[JiaShareHelper shareUrlDataWithPlatform:JiaSocialPlatformType_WechatSession withShareUrl:@"http://www.sina.com.cn" withTitle:@"新浪" withDescr:@"新浪網頁" withThumImage:@"http://dev.umeng.com/images/tab2_1.png" withCompletion:^(id result, NSError *error) { if(error) { NSLog(@"分享出錯了"); } }];
如果有加載H5頁面,而且也要進行分享的功能就可以使用JiaWebShareHelper,因為使用到的是JavaScriptCore,所以只能用在UIWebView中,如果你要是使用WKWebView可以自個再進行封閉,下面是加載H5頁面中的webViewDidFinishLoad代碼;
- (void)webViewDidFinishLoad:(UIWebView *)webView { self.jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; // 通過模型調用方法,這種方式更好些。 JiaWebShareHelper *shareHelper = [[JiaWebShareHelper alloc] init]; self.jsContext[@"jia"] = shareHelper; shareHelper.jsContext = self.jsContext; shareHelper.webView = self.webView; self.jsContext.exceptionHandler = ^(JSContext *context, JSValue *exceptionValue) { context.exception = exceptionValue; NSLog(@"異常信息:%@", exceptionValue); }; }
H5中就可以很方便直接進行:
下面簡單介紹關於第三方登錄跟獲取用戶信息的功能,其功能代碼放在JiaPlatformHelper裡面,已經對授權成功及獲取用戶信息都有回去調相應的參數回來:
if(![JiaPlatformHelper installPlatAppWithType:JiaSocialPlatformType_QQ]) { [weakSelf showResult:@"沒有安裝QQ軟件,將此功能隱藏"]; return; } [JiaPlatformHelper authWithPlatform:JiaSocialPlatformType_QQ withCompletion:^(NSString *uid, NSString *openid,NSString *accessToken, NSError *error) { if (error) { NSLog(@"出錯了"); return; } NSString *result=[NSString stringWithFormat:@"獲得到的值為:uid:%@--token:%@--openid:%@",uid,accessToken,openid]; [weakSelf showResult:result]; }];
獲取用戶信息的代碼如下:
[JiaPlatformHelper getUserInfoWithPlatform:JiaSocialPlatformType_QQ withCompletion:^(NSString *name, NSString *iconUrl, NSString *gender, NSError *error) { if (error) { NSLog(@"出錯了"); return; } NSString *result=[NSString stringWithFormat:@"獲得到的值為:name:%@--性別:%@",name,gender]; [weakSelf showResult:result]; }];
關於取消授權也有相應的方法:
[JiaPlatformHelper cancelAuthWithPlatform:JiaSocialPlatformType_QQ withCompletion:^(id result, NSError *error) { if (error) { NSLog(@"出錯了"); return; } NSString *ressult=@"取消成功"; [weakSelf showResult:ressult]; }];
模塊化結合私有Pods方案
上面實例中只是把相關模塊化的提取都在一個工程進行體現,最後還是要落實結合Pods進行管理,把每個模塊分開管理,不同的APP可以簡單通過Pods指令就可以達到引入模塊的效果,對於一些相同模塊可以在不同的APP重復引用,減小重復開發成本;
在本項目中已經引入的Pod來管理目前開發的幾個模塊,並導入在我目前的Github的一個庫裡Spec進行統一管理,首先要引入Pod來管理則要增加jiaModule.podspec文件;
Pod::Spec.new do |s| s.name = "jiaModule" s.version = "0.0.6" s.summary = "iOS模塊化功能的引用" s.homepage = "https://github.com/wujunyang/jiaModuleDemo" s.license = { :type => "MIT", :file => "FILE_LICENSE" } s.author = { "wujunyang" => "[email protected]" } s.platform = :ios, "7.0" s.source = { :git => "https://github.com/wujunyang/jiaModuleDemo.git", :tag => "0.0.6" } s.requires_arc = true s.subspec 'JiaCore' do |jiaCore| jiaCore.source_files = 'jiaModuleDemo/BaseModule/JiaCore/**/*.{h,m}' jiaCore.dependency 'XAspect' jiaCore.dependency 'YYCache' jiaCore.dependency 'JSPatch' jiaCore.dependency 'RealReachability' jiaCore.dependency 'FLEX', '~> 2.0' jiaCore.dependency 'CocoaLumberjack', '~> 2.0.0-rc' jiaCore.dependency 'AFNetworking', '~>2.6.0' end s.subspec 'JiaGT' do |jiaGT| jiaGT.source_files = 'jiaModuleDemo/BaseModule/JiaGT/**/*' jiaGT.dependency 'jiaModule/JiaCore' jiaGT.dependency 'XAspect' jiaGT.dependency 'GTSDK', '~> 1.5.0' end s.subspec 'JiaAnalytics' do |jiaAnalytics| jiaAnalytics.source_files = 'jiaModuleDemo/BaseModule/JiaAnalytics/**/*' jiaAnalytics.dependency 'jiaModule/JiaCore' jiaAnalytics.dependency 'XAspect' jiaAnalytics.dependency 'Aspects' jiaAnalytics.dependency 'UMengAnalytics-NO-IDFA', '~> 4.1.1' end s.frameworks = 'UIKit' # s.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" } # s.dependency "JSONKit", "~> 1.4" end
上面的文件會把不同的模塊進行分離,可以一起引入也可以單獨引入某一個模塊;pod會自動把相應的依賴都引入,下面是全部引入關於jiaModule模塊
source 'https://github.com/CocoaPods/Specs.git' source 'https://github.com/wujunyang/WjySpecs.git' platform :ios, "7.0" pod 'jiaModule'
假如要引入只是其中一個模塊:
source 'https://github.com/CocoaPods/Specs.git' source 'https://github.com/wujunyang/WjySpecs.git' platform :ios, "7.0" pod 'jiaModule/JiaCore’ pod 'jiaModule/JiaGT’
下面簡單介紹兩條關於驗證跟提交jiaModule.podspec的指令,都要打開終端進入項目根目錄,也就是jiaModule.podspec所在的目錄,然後進行執行;
#驗證是否正確(後面還有一個git的私有地址) pod lib lint jiaModule.podspec --allow-warnings --use-libraries --sources= #提交到庫 (specs就是你們的私有庫名,見下面repo add指令時的名字,後面還有一個git的私有地址) pod repo push specs jiaModule.podspec --allow-warnings --use-libraries --sources=https://github.com/CocoaPods/Specs.git,https://github.com/wujunyang/WjySpecs.git
注意:如果提交到庫時報下面的問題,說明還沒有把私有倉庫集下載到本地:
[!] Unable to find the `specs` repo. If it has not yet been cloned, add it via `pod repo add`.
可以直接執行指令(specs名字可以自行定義,跟上面提交時對應該上就行):
pod repo add specs https://github.com/wujunyang/WjySpecs.git
會在路徑:/Users/自個電腦用戶名/.cocoapods/repos(被隱藏,要用指令進行顯示出來) 下有一個文件夾specs 另外還有一個是Pod官網的文件夾;
最後還要登錄Git賬號跟密碼,就可以成功提交了
Username for 'https://github.com': [email protected] Password for 'https://[email protected]@github.com': To https://github.com/wujunyang/WjySpecs.git 80ca876..d4f7446 master -> master