iOS App組件化開發(fā)實(shí)踐

前因

其實(shí)我們這個(gè)7人iOS開發(fā)團(tuán)隊(duì)并不適合組件化開發(fā)。原因是因?yàn)樾詢r(jià)比低节预,需要花很多時(shí)間和經(jīng)歷去做這件事,帶來的收益并不能徹底改變什么。但是因?yàn)橛?~3個(gè)星期的空檔期辆苔,并不是很忙;另外是可以用在一個(gè)全新的App上扼劈。所以決定想嘗試下組件化開發(fā)驻啤。

所謂嘗試也就是說:去嘗試解決組件化開發(fā)當(dāng)中的一些問題。如果能解決荐吵,并且有比較好的解決方案骑冗,那就繼續(xù)下去,否則就放棄先煎。

背景

脫離實(shí)際情況去談方案的選型是不合理的贼涩。

所以先簡(jiǎn)單介紹下背景:我們是一家納斯達(dá)克交易所上市的科技企業(yè)。我們公司還有好幾款A(yù)pp薯蝎,由不同的幾個(gè)團(tuán)隊(duì)去維護(hù)遥倦,我們是其中之一。我們這個(gè)團(tuán)隊(duì)是一個(gè)7人的iOS開發(fā)小團(tuán)隊(duì)占锯。作者本人是小組長(zhǎng)袒哥。

之前的App已經(jīng)使用了模塊化(CocoaPods)開發(fā)缩筛,并且已經(jīng)使用了二進(jìn)制化方案。App已經(jīng)在使用自動(dòng)化集成堡称。

雖然要開發(fā)一個(gè)新App瞎抛,但是很多業(yè)務(wù)和之前的App是一樣的或者相似的。

為什么要寫這篇博客却紧?

想把整個(gè)過程記錄下來桐臊,方便以后回顧。

我們的思路和解決方案不一定是對(duì)的或者是最好的啄寡。所以希望大家看了這篇博客之后豪硅,能給我們提供很多建議和別的解決方案,讓我們可以優(yōu)化使得這個(gè)組件化開發(fā)的方案能變得更加好挺物。

技術(shù)棧

gitlab gitlab-runner CocoaPods CocoaPods-Packager fir 二進(jìn)制化 fastlane deploymate oclint Kiwi

成果

使用組件化開發(fā)App之后:

  • 代碼提交更規(guī)范懒浮,質(zhì)量提高。體現(xiàn)在測(cè)試人員反饋的bug明顯減少识藤。
  • 編譯加快砚著。在都是源碼的情況下:原App需要150s左右整個(gè)編譯完畢,然后開發(fā)人員才可以開始調(diào)試痴昧。而現(xiàn)在組件化之后稽穆,某個(gè)業(yè)務(wù)組件只需要10s~20s左右。在依賴二進(jìn)制化組件的情況下赶撰,業(yè)務(wù)組件編譯速度一般低于10s舌镶。
  • 分工更為明確,從而提升開發(fā)效率豪娜。
  • 靈活餐胀,耦合低。
  • 結(jié)合MVVM瘤载。非常細(xì)致的單元測(cè)試否灾,提高代碼質(zhì)量,保證App穩(wěn)定性鸣奔。體現(xiàn)在測(cè)試人員反饋的bug明顯減少墨技。
  • 回滾更方便。我們經(jīng)常會(huì)發(fā)生業(yè)務(wù)或者UI變回之前版本的情況挎狸,以前我們都是checkout出之前的代碼扣汪。而現(xiàn)在組件化了之后,我們只需要使用舊版本的業(yè)務(wù)組件Pod庫(kù)锨匆,或者在舊版本的基礎(chǔ)上再發(fā)一個(gè)Pod庫(kù)私痹。
  • 新人更容易上手。

對(duì)于我來說:

  • 更加容易地把控代碼質(zhì)量统刮。
  • 更加容易地知道小組成員做了些什么紊遵。
  • 更加容易地分配工作。
  • 更加容易地安排新成員侥蒙。

解耦

我們的想法是這樣的暗膜,就算最后做不成組件化開發(fā),把這些應(yīng)該重用的代碼抽出來做成Pod庫(kù)也沒有什么影響鞭衩。所以優(yōu)先做了這一步学搜。

哪些東西需要抽成Pod庫(kù)?

我們之前的App已經(jīng)使用了模塊化(CocoaPods化)開發(fā)论衍。我們已經(jīng)把會(huì)在App之間重用的Util瑞佩、Category、網(wǎng)絡(luò)層和本地存儲(chǔ)等等這些東西抽成了Pod庫(kù)坯台。還有一些和業(yè)務(wù)相關(guān)的炬丸,比如YTXChart,YTXChartSocket;這些也是在各個(gè)App之間重用的蜒蕾。

所以得出一個(gè)很簡(jiǎn)單的結(jié)論:要在App之間共享的代碼就應(yīng)該抽成Pod庫(kù)稠炬,把它們作為一個(gè)個(gè)組件。

我們?nèi)プ屑?xì)查看了原App代碼咪啡,發(fā)現(xiàn)很多東西都需要重用而我們卻沒有把它們組件化首启。

為什么沒有把這些代碼組件化?

