IOS元件化方案總結

我是繁星發表於2018-12-26

1.啥是元件化

timg.jpeg
打一個比較形象的比喻,把APP比作我們的人體,把胳膊、大腿、心、肝、肺這些人體器官比作元件,各個器官分別負責他們各自的功能,但是他們之間也有主次之分,試想我們的胳膊、大腿等是不能獨立完成某個任務的,必須需要心、肺、肝、膽等的能量支援,那麼可以把胳膊、大腿這種功能性器官比作業務元件,把我們的心、肝、脾、肺、腎比作基礎元件。 那麼我們的業務元件必須要依賴於我們的基礎元件才能發揮其應有的功能,我們的基礎元件(心、肝、肺等)是高度複用的,胳膊、大腿等業務元件要解耦合,難道你的胳膊動,大腿也要跟著動嗎~,最終由大腦整合(那麼大腦可以類比成主工程)。 不知道您是否領會了精神,綜上就是我們的元件化的思路。

2.為什麼要做元件化

繼續我們上面那個驚悚的例子,但是我們把人變成機器人

鋼鐵俠.jpeg

  • 單獨專案執行除錯更快 ~Tony直接穿戴手部機甲,肯定比穿戴全套的快呀~
  • 各元件自由選擇開發姿勢 ~Tony開發完手部機甲用的MVC,感覺這種不好,開發腿部的時候用MVVM了~
  • 工程可以獨立開發,方便 QA 有針對性地測試 ~Tony研發手部功能,直接把手部穿上測試就好嘍。~
  • 業務分層、解耦使程式碼的可維護性更高 ~Tony想升級手部的鐳射,動手就行了,別的地方都不用管!~
  • 便於各業務功能拆分、抽離,實現真正的功能複用(特別是對於多APP來說更加突出) ~Tony想給殘疾人研發個假腿,直接把腿部拿過來用就好嘍~
  • 業務隔離,跨團隊開發程式碼控制和版本風險控制的實現 ~Tony有錢可以僱好幾個團隊同步開發,你開發胳膊,我開發腿,互不干擾,完事裝到一起就行~

3.元件化的設計原則及目的

~注:這裡的穩定性指通俗地講就是是否需要頻繁修改程式碼~

  • 越底層的模組,應該越穩定,越抽象,越高度複用。 穩定性取決於是否需要頻繁改變,底層庫之所以要穩定是因為其會被業務層元件頻繁呼叫,一旦變化,可能會影響幾乎所有業務層元件,要做到設計一套API很久都不用改變,就需要設計的時候能越抽象, 即需要我們的抽象總結能力。
  • 注意穩定性傳遞 穩定性是可以傳遞的,例如元件A穩定,元件B不穩定,元件A依賴於元件B,那麼A其實也是不穩定的。
  • 自完備性有的時候要優於程式碼複用 緊接上例:如何實現A、B的解耦呢?假設A依賴於B中的X程式碼段 1>. 如果X是相對獨立且高度複用的,我們當然可以將其提取出來如下
    未命名檔案-5.png
    2>.如果X只是一個方法或者函式,並不適合單獨提取出一個模組,那麼直接copy一份X程式碼到B權衡之下也是沒有問題的。

那麼我們的最終目的可以總結為: 在基於模組設計原則上, 讓模組之間沒有迴圈依賴, 讓業務模組之間解除依賴。


4.元件化耦合關係

綜上所述我們專案元件化方案示意圖可以是這樣

未命名檔案-8.png
如上圖業務元件單向耦和於基礎元件,這樣的架構完成了基礎元件的高度複用和業務元件的解耦。但是問題又來了,各個業務元件之間難免有頁面跳轉和資料互動,業務元件不耦合意味著不能直接呼叫,那麼我們引入一箇中間層。
改進耦合圖-9.png
這裡注意,依賴一定是單項的,否則我們只是把融在一起的程式碼塊拆分成多個程式碼塊,而且比之前更麻煩了。
未命名檔案-4.png

5.中間層的實現方案

關於中間層實現眾說紛紜,這裡說下我們實踐的方案。

未命名檔案-10.png
主要分成四部分:

  • routerManager:routerModules(以字串儲存各個module中相應router類的名字,方便用執行時方法呼叫,著也是router與各個模組之間解耦的關鍵),routerMap:維護這url和block之間的對應關係是介面跳轉的關鍵,methodMap:與routerMap的區別就在與block中程式碼塊是呼叫方法的。
