2018,我們的元件化實施之路 | 掘金年度徵文

諸葛找房技術團隊發表於2019-01-22

前言:本篇文章是諸葛找房 iOS 技術團隊近半年元件化實施之路的經驗積累與沉澱,近半年來我們的元件化經歷了從0到1的質變,開發方式由原有的多人混合開發逐漸變為了相對獨立的分業務線開發,並且已經初步實現了同一元件在不同專案下的快速整合,元件化帶來的便捷與迅速正在慢慢地向我們鋪展開來,後續充滿想象空間。然而對一個已經線上上運營多年的系統進行元件化,並不是一條容易的路,這一路我們升級打怪,彙集全組人的智慧,及時的調整元件化結構,讓它不至於走彎路。今天我們將主要從工程實施的方方面面與大家分享一點我們的見解,文章較長,建議大家先收藏哈。

首先簡單看下關於元件化選用哪種方案和元件化分層的問題

  • 目前比較流行的大致有3種,RouterProtocolTarget-Action.我們採用了第三種,在此要感謝casa前輩的智慧與無私貢獻。至於選用哪一種,不在今天的討論範圍內,因為無論你打算或者正在使用哪一種,與今天我們要講的都沒有衝突。
  • 元件化一般分3層,從下至上依次是基礎元件、基礎業務元件和業務元件。其中下層不依賴於上層,下層的實現對上層是透明的,上層使用下層提供的服務和介面而不必關心其實現細節,下層不可隨意更改對外的介面,位於同一層的各個實體之間通過協議進行通訊。讀者可以通過諸葛 iOS 技術演進元件化來了解我們的元件化整體架構設計。

一、從工程的角度,如何看待元件化?

將一個工程元件化,就像是重新建造一個結構設計不合理的大樓一樣,這個大樓的各種線路、各種管道都雜糅在一起,承重牆和傢俱東倒西歪,雖然能提供正常的居住服務,但是後續對大樓的的改造與裝修卻很費事。為了以後能容納更多人居住、提供更好的居住體驗,有必要在現在對大樓進行重建。在改造的過程中,仍然需要提供正常的居住服務,因此大樓不能採用爆破的方式完全推到重建,因為那樣的成本過高,只能在每一次版本迭代中進行元件化,元件化對使用者和市場來說是無感知的才對。改造的時候是一個房間一個房間的改造,需要進行房屋的物品歸類、線路拆分、垃圾傾倒等各種準備工作,然後就是把所有相關東西都挪出去進行單獨改造,挪出去的時候要保證整棟大樓不會倒塌,其他的功能不受影響才可以,挪出去的東西要組裝成一間功能獨立的屋子,這個屋子就是一個小的生態系統,後續的維修與改造都只需要care這個屋子就可以;建好這個屋子後,需要再把它放回原處,這間屋子就可以正常住人了。

二、業務元件拆分的幾個步驟

1、元件預處理

預處理需要在主工程中進行,預處理的主要目的是為了給第二步元件從主工程抽離達到單獨執行鋪平道路,由於預處理髮生在主工程中,因此預處理階段不需要考慮程式碼同步的問題,預處理主要包括以下幾個方面:

  • 在主工程中對該元件的所有相關控制器跳轉和服務呼叫進行引用解耦,如果採用的target-action方案,就都通過mediator進行頁面跳轉和服務呼叫,但元件內部可以不必採用這種方式。類似於拆承重牆之前,先從別的地方運一些足夠結實的柱子過來,支撐在原來的地方,保證屋子拆出去後,大樓不會發生倒塌。
  • 在主工程中將該元件涉及到的檔案引用關係梳理清楚,去掉沒有用的引用,同時對該元件按照統一模板建立對應資料夾。這裡的統一模板基本上跟我們主工程的檔案結構一致,都有自己獨立的網路層、儲存層等。總之需要把要拆出來的元件當做一個獨立的專案來看待,一個專案需要有什麼,這個元件就需要有什麼。
  • 按照模板將該元件的所有檔案進行重新歸類,在這過程中需要區分哪些檔案屬於公共的需要下沉的,哪些是這個元件獨有的。因此這個過程會不斷豐富完善公共的基礎業務元件。