因?yàn)楫?dāng)時(shí)沒想好怎么解耦撤摸,舉個(gè)例子毅桃。

有一個(gè)類叫做YTXAnalytics。是依賴UMengAnalytics來做統(tǒng)計(jì)的准夷。
它的耦合是在于一個(gè)方法钥飞。這個(gè)方法是用來收集信息的。它依賴了User冕象,還依賴了currentServerId這個(gè)東西代承。

+ (NSDictionary*)collectEventInfo:(NSString*)event withData:(NSDictionary*)data
{
.......
    return @{
        @"event" : event,
        @"eventType" : @"event",
        @"time" : [[[NSDate date] timeIntervalSince1970InMillionSecond] stringValue],
        @"os" : device.systemName,
        @"osVersion" : device.systemVersion,
        @"device" : device.model,
        @"screen" : screenStr,
        @"network" : [YTXAnalytics networkType],
        @"appVersion" : [AppInfo appVersion],
        @"channel" : [AppInfo marketId],
        @"deviceId" : [ASIdentifierManager sharedManager].advertisingIdentifier.UUIDString,
        @"username" : objectOrNull([YTXUserManager sharedManager].currentUser.username),
        @"userType" : objectOrNull([[YTXUserManager sharedManager].currentUser.userType stringValue]),
        @"company" : [[ServiceProvider sharedServiceProvider].currentServerId stringValue],
        @"ip" : objectOrNull([SSNetworkInfo currentIPAddress]),
        @"data" : jsonStr
    };
}

解決方案是,搞了一個(gè)block渐扮,把獲取這些信息的責(zé)任丟出來论悴。

     [YTXAnalytics sharedAnalytics].analyticsDataBlock = ^ NSDictionary *() {
        return @{
                 @"appVersion" : objectOrNull([PBBasicProviderModule appVersion]),
                 @"channel" : objectOrNull([PBBasicProviderModule marketId]),
                 @"username" : objectOrNull([PBUserManager shared].currentUser.username),
                 @"userType" : objectOrNull([PBUserManager shared].currentUser.userType),
                 @"company" : objectOrNull([PBUserManager shared].currentUser.serverId),
                 @"ip" : objectOrNull([SSNetworkInfo currentIPAddress])
                 };
    };

我們的耦合大多數(shù)都是這種。解決方案都是弄了一個(gè)block墓律,把獲取信息的職責(zé)丟出來到外面膀估。

我們解耦的方式就是以下幾種:

1.把它依賴的代碼先做成一個(gè)Pod庫(kù),然后轉(zhuǎn)而依賴Pod庫(kù)耻讽。有點(diǎn)像是“依賴下沉”察纯。

2.使用category的方式把依賴改成組合的方式。

3.使用一個(gè)block或delegate(協(xié)議)把這部分職責(zé)丟出去。

4.直接copy代碼饼记。copy代碼這個(gè)事情看起來很不優(yōu)雅香伴,但是它的好處就是快。對(duì)于一些不重要的工具方法具则,也可以直接copy到內(nèi)部來用即纲。

初始化

AppDelegate充斥著各種初始化。
比如我們自己的代碼博肋。已經(jīng)只是截取了部分低斋!

    [self setupScreenShowManager];
    
    //event start
    [YTXAnalytics createYtxanalyticsTable];
    [YTXAnalytics start];
    [YTXAnalytics page:APP_OPEN];
    [YTXAnalytics sharedAnalytics].analyticsDataBlock = ^ NSDictionary *() {
        return @{
                 @"appVersion" : objectOrNull([AppInfo appVersion]),
                 .......
                 @"ip" : objectOrNull([SSNetworkInfo currentIPAddress]),
                 };
    };
    
    [self registerPreloadConfig];
    //Migrate UserDefault 轉(zhuǎn)移standardUserDefault到group
    [NSUserDefaults migrateOldUserDefaultToGroup];
    [ServiceProvider sharedServiceProvider];
    
    [YTXChatManager sharedYTXChatManager];
    [ChartSocketManager sharedChartSocketController].delegate = [ChartProvider sharedChartProvider];
 
    //初始化最初的行情集合
    [[ChartProvider sharedChartProvider] addMetalList:[ChartSocketManager sharedChartSocketController].quoteList];
    
    //初始化環(huán)信信息Manager
    [YTXEaseMobManager sharedManager];

比如第三方:

    //注冊(cè)環(huán)信
    [self setupEaseMob:application didFinishLaunchingWithOptions:launchOptions];
    
    //Talking Data
    [self setupTalkingData];
    [self setupAdTalkingData];
    [self setupShareSDK];
    [self setupUmeng];
    [self setupJSPatch];
    [self setupAdhocSDK];
    [YTXGdtAnalytics communicateWithGdt];//廣點(diǎn)通

首先這些初始化的東西是會(huì)被各個(gè)業(yè)務(wù)組件都用到的。

那我組件化開發(fā)的時(shí)候匪凡,每一個(gè)業(yè)務(wù)組件如何保證我使用這些東西的時(shí)候已經(jīng)初始化過了呢膊畴?難道每一個(gè)業(yè)務(wù)組件都初始化一遍?有參數(shù)怎么辦病游,能不能使用單例唇跨?