//運用的是swift中名稱空間的概念,用執行時方法NSClassFromString獲取到相應的型別
    private static let routerModules:[String] = ["MessageProject.MessageProjectRouter",
                                                 "IMProject.IMProjectRouter",
                                                 "CommunityProject.CommunityProjectRouter",
                                                 "CourseProject.CourseProjectRouter",
                                                 "VideoProject.VideoProjectRouter",
                                                 "QuestionbankProject.QuestionbankProjectRouter",
                                                 "UserinfoProject.UserinfoProjectRouter",
                                                 "CustomUIProject.CustomUIProjectRouter",
                                                 "UIFrameProject.UIFrameProjectRouter",
                                                 "BasicUIServiceProject.BasicUIServiceProjectRouter",    "ActivityOperationProject.ActivityOperationProjectRouter"]
    private static var routerMap:Dictionary<String,[RouterHandler]> = [:]
    private static var methodMap:Dictionary<String,MethodHandler> = [:]
複製程式碼
  • ** RegisterRoutersProtocol:宣告一個通用介面,在各個模組的router類中去實現**
// 每個模組需要實現一個該協議的類,用於模組內部VC和method的註冊
public protocol RegisterRoutersProtocol {
    static func registerModuleRouters()
}
複製程式碼
  • UIViewController+Router:在各個模組的router類中,將需要跳轉的VC進行註冊
// VC註冊,子類需要的話可以重寫
    @objc open class func registerRouterVC(_ routerURL:String)
    {
        guard let tempRouterURL = URL(string:routerURL) else {
            return
        }
        SDJGUrlRouterManager.registerRouterWithHandler(handler: { (transferURL:URL, transferType:SDJGTransfromType, sourceVC:UIViewController, userInfo:[String:Any]?, animated:Bool) -> UIViewController? in
            if transferURL.hasSameTrunkWithURL(tempRouterURL) {
                let viewController = self.init()
                viewController.setRouterInfo(userInfo: userInfo)
                if transferType == .push {
                    if let nav = sourceVC.navigationController {
                        // navController
                        nav.pushViewController(viewController, animated: animated)
                    } else {
                        // modal nav vc
                        sourceVC.modelVC(viewController, true, animated)
                    }
                } else if transferType == .model {
                    sourceVC.modelVC(viewController, false, animated)
                }else if transferType == .modelNav {
                    sourceVC.modelVC(viewController, true, animated)
                } else {
                }
                return viewController
            } else {
                return nil
            }
        }, prefixURL: tempRouterURL)
    }
複製程式碼
  • 各個模組中router類:繼承自NSObject,實現routerProtocol中的註冊方法,在註冊方法中呼叫各自VC的UIViewController+Route擴充套件方法進行跳轉和方法註冊。同時這個類中也維護著key和類的對應關係。
import Foundation
import URLRouteProject
//課程下載介面
public let kCourseFileDownloadURLString = "sina://router/downloadserviceproject/coursefiledownload"
//資料下載介面
public let kDownLoadVCURLString = "sina://router/downloadserviceproject/download"
class DownloadServiceProjectRouter: RegisterRoutersProtocol {
    public static func registerModuleRouters()
    {
        JCourseFileDownLoadVC.registerRouterVC(kCourseFileDownloadURLString)
        JDownLoadVC.registerRouterVC(kDownLoadVCURLString)
    }
    
}
複製程式碼

引數傳遞:為了有更多的型別引數可以傳遞,我們在router跳轉方法裡多加了一個引數,而不是用url拼接的方式,因為這樣的話只能傳遞基本型別引數,像UIImage這種就無能為力了。

// 用於註冊VC Router的閉包定義,會在頁面跳轉的時候執行閉包,引數為[String:Any]型別,這樣引數就可以隨意傳了。
public typealias SDJGRouterHandler = (_ url:URL, _ transferType:SDJGTransfromType, _ sourceVC:UIViewController, _ userInfo:[String:Any]?, _ animated:Bool) -> UIViewController?
複製程式碼

openURL的處理:我們為openURL提供了單獨的方法跳轉,其中包含了引數的解析。