2、元件抽離、編譯、執行

  • 首先可以通過pod lib create XXX命令建立一個pod工程, 然後配置該元件的podspec檔案,指明該元件都需要依賴哪些庫。
  • 將經過預處理的的元件抽出來放到Development Pods中的class中,將圖片資源放到Asset中。
  • 給該業務元件設定開發環境變數開關,更改元件中本地資源如圖片、plist等的載入方式,設定元件的程式入口等
  • 如果該元件跟別的業務元件有通訊,那麼還需要提供別的業務元件的介面。如果此時別的業務還沒有拆成元件,那麼建議建立一個介面元件,專門存放那些還沒有抽成元件的業務介面和服務。

3、元件引入

  • 第二步結束,且元件通過初步測試後,就可以將元件以開發庫的方式引入主工程了,此時該業務元件不需要進行發版。關於開發庫的引入方式,下面會再進行介紹。

以上大致的介紹了業務元件拆分的三個大階段,接下來我們再看下元件拆分過程中遇到的一些具體問題,這些問題相信大家在實踐中基本都會遇到。

一、主工程中業務元件的引用方式問題

注:業務元件可以不必進行pod發版,一是因為業務元件開發中頻繁的發版很耗時間,二是業務元件擁有自己的tag,不必通過pod版本號進行控制。可以直接在主工程的podfile中以開發庫的方式引入業務元件,開發庫一般有兩種方式,分別是pathcommitId方式。

  • path方式

    • 簡介:path方式是指業務元件在主工程中以路徑的方式引入,這個路徑可以是相對的也可以是絕對的,一般選用相對路徑,即將所有的元件與主工程都放在一個資料夾下,這樣在主工程中就可以用下面的方式引入業務元件。
      • pod 'ZGAModule', :path=>'../ZGAModule/'
    • 優點:開發庫與遠端是同步的,元件可以直接在主工程裡改,改動後分別在元件裡提交即可。
    • 缺點:所有的開發人員電腦上必須用一套路徑一致的資料夾,想要把主工程執行起來的話,需要將所有的開發庫都下載下來,否則當別人引用了一個我電腦上沒有的元件的時候,我這裡就會報錯;update主工程的時候,必須依次更新所有的開發庫,可能會有遺漏;如果使用了Jenkins打包,會汙染Jenkins環境。
  • commitId方式

    • 簡介:commitId方式要求開發庫在主工程中以該元件遠端倉庫地址的方式引入,可以指定commitId,也可以不指定。不指定commitId的話,就預設始終指向最新的業務程式碼。
      • 指定commitId:pod 'ZGAModule', :git => 'git@git.zhuge.com:iOS/ZGAModule.git', :commit => '1234567'
      • 不指定commitIdpod 'ZGAModule',:git => 'git@git.zhuge.com:iOS/ZGAModule.git'或者pod 'ZGAModule',:git => 'git@git.zhuge.com:iOS/ZGAModule.git', :branch => 'dev'
    • 優點:開發人員本地不需要維護一套路徑一致的工程檔案,想要執行主工程,直接pod install安裝各個元件即可;不侵入Jenkins的環境;
    • 缺點:開發庫與遠端不能同步,元件有改動的話,只能在元件裡改,不能直接在主工程裡改;如果沒有指定commitId,那麼元件更新後,主工程只能用pod update的方式更新該元件,速度較慢;如果指定了commitId,那麼每次改動元件後都得到主工程的podfile中,修改該元件的commitId;
  • 我們採用的方案

    • 以上兩種方案各有優缺點,我們綜合了這兩種方式的優點,我們整體上採用了第二種方案,但沒有指定具體的commitId,這樣就會始終指向最新的業務元件;但podfile中所有的業務元件都有一份以path方式引入的備份,只不過在遠端這些path引用是被註釋掉的。之所以有這些path備份,是為了方便開發人員在本地自由切換,最大限度的提升開發效率。
    • 針對小的改動,開發人員可以在主工程中手動將commitId方式改成path方式,除錯通過後,再將podfile改回去,然後直接提交對應元件的改動。
    • 針對大的改動,建議直接在元件中進行開發與提交,最後在主工程中update該元件即可。
    • 其他方式:對應的可以通過指令碼方式進行元件化工程的installcommitupdate等一系列操作,或者通過指令碼進行兩個不同repo之間的merge操作,但這樣做都需要再開發維護一套指令碼,導致系統複雜性變高。
  • 附:主工程podfile的大致寫法

  #******************** 諸葛私有庫 ********************
  #遠端庫
  #A業務
  pod 'ZGAModule',:git => 'git@git.zhuge.com:iOS/ZGAModule.git'
  pod 'ZGBModule',:git => 'git@git.zhuge.com:iOS/ZGBModule.git'
  pod 'ZGCModule',:git => 'git@git.zhuge.com:iOS/ZGCModule.git'

  # 本地庫(遠端這些本地庫方式一定是被註釋掉的,review的時候如果發現被開啟了,那麼可以將本次提交打回去,或者開發相關指令碼進行監測)
  #A業務
    #   pod 'ZGAModule', :path=>'../ZGAModule/'
    #   pod 'ZGBModule', :path=>'../ZGBModule/'
    #   pod 'ZGCModule', :path=>'../ZGCModule/'