但問題是第三方庫(kù)基本都需要注冊(cè)一個(gè)AppKey,我每一個(gè)業(yè)務(wù)組件里都寫一份礁遵?那樣肯定不好轻绞,那我配置在主App里的info.plist里面,每一個(gè)業(yè)務(wù)組件都初始化一下好了佣耐,也不會(huì)有什么副作用政勃。但這樣感覺不優(yōu)雅,而且有很多重復(fù)代碼兼砖。萬一某個(gè)AppKey或重要參數(shù)改了奸远,那每一個(gè)業(yè)務(wù)組件豈不是都得改了。這樣肯定不行讽挟。另外一點(diǎn)懒叛,那我的業(yè)務(wù)組件必須依賴主App的內(nèi)容了。無論是在主App里調(diào)試還是把主App的info.plist的相關(guān)內(nèi)容拷貝過來使用耽梅。

更關(guān)鍵的是有一些第三方的庫(kù)需要在application: didFinishLaunchingWithOptions:時(shí)初始化薛窥。

//初始化環(huán)信,shareSDK, 友盟, Talking Data等
[self setupThirdParty:application didFinishLaunchingWithOptions:launchOptions];

有沒有更好的辦法呢眼姐?

首先我寫了一個(gè)YTXModule诅迷。它利用runtime,不需要在AppDelegate中添加任何代碼众旗,就可以捕獲App生命周期罢杉。

在某個(gè)想獲得App生命周期的類中的.m中這樣使用:

YTXMODULE_EXTERN()
{
    //相當(dāng)于load
    isLoad = YES;
}
+ (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(nullable NSDictionary *)launchOptions
{
    //實(shí)現(xiàn)一樣的方法名,但是必須是靜態(tài)方法贡歧。
    return YES;
}

分層

因?yàn)樵诮鉀Q初始化問題的時(shí)候滩租,要先設(shè)計(jì)好層級(jí)結(jié)構(gòu)赋秀。所以這里突然跳轉(zhuǎn)到分層。

上個(gè)圖:


layer

我們自己定了幾個(gè)原則律想。

  • 業(yè)務(wù)組件之間不能有依賴關(guān)系猎莲。
  • 按照?qǐng)D示不能跨層依賴。
  • 所謂弱業(yè)務(wù)組件就是包含著少部分業(yè)務(wù)蜘欲,并且可以在這個(gè)App內(nèi)的各個(gè)業(yè)務(wù)組件之間重用的代碼益眉。
  • 要依賴YTXModule的組件一定要以Module結(jié)尾,而且它一定是個(gè)業(yè)務(wù)組件或是弱業(yè)務(wù)組件姥份。
  • 弱業(yè)務(wù)組件以App代號(hào)開頭(比如PB),以Module結(jié)尾年碘。例:PBBasicProviderModule澈歉。
  • 業(yè)務(wù)組件以App代號(hào)開頭(比如PB)BusinessModule結(jié)尾。例:PBHomePageBusinessModule屿衅。

業(yè)務(wù)組件之間不能有依賴關(guān)系埃难,這是公認(rèn)的的原則。否則就失去了組件化開發(fā)的核心價(jià)值涤久。

弱業(yè)務(wù)組件之間也不應(yīng)當(dāng)有依賴關(guān)系涡尘。如果有依賴關(guān)系說明你的功能劃分不準(zhǔn)確。

初始化設(shè)計(jì)

我們約定好了層級(jí)結(jié)構(gòu)响迂,明確了職責(zé)之后考抄。我們就可以跳回初始化的設(shè)計(jì)了。

創(chuàng)建一個(gè)PBBasicProviderModule弱業(yè)務(wù)組件蔗彤。

  • 它通過依賴YTXModule來捕捉App生命周期川梅。
  • 它來負(fù)責(zé)初始化自己的和第三方的東西。
  • 所有業(yè)務(wù)組件都可以依賴這個(gè)弱業(yè)務(wù)組件然遏。
  • 它來保證所有東西一定是是初始化完畢的贫途。
  • 它來統(tǒng)一管理。
  • 它來暴露一些類和功能給業(yè)務(wù)組件使用待侵。

反正就是業(yè)務(wù)組件中依賴PBBasicProviderModule丢早,它保證它里面的所有東西都是好用的。

因?yàn)橛辛薖BBasicProviderModule秧倾,所以才讓我更明確了弱業(yè)務(wù)組件這個(gè)概念怨酝。

因?yàn)槲覀儜校绻裀BBasicProvider定義為業(yè)務(wù)組件中狂。那它和其他業(yè)務(wù)組件之間的通信就必須通過Bus凫碌、Notification或協(xié)議等等。

但它又肯定是業(yè)務(wù)啊胃榕。因?yàn)槟切〢ppKey肯定是和這個(gè)App有關(guān)系的盛险,也就是App的相關(guān)配置和參數(shù)也可以說是業(yè)務(wù)瞄摊;我需要初始化設(shè)置那些Block依賴User信息、CurrentServerId等等肯定都是業(yè)務(wù)啊苦掘。

那只好搞個(gè)弱業(yè)務(wù)出來啊换帜。因?yàn)槲也荒艽蚱七@個(gè)原則啊:業(yè)務(wù)組件之間不能互相依賴鹤啡。