6.IOS元件化實現方案和實際開發運營

cocoapods管理: 程式碼解耦只需要遵循上述原則就好,最根本的目的是業務元件的解耦,cocoapod的原理及使用在這裡不在贅述(一搜一大堆)

d9feb1e6d6503d261c9e90f0fdd942a4.png
正規的方式是 專案工程釋出tag->配置本地podSpec檔案並上傳->校驗->私有庫釋出->其他工程引入。 但在實際操作中有很多情況pod lib link由於種種原因會失敗,而且釋出私有庫本身也需要時間,所以在依賴不變的情況下我們可以用其他的方式引入其他模組程式碼

//拉取對應commit程式碼
pod 'AFNetworking', :git => 'https://github.com/gowalla/AFNetworking.git', :commit => '082f8319af'
//預設拉取dev分支最新程式碼
pod 'AFNetworking', :git => 'https://github.com/gowalla/AFNetworking.git', :branch => 'dev'
//拉取0.7.0tag的程式碼
pod 'AFNetworking', :git => 'https://github.com/gowalla/AFNetworking.git', :tag => '0.7.0'
複製程式碼

直接修改對應提交的commit,這樣同樣缺點也很明顯,需要程式設計師自己保證程式碼無誤才可以提交,不會有pod的校驗,所以這兩點需要我們權衡。 為了提高效率,我們採用開發時提交commit,由各個業務負責人負責維護commit,每個版本發版時釋出私有庫的方式。

    #社群專案 張三
    pod 'CommunityProject',:git => 'http://172.16.117.224/ios-team/communityproject.git', :commit => '15407bae8eccafa14eab4d200e2a8ae763810f15'

    #使用者資訊專案 李四
    pod 'UserinfoProject',:git => 'http://172.16.117.224/ios-team/userinfoproject.git',:commit => 'f1a408cd747e215f0b4fb08b4999edf00570c085'
   
    #活動運營 王二麻子 
    pod 'ActivityOperationProject',:git => 'http://172.16.117.224/ios-team/activityoperationproject.git', :commit => '432a21212fc5d6eed9d5d28eacb320e01ec9cc47'
    
    #課程專案  李六
    pod 'CourseProject',:git => 'http://172.16.117.224/ios-team/courseproject.git', :commit => 'da10da98af8d53bfe15572958cef8d0cf5e5ba2a'
複製程式碼

7.講講坑

實際開發當中會遇到種種坑?如下

  • 注意類和方法及屬性的許可權問題public、pravite等(swift、oc不用)

  • 業務模組當然要有自己的測試入口,否則很多業務場景都沒有入口,這就需要業務負責人自己新增自己的頁面入口,這也是元件化之後的好處,每個業務元件都可以單獨執行,單獨測試,更加輕量級。

  • Unable to satisfy the following requirements:

    3951523621579_.pic_hd.jpg
    這類問題是/Users/xingfan/.cocoapods/repos/master也就是cocoapod的本地索引庫沒有更新最新,裡面沒有Charts(3.1.0)版本的spec檔案,導致它不知道去哪裡拉程式碼。執行pod udate,一般這種問題都是嫌pod update太慢執行pod update --verbose --no-repo-update導致的

  • pod update會主動更新本地repo,如果報錯,可以指定到本地spec倉庫,一般在cd ~/.cocoapods/repos/iosspecrepo,然後git clean -f,如果再有問題,那就是元件間依賴出錯,找相關負責人處理。

  • 最後說說spec倉庫,本身就是一個git倉庫,pod repo update就相當於拉取並同步遠端spec倉庫(git pull),通過其中的spec檔案(描述了目標源所在的地址、tag、依賴庫的版本等)準確的找到想拉取的程式碼。

8.談談優化。

1.用cocoapods的缺點,程式碼整合到主工程後同樣執行緩慢,原因是因為拉取的程式碼依然是需要編譯的,本質上與原本沒有區別。 針對這一點我們可以用Cathage替代cocoapods,CocoaPods (預設)自動建立和更新一個Xcode workspace,用來管理你的專案和所有依賴。Carthage使用xcodebuild來編譯出二進位制庫,剩下的整合工作完全交給開發人員。模組變成可執行的二進位制檔案之後執行速度自然會快很多。 有興趣的同學可以自行研究。

相關文章