複製程式碼

二、業務元件的開發時機問題

這裡我們主要分享下如何在人員有限、需求不斷地情況下處理好元件化與版本迭代之間的時間衝突。

1、需求開發時間佔比和兩個生命週期

首先我們提出一個簡單的概念,這個概念大家一聽便知:需求開發時間佔比,也就是版本需求開發時間佔當天有效開發總時間的比例,並給出高(80%)中(50%-80%)低(50%以下)三個時間檔。假設我們可以在版本迭代過程中需求開發時間佔比為的所有時間段進行元件化開發。

下面我們再看下版本迭代的大致生命週期,不同公司可能稍微有些差異,但大致是相似的。

版本生命週期-開發時間佔比

一個業務元件的一般生命週期如下 :

元件化生命週期

2、元件的開發時機

元件的開發時機要避開需求開發時間佔比較高的時間段,從上圖中可以看出,元件的開發時間主要是以下兩個時間階段:

  • 版本上線之後至版本新需求開始研發之前
  • 版本整體提測後至版本灰度測試或者臨近上線前

3、 元件生命週期不同階段的時間原則

  • 元件的預處理階段,需要在主工程中進行,不用考慮程式碼不同步問題,並且預處理工作bug率最低,在版本生命週期的任何階段都可以進行
  • 元件預處理階段結束後,需要看版本整體時間是否充分,不充分的話不要把元件從主工程中抽離出來,因為元件相關的程式碼一旦從主工程中抽離出來,元件化的生命週期就一定要在當前版本的生命週期內,而不能拖延到下一個,否則的話需要處理很多程式碼不同步的問題。因此預處理工作結束後,如果時間不夠充分,那就在下個版本,把該元件從主工程中抽離出來。
  • 新的業務元件不需要考慮預處理和原有程式碼的抽離工作,直接在當前版本迭代中開發即可

三、業務元件測試

為了保證線上服務的穩定性,需要對新抽離的元件進行多輪測試,尤其是元件程式碼跟主工程程式碼不同步的情況下,更要加強測試。可以採用三級測試的方式進行測試:

  • 三級測試

    • 元件單元提測(元件開發好後需要該元件的開發人員對元件進行自測)
    • 元件主工程提測 (自測沒有大問題後,可以通知開發過該業務的人需要進行一輪測試)
    • 版本提測(依賴測試人員進行迴歸測試)
  • 測試原則:

    • 功能一致:與線上版本的功能完全一致
    • 程式碼一致性:分模組分頁面一行一行的捋程式碼

四、業務元件化分支合併到matser

  • 一般的,元件化分支是單獨的一個分支,由於元件化工程與master主工程差距較大,很多檔案被移動,進行程式碼合併時必然會產生很多不必要的衝突,因此元件化工程測試完畢後,如果不想解決那些衝突,可以不採取合併到master的方式,而是直接替換master上的工程
  • 業務開發直接在當前的元件化分支中開發,不再從master遷出
  • 等元件化基本抽離完成後,再回到正常的開發方式中

五、元件後續的開發與維護和元件回滾

  • 關於元件後續的開發與維護

    • 原則上只需要在各自元件工程中進行開發,不需要依賴主工程。
    • 要保證元件的基礎生態環境與主工程的基礎生態環境一致;
    • 元件對外暴露的介面一旦成型後,後續不可以輕易改動,但是可以增加新介面。
    • 元件改動後,需要通知主工程的負責人,更新該元件,目前沒有主動通知的機制,後續需要進行完善。
    • 元件後續的開發遵循正常的git開發準則和gerrit程式碼review準則
  • 關於元件回滾

    • 由於各個元件都是一個個獨立的git專案,目前可以做到針對某一個業務進行程式碼回滾,而不必主工程整體回滾,回滾時直接在主工程中重新指定commitId即可