再進(jìn)一步分清弱業(yè)務(wù)組件和業(yè)務(wù)組件惯驼。

業(yè)務(wù)組件里面基本都有:storyboard、nib递瑰、圖片等等祟牲。弱業(yè)務(wù)組件里面一般沒有。這不是絕對(duì)的抖部,但一般情況是這樣说贝。

業(yè)務(wù)組件一般都是App上某一具體業(yè)務(wù)。比如首頁(yè)慎颗、我乡恕、直播、行情詳情俯萎、XX交易大盤傲宜、YY交易大盤、XX交易中盤夫啊、資訊函卒、發(fā)現(xiàn)等等。而弱業(yè)務(wù)組件是給這些業(yè)務(wù)組件提供功能的涮母,自己不直接表現(xiàn)在App上展示谆趾。

我們還可以創(chuàng)建一些弱業(yè)務(wù)組件給業(yè)務(wù)組件提供功能。當(dāng)然了叛本,不能夠?yàn)E用沪蓬。需要準(zhǔn)確劃分職責(zé)。

最后来候,代碼大概是這樣的:

@implementation PBBasicProviderModule

YTXMODULE_EXTERN()
{
    
}

+ (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(nullable NSDictionary *)launchOptions
{
    [self setupThirdParty:application didFinishLaunchingWithOptions:launchOptions];
    [self setupBasic:application didFinishLaunchingWithOptions:launchOptions];

    return YES;
}

+ (void) setupThirdParty:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        [self setupEaseMob:application didFinishLaunchingWithOptions:launchOptions];
        [self setupTalkingData];
        [self setupAdTalkingData];
        [self setupShareSDK];
        [self setupJSPatch];
        [self setupUmeng];
//        [self setupAdhoc];
    });
}

+ (void) setupBasic:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [self registerBasic];
    
    [self autoIncrementOpenAppCount];
    
    [self setupScreenShowManager];
    
    [self setupYTXAnalytics];
    
    [self setupRemoteHook];
}

+ (YTXAnalytics) sharedYTXAnalytics
{
    return ......;
}
......

設(shè)想

這個(gè)PBBasicProviderModule簡(jiǎn)直就是個(gè)大雜燴啊跷叉,把很多以前寫在AppDelegate里的東西都丟在里面了。毫無優(yōu)雅可言营搅。

的確是這樣的云挟,感覺沒有更好的辦法了。

既然已經(jīng)這樣了转质。我們可不可以大膽地設(shè)想一下:每個(gè)開發(fā)者開發(fā)自己負(fù)責(zé)的業(yè)務(wù)組件的時(shí)候不需要關(guān)心主App园欣。

因?yàn)槲抑烂缊F(tuán)的組件化開發(fā)必須依賴主App的AppDelegate的一大堆設(shè)置和初始化。所以干脆他們就直接在主App中集成調(diào)試休蟹,他們通過二進(jìn)制化和去Pod依賴化的方式讓主App的構(gòu)建非撤锌荩快日矫。

所以我們是不是可以繼續(xù)污染這個(gè)PBBasicProviderModule。不需要在主App項(xiàng)目里的AppDelegate寫任何初始化代碼绑榴?基本或者盡量不在主App里寫任何代碼哪轿?改依賴主App變?yōu)橐蕾囘@個(gè)弱業(yè)務(wù)組件?

按照這個(gè)思路我們搬空了AppDelegate里的所有代碼翔怎。比如一些初始化App樣式的東西窃诉、初始化RootViewController等等這些都可以搬到一個(gè)新的弱業(yè)務(wù)組件里。

而業(yè)務(wù)組件其實(shí)根本不需關(guān)心這個(gè)弱業(yè)務(wù)組件赤套,開發(fā)人員只需要在業(yè)務(wù)組件中的Example App中的AppDelegate中初始化自己業(yè)務(wù)組件的RootViewController就好了飘痛。

其他的事情交給這個(gè)新的弱業(yè)務(wù)組件就好了。而主App和Example App只要在Podfile中依賴它就好了于毙。

所以最后的設(shè)想就是:開發(fā)者不會(huì)去改主App項(xiàng)目敦冬,也不需要知道主App項(xiàng)目。對(duì)于開發(fā)者來說唯沮,主App和業(yè)務(wù)組件之間是隔絕的。

有一個(gè)更大的好處堪遂,我只要更換這個(gè)弱業(yè)務(wù)組件介蛉,這個(gè)業(yè)務(wù)組件就能馬上適配一個(gè)新App。這也是某種意義上的解耦溶褪。

Debug/Release

誰說不用在主App里的AppDelegate寫任何代碼的币旧,打臉。猿妈。吹菱。

我們?cè)趯?duì)二進(jìn)制Pod庫(kù)跑測(cè)試的發(fā)現(xiàn),源碼能過彭则,二進(jìn)制(.a)不能過鳍刷。百思不得其解,然后仔細(xì)查看代碼俯抖,發(fā)現(xiàn)是這個(gè)宏的鍋:

#ifdef DEBUG

#endif

DEBUG在編譯階段就已經(jīng)決定了输瓜。二進(jìn)制化的時(shí)候已經(jīng)編譯完成了。
而我們的代碼中充滿著#ifdef DEBUG 就這樣這樣芬萍。那怎么辦尤揣,這是二進(jìn)制化的鍋。但是我們的二進(jìn)制化已經(jīng)形成了標(biāo)準(zhǔn)柬祠,大家都自覺會(huì)這么做北戏,怎么解決這個(gè)問題呢。

解決方案是:

創(chuàng)建了一個(gè)PBEnvironmentProvider漫蛔。大家都去依賴它嗜愈。

然后原來判斷宏的代碼改成這樣:

if([PBEnvironmentProvider testing])
{
//...
}

在主App的AppDelegate中這樣:

#if DEBUG && TESTING
//PBEnvironmentProvider提供的宏
CONFIG_ENVIRONMENT_TESTING
#endif

原理是:如果AppDelegate有某個(gè)方法(CONFIG_ENVIRONMENT_TESTING宏會(huì)提供這個(gè)方法)旧蛾,[PBEnvironmentProvider testing]得到的結(jié)果就是YES。

為什么要寫在主App里呢芝硬?其實(shí)也可以丟在PBBasicProviderModule里面蚜点,提供一個(gè)方法啊。

因?yàn)橹鰽pp的AppDelegate.m是源碼拌阴,未經(jīng)編譯绍绘。另外注意TESTING這個(gè)宏。我們可以在xcode設(shè)置里加一個(gè)macro參數(shù)TESTING迟赃,并且修改為0的情況下陪拘,能夠生成一個(gè)實(shí)際是DEBUG的App但里面內(nèi)容卻是線上的內(nèi)容。

這個(gè)需求是來自于我們經(jīng)常需要緊急通過xcode直接build一個(gè)app到手機(jī)上以解決或確認(rèn)線上的問題纤壁。

雖然打臉了左刽,但是也還好,以后也不用改了酌媒。再說這個(gè)是特殊需求欠痴。除了這個(gè)之外,主App沒有其他代碼了秒咨。

業(yè)務(wù)組件間通信

我們解決了初始化和解耦的問題喇辽。接下來只要解決組件間通信的問題就好了。

然后我找了幾個(gè)第三方庫(kù)雨席,選用了MGJRouter菩咨。本來直接依賴它就好了。

后來覺得都使用Block的方式會(huì)導(dǎo)致這樣的代碼陡厘,全部堆在了一個(gè)方法里:

+ (void) setupRouter
{
......
[MGJRouter registerURLPattern:@"mgj://foo/a" toHandler:^(NSDictionary *routerParameters) {
    NSLog(@"routerParameterUserInfo:%@", routerParameters[MGJRouterParameterUserInfo]);
}];
[MGJRouter registerURLPattern:@"mgj://foo/b" toHandler:^(NSDictionary *routerParameters) {
    NSLog(@"routerParameterUserInfo:%@", routerParameters[MGJRouterParameterUserInfo]);
}];
......
}

這樣感覺很不爽抽米。那我干脆就把MGJRouter代碼復(fù)制了下來,把Block改成了@selector糙置。并且把它直接加入了YTXModule里面云茸。并且使用了宏,讓結(jié)果看起來優(yōu)雅些罢低。代碼看起來是這樣的:

//在某個(gè)類的.m里查辩,其實(shí)并不需要繼承YTXModule也可以使用該功能
YTXMODULE_EXTERN_ROUTER_OBJECT_METHOD(@"object1")
{
    YTXMODULE_EXAPAND_PARAMETERS(parameters)
    NSLog(@"%@ %@", userInfo, completion);
    isCallRouterObjectMacro2 = YES;
    return @"我是個(gè)類型";
}

YTXMODULE_EXTERN_ROUTER_METHOD(@"YTX://QUERY/:query")
{
    YTXMODULE_EXAPAND_PARAMETERS(parameters)
    NSLog(@"%@ %@", userInfo, completion);
    testQueryStringQueryValue = parameters[@"query"];;
    testQueryStringNameValue = parameters[@"name"];
    testQueryStringAgeValue = parameters[@"age"];
}

調(diào)用的時(shí)候看起來是這樣的:

 [YTXModule openURL:@"YTX://QUERY/query?age=18&name=CJ" withUserInfo:@{@"Test":@1} completion:nil];

 NSString * testObject2 = [YTXModule objectForURL:@"object1" withUserInfo:@{@"Test":@2}];

通信問題解決了。其實(shí)頁(yè)面跳轉(zhuǎn)問題也解決了网持。

頁(yè)面跳轉(zhuǎn)

頁(yè)面跳轉(zhuǎn)解決方案與業(yè)務(wù)組件之間通信問題是一樣的宜岛。

但是需要注意的是,你一個(gè)業(yè)務(wù)組件內(nèi)部的頁(yè)面跳轉(zhuǎn)也請(qǐng)使用URL+Router的方式跳轉(zhuǎn)功舀,而不要自己直接pushViewController萍倡。

這樣的好處是:如果將來某些內(nèi)部跳轉(zhuǎn)頁(yè)面需要給其他業(yè)務(wù)組件調(diào)用,你就不需要再注冊(cè)個(gè)URL了辟汰。因?yàn)楸緛砭陀小?/p>