六、業務元件的粒度(由大變小)

  • 工具性以外的業務元件,可以不遵循子庫的設計方式,一是它本身已經很大,都在一個工程裡開發容易產生工程檔案衝突;二是業務之間的引用關係由外界決定,無法肯定兩個業務子庫之間不會產生引用,即便通過mediator引用,也不太好,不如拆成一個一個的小元件

七、關於業務元件中檔案的命名規則

  • 不同專案中引入同一業務元件時,為了降低元件對主工程的侵入性。最好對元件中的類進行重新命名,可以採用元件名+類名的方式命名。如新房業務元件下的新房列表,可以叫做ZGNewHouseModuleNewHouselistVC

八、關於不同專案下同一業務元件的個性化差異解決方案

關於元件的個性化解決方案,目前可供我們選擇的主要有兩種,一個是在元件內部通過環境變數來區分不同端,另一個是通過git的分支進行管理。

兩種方案各有優缺點,至於選用哪一種方案必須從業務當前的相似性業務之後的發展趨勢(只是同步現有的程式碼還是同步以後所有的程式碼、產品之間的差異)程式碼基礎環境相似性程式碼複雜度開發人力等幾個方面綜合考慮,具體分析,不能盲目的選擇。 簡單看下這兩種方案:

關於第一種方案:

元件需要對外暴露一個設定當前App型別的介面,元件儲存該型別後,開發者需要在元件內部有區別的地方通過該App型別進行區分,來展示不同的檢視、提供不同的功能與服務。

優點:

  • 元件只需要有一個分支,各個專案可以用一個地址引用該元件,便於管理。
  • 元件修改提交後,各個業務線都會有更新,只需要改一次即可。

缺點:

  • 每次有新的App,都需要重新定義一個新的App型別,會讓元件內部的程式碼複雜度變高,後續維護成本變高,對新人不友好。
  • 修改一個應用的個性化需求後,存在汙染其他應用的可能性。
  • 元件的基礎環境可能跟不同專案的基礎環境不一致。如A專案要求元件使用1.1版本的三方庫,B專案要求元件使用最新的三方庫,由於無法簡單的強制不同端的基礎環境一致,這種方式會引起庫衝突。
  • 人力分配比較模糊。

關於第二種方案:

業務元件為每一個應用都建立對應的分支,以C端的新房元件為例,master分支為C端的新房,經紀人端的新房從master分出,可以叫做newhouse_agent分支,開發人員在各個分支中進行元件化的差異性開發。共性的東西由master的維護者開發。從現有的C端實際情況來看的話,基本不存在個性化分支合併到master的情況,因為經紀人業務不太可能面向C端使用者,只存在master往別的分支合併的情況,通過git的程式碼合併來同步共性業務。

優點:

  • 不同端的個性化需求通過git多分支進行管理,元件內部,不需要定義App型別,複雜度低,維護成本低。
  • 修改一個應用的個性化需求後,不存在汙染其他應用的情況,因為彼此之間保持獨立。
  • 元件在不同應用下的的基礎環境可以不一致,針對不同專案可以有不同的三方庫版本,甚至不同的三方庫依賴,靈活性較高。
  • 共性業務由master的開發者、一般是元件的建立者維護。個性化需求由對應端的開發者維護,分工明確。原則上允許master合併到其他分支,不允許其他分支合併到master,其他分支和分支之間可以根據業務需求有選擇性的合併。共性業務同樣只需要修改一次即可,原則上master上是個性化最少的元件。

缺點:

  • 一個元件可能會有多個分支,如果用的不是master分支,那麼就必須指定版本號才可以。
  • 組建依賴的相關基礎元件可能都需要有調整。
  • 合併過程中可能出現衝突以及一些不需要的功能,因此對於小的同步,建議直接手動複製、貼上修改,大的同步可以用merge操作,然後進行微調,該操作需要進行估時,納入排期。

流程如下圖所示:

git 多分支管理
綜合分析後,我們整體採用git分支、區域性採用環境變數的方式進行管理。