是否去Model化

去Model化主要體現(xiàn)在業(yè)務(wù)組件間通信列敲,要不要傳一個(gè)Model過去(傳過去的Dictionary中的某個(gè)鍵是Model)阱佛。

如果去Model化,這個(gè)業(yè)務(wù)組件的開發(fā)者如何確定Dictionary里面有哪些內(nèi)容分別是什么類型呢戴而?那需要有個(gè)地方傳播這些信息凑术,比如寫在頭文件,wiki等等所意。

如果不去Model化的話淮逊,就需要把這個(gè)Model做成Pod庫(kù)。兩個(gè)業(yè)務(wù)組件都去依賴它扶踊。

最后決定不去Model泄鹏。因?yàn)閷?shí)際上有一些Model就是在各個(gè)業(yè)務(wù)組件之間公用的(比如User),所以肯定就會(huì)有Model做成Pod庫(kù)秧耗。我們可以把它做成重Model备籽,Model里可以帶網(wǎng)絡(luò)請(qǐng)求和本地存儲(chǔ)的方法。唯一不能避免的問題是分井,兩個(gè)業(yè)務(wù)組件的開發(fā)者都有可能去改這個(gè)Model的Pod庫(kù)车猬。

信息的披露

跳轉(zhuǎn)的頁(yè)面需要傳哪些參數(shù)?
業(yè)務(wù)組件之間傳遞數(shù)據(jù)時(shí)候本質(zhì)的載體是什么尺锚?

不同業(yè)務(wù)開發(fā)者如何知曉這些信息诈唬。

使用去Model化和不使用去Model化,我們都有各自的方案缩麸。

去Model化,則披露頭文件赡矢,在頭文件里面寫詳細(xì)的注釋杭朱。

如果不去Model化,則就看Model就可以了吹散。如有特殊情況弧械,那也是文檔寫在頭文件內(nèi)。

總結(jié)的話:信息披露的方式就是把注釋文檔寫在頭文件內(nèi)空民。

組件的生命周期

業(yè)務(wù)組件的生命周期和App一樣刃唐。它本身就是個(gè)類,只暴露類方法界轩,不存在需要實(shí)例画饥,所以其實(shí)不存在生命周期這個(gè)概念。而它可以使用類方法創(chuàng)建很多ViewController浊猾,ViewController的生命周期由App管理抖甘。哪怕這些ViewController之間需要通信,你也可以使用Bus/YTXModule/協(xié)議等等方式來做葫慎,而不應(yīng)該讓業(yè)務(wù)組件這個(gè)類來負(fù)責(zé)他們之間的通信衔彻;也不應(yīng)該自己持有ViewController薇宠;這樣增加了耦合。

弱業(yè)務(wù)組件的生命周期由創(chuàng)建它的對(duì)象來管理艰额。按需創(chuàng)建和ARC自動(dòng)釋放澄港。

基礎(chǔ)功能組件和第三方的生命周期由創(chuàng)建它的對(duì)象來管理。按需創(chuàng)建和ARC自動(dòng)釋放柄沮。

版本規(guī)范

我們自己定的規(guī)則回梧。

所有Pod庫(kù)都只依賴到minor

"~> 2.3"

主App中精確依賴到patch

"2.3.1"

參考:Semantic Versioning RubyGems Versioning Policies

二進(jìn)制化

二進(jìn)制化我認(rèn)為是必須的,能夠加快開發(fā)速度铡溪。

而我使用的這個(gè)二進(jìn)制方案

有個(gè)坑就是在gitlab-runner上在二進(jìn)制和源碼切換時(shí)漂辐,經(jīng)常需要pod cache clean --all,test/lint/publish才能成功棕硫。而每次pod cache clean --all之后CocoaPods會(huì)去重新下載相關(guān)的pod庫(kù)髓涯,增加了時(shí)間和不必要的開銷。

我們現(xiàn)在通過podspec中增加preserve_paths和執(zhí)行download_zip.sh解決了cache的問題哈扮。原理是讓pod cache既有源碼又有二進(jìn)制.a纬纪。具體可以看ytx-pod-template項(xiàng)目中的Name.podspecdownload_zip.sh

二進(jìn)制化還得注意宏的問題滑肉。小心使用宏包各,尤其是#ifdef。避免源碼和二進(jìn)制代碼運(yùn)行的結(jié)果不一樣靶庙。

集成調(diào)試

集成調(diào)試很簡(jiǎn)單问畅。每一個(gè)業(yè)務(wù)組件在自己的Example App中調(diào)試。

這個(gè)業(yè)務(wù)組件的podspec只要寫清楚自己依賴的庫(kù)有哪些六荒。剩下的其他業(yè)務(wù)組件應(yīng)該寫在Example App的Podfile里面护姆。

依賴的Pod庫(kù)都是二進(jìn)制的。如有問題可以裝源碼(IS_SOURCE=1 pod install)來調(diào)試掏击。

開發(fā)人員其實(shí)只需要關(guān)心自己的業(yè)務(wù)組件卵皂,這個(gè)業(yè)務(wù)組件是自洽的。

公共庫(kù)誰來維護(hù)的問題

這個(gè)問題在我們這種小Team不存在砚亭。沒有仔細(xì)地去想過灯变。但是只要做好代碼準(zhǔn)入(Test/Lint/Code Review)和權(quán)限管理就應(yīng)該不會(huì)存在大的問題。

單元測(cè)試

單元測(cè)試我們用的是Kiwi捅膘。
結(jié)合MVVM模式添祸,對(duì)每一個(gè)業(yè)務(wù)組件的ViewModel都進(jìn)行單元測(cè)試。每次push代碼篓跛,gitlab-runner都會(huì)自動(dòng)跑測(cè)試膝捞。一旦開發(fā)人員發(fā)現(xiàn)測(cè)試掛了就能夠及時(shí)找到問題。也可以很容易的追溯哪次提交把測(cè)試跑掛了。

這也是我們團(tuán)隊(duì)的強(qiáng)制要求蔬咬。沒有測(cè)試鲤遥,測(cè)試寫的不好,測(cè)試掛了林艘,直接拒絕merge request盖奈。


gitlab-runner-test

lint

對(duì)每一個(gè)組件進(jìn)行l(wèi)int再發(fā)布,保證了正確性狐援。這也是一步強(qiáng)制要求钢坦。

lint的時(shí)候能夠發(fā)現(xiàn)很多問題。通常情況下不允許warning出現(xiàn)的啥酱。如果不能避免(比如第三方)請(qǐng)用--allow-warnings爹凹。

pod lib lint --sources=$SOURCES --verbose --fail-fast --use-libraries

統(tǒng)一的網(wǎng)絡(luò)服務(wù)和本地存儲(chǔ)方式

這個(gè)就很簡(jiǎn)單。把這兩個(gè)部分抽象成幾個(gè)Pod庫(kù)供所有業(yè)務(wù)組件使用就好了镶殷。
我們這邊分別是三個(gè)Pod庫(kù):

  • YTXRequest
  • YTXRestfulModel
  • NSUserDefault+YTX

其他一些內(nèi)容

ignore了主App中的Podfile.lock盡量避免沖突禾酱。

主App Archive的時(shí)候要使用源碼,而不是二進(jìn)制绘趋。

后期可以使用oclint和deploymate檢查代碼颤陶。

使用fastlane match去維護(hù)開發(fā)證書。

一些需要從plist或者json讀取配置的Pod庫(kù)模塊陷遮,要注意讀出來的內(nèi)容最好要加一個(gè)namespace滓走。namespace可以是這個(gè)業(yè)務(wù)組件的名字。

業(yè)務(wù)組件讀取資源文件的區(qū)別

#從main bundle中取帽馋。如果圖片希望在storyboard中被找到搅方,使用這種方式。
s.resource = ["#{s.name}/Assets/**"]

#只是希望在我這個(gè)業(yè)務(wù)組件的bundle內(nèi)使用的plist绽族。作為配置文件腰懂。這是官方推薦方式。
s.resource_bundles = {
  "{s.name}/" => ["{s.name}/Assets/config.plist"]
}

持續(xù)集成

原來的App就是持續(xù)集成的项秉。想當(dāng)然的,我們希望新的組件化開發(fā)的App也能夠持續(xù)集成慷彤。

Podfile應(yīng)該是這樣的:這里面出現(xiàn)的全是私有Pod庫(kù)娄蔼。

pod 'YTXRequest', '2.0.1'
pod 'YTXUtilCategory', '1.6.0'

pod 'PBBasicProviderModule', '0.2.1'
pod 'PBBasicChartAndSocketModule', '0.3.1'
pod 'PBBasicAppInitModule', '0.5.1'
...

pod 'PBBasicHomepageBusinessModule', '1.2.15'
pod 'PBBasicMeBusinessModule', '1.2.10'
pod 'PBBasicLiveBusinessModule', '1.2.1'
pod 'PBBasicChartBusinessModule', '1.2.6'
pod 'PBBasicTradeBusinessModule', '1.2.7'
...

如果Pod依賴的東西特別特別多,比如100多個(gè)底哗。另外又必須依賴主App做集成調(diào)試岁诉。
你也可以用這種方案:把你所有的Pod庫(kù)的依賴都展開寫到主App的Podfile中。而發(fā)布Pod庫(kù)時(shí)podspec中不帶任何的依賴的跋选。這樣就避免了pod install的時(shí)候解析依賴特別耗時(shí)的問題涕癣。

各個(gè)腳本都在這個(gè)ytx-pod-template。先從.gitlab-ci.yml看起前标。

我們持續(xù)集成的工具是gitlab runner坠韩。

持續(xù)集成的整個(gè)流程是:

第一步:

使用template創(chuàng)建Pod距潘。像這樣:

pod lib create <Pod庫(kù)名稱> --template-url="http://gitlab.baidao.com/pods/ytx-pod-template"

第二步:

創(chuàng)建dev分支。用來開發(fā)只搁。

第三步:

每次push dev的時(shí)候會(huì)觸發(fā)runner自動(dòng)跑Stage: Init Lint(中的test)

gitlab-runner-init-test

第四步:

1.準(zhǔn)備發(fā)布Pod庫(kù)音比。修改podspec的版本號(hào),打上相應(yīng)tag氢惋。
2.使用merge_request.sh向master提交一個(gè)merge request洞翩。