九、其他細節問題

  • 如何使用指令碼提升效率,我們開發或規劃了哪些指令碼?

    • zg_pod_upload:用於簡化pod庫的發版流程,同時支援元件的本地校驗。
    • zg_pod_initialize:用於快速建立一個元件化工程,合併了pod lib create的幾個命令,並在對應的class檔案下,建立對應的元件模板。
    • zg_file_filter:用於元件引入時的檔案去重。
    • zg_file_replace:檔案重新命名指令碼,用於批量的對業務元件的相關程式碼重新命名。
  • 如何處理每個業務元件的三方庫初始化?

    • 每個元件負責自己三方庫的初始化,所有的三方庫都在各自的業務線中進行初始化,在主工程殼子中呼叫各個業務元件對外暴露的SDK初始化方法即可。
    • 如果三方庫依賴於bundleID,那麼需要為對應的bundleID申請對應的三方庫配置ID和Key
  • 如何處理元件之間的相互跳轉?

    • 現有的Mediator方案本身就支援帶引數、不帶引數、帶返回值、無返回值、帶block回撥、不帶block回撥的呼叫。具體可閱讀casa的系列文章。
  • 業務元件之間如何進行復用?

    • 複用一般分為UI複用、Model複用、資料與服務複用
    • 原則上不提倡通過介面方式進行業務UIModel的複用,如果就是想複用,可以把對應的UIModel下沉到Base層之後再複用。
    • 資料與服務可以通過業務介面的形式複用。
  • 業務元件的介面是否需要與業務程式碼拆開?

    • 業務元件介面與業務程式碼放到一起太容易發生跨域訪問,後續維護問題多,因為開發人員可能為了圖省事,不通過業務介面進行通訊,而直接引入了具體的類檔案。
    • 可以把業務元件的介面做成業務元件的子庫,並且約定規範,業務呼叫時只允許使用該業務元件的介面元件,不允許直接使用業務程式碼元件。
    • 或者單獨建立一個倉庫,裡面存放所有的業務介面。
  • 如何處理業務元件的圖片資源歸屬問題 ?

    • 業務元件自己維護自己的所有圖片,不需要把所有的圖片資源都放到Base層,以免打散後續開發的連貫性。
    • 允許不同業務線之間有重複的圖片
    • 不需要將重複的圖片單獨搞成pod庫
  • 元件之間出現雙向引用或者主庫下的子庫之間出現橫向依賴怎麼辦?

    • 同一層的元件實體之間通過協議進行解耦,需要避免出現這種情況
  • 如何讓分離出來的程式碼與主工程保持同步?

    • 總原則是推遲業務元件從主工程中分離出來的時間點
    • 儘可能的在預處理階段做更多的事情
    • 分離出來後,就需要做好程式碼修改記錄,之後手工進行同步了,這時候應該快速的把它做成pod庫,之後就以元件化方式開發
    • 分模組、分功能進行全鏈路的迴歸測試
  • 如何處理域名與多target問題?

    • 第一種是在各自的元件中自己維護自己的域名,分散管理,缺點是會寫一些邏輯相似的程式碼,每個元件都需要進行環境配置,可能會拖慢啟動速度
    • 第二種是將域名寫在baseModule裡,統一管理,將target的邏輯也寫在baseModule
  • 如果元件A有一部分邏輯沒有完全從主工程中抽離出來,或者元件A引用了還沒有拆出來的元件B,這時候該怎麼辦?(半元件化)

    • 針對第一種情況以將未完全抽離出來的邏輯寫在主工程中A對應的target裡,在A中通過Mediator進行呼叫,後續再進行不斷地拆分
    • 針對第二種情況可以將元件B的介面寫到CommonModuleExports中,元件B的target依然放到主工程中,在A中以Mediator的方式引用B元件,後續再對B元件進行拆分
  • 如何管理通知?

    • 元件內部可以正常使用通知
    • 跨元件的通知需要慎重,儘量不要習慣性的採用通知。

十、關於業務元件的評價標準

標準總是要有的,有了標準,才會有前進的方向。然而目前關於如何評價一個業務元件的好壞,還沒有統一的標準。我們在實踐中試著總結了幾條,供大家交流參考。

  • 元件可單獨編譯與執行,不需要依賴主工程。
  • 元件橫向之間沒有侵入性,元件修改後不會影響跟它位於同一層次的元件,PM不用擔心改了A,壞了B的問題。
  • 元件在不同專案中具有較高的可移植性。可以快速的移植到新的專案或者現有的別的專案中,而不用對別的專案進行較大的改動。

結語

元件化是一個漫長、繁瑣、複雜但有意義的過程,是一項團隊性的工作,建議大家在過程當中加強團隊成員之間的溝通,遇到問題及時解決,及時調整,定好方向後就只管大膽地往前走。同時也歡迎大家與我們溝通交流,希望我們的分享能夠在實踐中幫到大家!預祝大家新年快樂~

掘金年度徵文 | 2018 與我的技術之路 徵文活動正在進行中......

相關文章