gitlab-runner-merge-request

第五步:

1.其他有權(quán)限開發(fā)者code review之后,接受merge request焰望。
2.master合并這個(gè)merge request
3.master觸發(fā)runner自動(dòng)跑Stage: Init Package Lint ReleasePod UpdateApp

第六步:

如果第五步正確骚亿。主App的dev分支會(huì)收到一個(gè)merge request,里面的內(nèi)容是修改Podfile熊赖。
圖中內(nèi)容出現(xiàn)了AFNetworking等是因?yàn)檫@個(gè)時(shí)候在做測(cè)試来屠。


gitlab-runner-merge-request-to-app

第七步:

主App觸發(fā)runner,會(huì)構(gòu)建一個(gè)ipa自動(dòng)上傳到fir秫舌。

Init

  • 初始化一些環(huán)境的妖。
  • 打印一些信息。

Package

  • 二進(jìn)制化打包成.a

Lint

  • Pod lib lint足陨。二進(jìn)制和源碼都lint嫂粟。
  • 測(cè)試。
  • 以后考慮加入oclint和deploymate墨缘。

ReleasePod

  • 把相關(guān)文件zip后星虹,傳到靜態(tài)服務(wù)器庫(kù)。以提供二進(jìn)制化下載包镊讼。
  • pod repo push宽涌。發(fā)布該P(yáng)od庫(kù)。

ReleasePod的時(shí)候不允許Pod庫(kù)出現(xiàn)警告蝶棋。

UpdateApp

  • 下載App代碼
  • 修改Podfile文件卸亮。如果匹配到pod庫(kù)文件名則修改,否則添加玩裙。
  • 生成一個(gè)merge request到主App的dev分支兼贸。

關(guān)于gitlab runner吃溅。

stage這個(gè)功能非常的厲害溶诞。強(qiáng)烈推薦。

每一個(gè)stage可以跑在不同的runner上。每一個(gè)stage失敗了可以單獨(dú)retry枉圃。而某一個(gè)stage里面的任務(wù)可以并行執(zhí)行:(test和lint就是并行的)


gitlab-runner-stage
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末赁酝,一起剝皮案震驚了整個(gè)濱河市酌呆,隨后出現(xiàn)的幾起案子隙袁,更是在濱河造成了極大的恐慌娜饵,老刑警劉巖箱舞,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件肺魁,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門分俯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來东亦,“玉大人嫉鲸,你說我怎么就攤上這事。” “怎么了司志?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵拓型,是天一觀的道長(zhǎng)册养。 經(jīng)常有香客問我帐我,道長(zhǎng),這世上最難降的妖魔是什么碳柱? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任的圆,我火速辦了婚禮,結(jié)果婚禮上消痛,老公的妹妹穿的比我還像新娘。我一直安慰自己阅羹,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著媳拴,像睡著了一般僻造。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上宝泵,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天好啰,我揣著相機(jī)與錄音,去河邊找鬼儿奶。 笑死框往,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的闯捎。 我是一名探鬼主播椰弊,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼隙券!你這毒婦竟也來了男应?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤娱仔,失蹤者是張志新(化名)和其女友劉穎沐飘,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體牲迫,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡耐朴,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了盹憎。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片筛峭。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖陪每,靈堂內(nèi)的尸體忽然破棺而出影晓,到底是詐尸還是另有隱情,我是刑警寧澤檩禾,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布挂签,位于F島的核電站,受9級(jí)特大地震影響盼产,放射性物質(zhì)發(fā)生泄漏饵婆。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一戏售、第九天 我趴在偏房一處隱蔽的房頂上張望侨核。 院中可真熱鬧草穆,春花似錦、人聲如沸搓译。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽些己。三九已至诗祸,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間轴总,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工博个, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留怀樟,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓盆佣,卻偏偏與公主長(zhǎng)得像往堡,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子共耍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容

  • 以下討論內(nèi)容都基于這個(gè)方案 問題:沒法使用closing issues via commit messages怎么...
    曹俊_413f閱讀 575評(píng)論 0 0
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,527評(píng)論 25 707
  • 前因其實(shí)我們這個(gè)7人iOS開發(fā)團(tuán)隊(duì)并不適合組件化開發(fā)茅郎。原因是因?yàn)樾詢r(jià)比低呢撞,需要花很多時(shí)間和經(jīng)歷去做這件事,帶來的收...
    其實(shí)也沒有閱讀 717評(píng)論 0 3
  • 重提家風(fēng),就是重拾價(jià)值觀楷兽,讓家風(fēng)內(nèi)化為道德修養(yǎng),外化為行為規(guī)范据沈。這樣莺奔,家風(fēng)自然會(huì)向民風(fēng)輻射,民風(fēng)自然會(huì)向社會(huì)風(fēng)氣延...
    木木林周閱讀 806評(píng)論 0 1
  • 這個(gè)短頭發(fā)遗淳,紅火朝氣上衣與同色耳飾品的女生是今天的時(shí)間官. 時(shí)間官拍柒,據(jù)說技術(shù)含量低又勞神的角色,這么美的女生選擇這...
    索亞之聲閱讀 375評(píng)論 0 1