聚美組件化實踐之路

從去年開始,就陸陸續續的愈來愈多的app開始進行了組件化重構。也有不少很是好的組件化方案博客分享,因此這篇文章並不以介紹組件化方案做爲主題,而是咱們應該如何一步步的從一個古老的項目,慢慢一步步拆分,完成組件化重構的。java

組件化的思想是好的,可是並非全部的項目都適合使用組件化的方式進行開發,因此通常須要使用組件化的項目。基本都是具有項目迭代時期久遠、項目大而臃腫、項目組成員多溝通成本大、項目複雜維護成本很高等特色。這類的項目纔會有組件化的用武之地。android

而其餘的一些人員少、功能簡單的小項目。就別去直接考慮組件化了。老老實實直接擼碼就好了。強行使用組件化只會增長維護成本與開發成本。得不償失~git

組件化結構

組件化歷來不是一個說重構就能重構的東西,在進行組件化重構以前。最好先對組件化的結構有一個基本的理解:github

上圖爲組件化最基本的結構。大體能夠看出。組件化主要分爲三層:json

  1. app殼:api

    此爲組件化的運行容器,殼中定義app入口,依賴業務組件進行運行。安全

  2. 業務組件:cookie

    此爲組件化的中間層,在一個大型項目組中。都有細分下來的不一樣的業務組,好比管登陸的、管購物的、管視頻的等等。這些不一樣業務組分別維護一個各自的業務組件,以達到各自業務組業務解耦的效果。網絡

    原則上來講:各個業務組件之間不能有直接依賴!全部的業務組件均須要能夠作到獨立運行的效果。對於測試的時候,須要依賴多個業務組件的功能進行集成測試的時候。可使用app殼進行多組件依賴管理運行。app

  3. 基礎組件:

    基礎組件也叫基礎功能組件。此部分組件爲上層業務組件提供基本的功能支持。如基礎網絡組件、基礎視圖組件、基礎數據存取組件等,以及組件化的核心通訊組件:路由組件。

以上便是組件化的最基本結構,固然在真正的項目之中,不可能會存在這麼簡單的結構,都是須要根據你的具體現狀進行擴充的。好比你能夠在基礎組件與業務組件之間,添加一層特殊的功能組件。此層的功能組件只被一個或者多個組件進行依賴,只要不破壞這層由下到上單向依賴鏈便可

準備

組件化重構歷來都不是說重構就能重構的,首先得有個強有力的領導去支持執行,而後你纔可能去具體的進行重構。

其次,你得提早對大家的項目進行大方向的分層結構劃分,哪些東西須要放在什麼層。須要提早有個明確的劃分。

重構你的基礎組件:即你的各類基礎功能框架須要提早從項目中拆分出來。包括網絡、圖片加載、數據存儲、埋點、路由等。

創建組件化項目結構

創建基礎組件合集

你須要建立一個基礎library module。用於依賴全部的基礎功能組件,如baselib。

做用:用於統一依賴基礎功能庫,並統籌、關聯好各功能框架的關係,作好各功能庫的初始化封裝操做。提供上層業務組件直接調用。

建立各自業務線的業務組件及app殼

與普通的組件化方案作法不一樣,普通的組件化方案是使用一個變量進行控制。使得業務組件能夠在application與library之間進行靈活切換。使得組件也是application。application也是組件。

可是這種作法,由於老是在libraryModule與applicationModule之間進行切換。很容易致使各類混亂問題:好比Manifest衝突,R文件衝突等。

因此咱們採用的是多app殼分組加載的方式:

能夠看到,每一個業務線的業務組件。都分別有一個各自的app殼模塊。而主app殼依賴全部的業務組件. 在進行業務開發時。各自業務組成員能夠直接運行各自的app殼模塊進行測試,主app殼進行全量打包。

在拆分初期,這個時候的建議以本來的項目application做爲主app殼

預留一個核心業務組件出來。好比登陸組件:此類組件爲業務組件,可是又被全部其餘組件所須要,因此將其單獨做爲核心業務組件獨立出來。而後別的業務組件。經過各自的app殼工程。依賴進入便可:再次提醒業務組件之間不能直接進行依賴

這種分層結構的好處有:

  1. 業務組件再也不在library與application之間進行切換。開發環境統一,不易出現環境切換衝突
  2. app殼單獨獨立出來。能夠在殼工程中添加一些特有的獨立代碼,因爲各自的殼功能不會參與到主app殼中去進行編譯,全部這裏面你能夠針對各自的業務。添加一些獨立的入口管理類。好比添加一個RootActivity,在此添加一個能夠跳轉到任意頁面的列表,方便進行測試運行等。

gradle統一配置管理

組件化重構後,module變多了,因此就須要對全部module的一些gradle腳本進行統一配置管理。避免混亂。

  • 新建dependencies.gradle腳本。添加統一的依賴版本號管理:
ext {
    COMPILE_SDK_VERSION = 25
    BUILD_TOOLS_VERSION = '25.0.0'
    MIN_SDK_VERSION = 16
    TARGET_SDK_VERSION = 19
    
    // SUPPORT
    SUPPORT_VERSION = '23.2.0'
    SUPPORTDEPS = [
            supportV4 : "com.android.support:support-v4:${SUPPORT_VERSION}",
            supportV13 : "com.android.support:support-v13:${SUPPORT_VERSION}",
            appcompatV7 : "com.android.support:appcompat-v7:${SUPPORT_VERSION}",
            cardview : "com.android.support:cardview-v7:${SUPPORT_VERSION}",
            design : "com.android.support:design:${SUPPORT_VERSION}",
            annotations : "com.android.support:support-annotations:${SUPPORT_VERSION}",
            multidex : 'com.android.support:multidex:1.0.1'
    ]
    ...
}
複製代碼

此腳本統一配置管理全部的版本號相關的數據。外部須要使用版本號及依賴時,須要統一今後文件配置屬性中進行讀取。好比要依賴supportV4包:

compile "${SUPPORTDEPS.supportV4}"
複製代碼
  • 定義baseConfig.gradle。統一配置組件基礎編譯腳本
boolean isAppModule = project.plugins.hasPlugin('com.android.application')
android {
    compileSdkVersion Integer.parseInt("${COMPILE_SDK_VERSION}")
    buildToolsVersion "${BUILD_TOOLS_VERSION}"

    lintOptions {
        abortOnError false
    }
    defaultConfig {
        if (isAppModule) {
            applicationId "com.haoge.component.demo"
        }
        minSdkVersion Integer.parseInt("${MIN_SDK_VERSION}")
        targetSdkVersion Integer.parseInt("${TARGET_SDK_VERSION}")

        versionCode Integer.parseInt("${DEFAULE_CONFIG.versionCode}")
        versionName "${DEFAULE_CONFIG.versionName}"
    }
}
複製代碼

這樣。就可使用apply語法。讓全部組件module。都統一依賴此gradle腳本。進行統一環境配置了。

細心點的能夠發現。我在baseConfig中,添加了默認的applicationId的指定。這是由於對於大部分應用而言。都有用過各類的第三方sdk。特別是第三方登陸,這種的sdk框架。不少都會須要進行包名驗證的,因此建議有此種狀況的,在此添加上默認的applicationId指定較好。

若是有嫌麻煩的又動手能力強的。能夠考慮本身封裝個gradle插件來進行統一配置管理

爲組件添加資源前綴

咱們須要對各自的組件,分別設置他自身的資源前綴來做爲命名約束,避免出現不一樣的組件對不一樣的資源起了同一個命名,致使編譯衝突等問題。

android {
    resourcePrefix 'lg_'
}
複製代碼

這個資源前綴的做用是:當你在該module下建立了一個資源命名時,若名字不能與此前綴進行匹配,則將會進行即時提醒。避免衝突。

大文件資源、圖片資源統一管理

組件化以後。資源管理也是個問題,圖片資源、assets資源、raw文件資源等。都具備佔用資源大、基本不多修改等特色。因此這裏最好將其單獨拆分出來。統一提供給全部組件進行使用:

因此,能夠考慮將此類大文件資源,統一放入組件化的最底層。使得不一樣組件不用本身單獨維護一份此大文件資源。避免資源浪費的現象。好比能夠直接將此部分資源。直接放入baselib中,做爲基礎功能提供庫進行使用。

作好各組件的application派發

可能有人會問:爲何要作組件的application的生命週期派發?

舉個栗子:都知道。網絡庫、圖片加載庫等,都須要進行對應的初始化操做才能進行使用的,可是在組件化中,若是不進行各自application的派發。不能進行一個統一流程的初始化操做。那麼可能你組件A須要本身手動寫基礎庫的初始化操做。組件B、組件C也須要。最後你的主app殼也須要,這個時候。就容易亂了!

因此須要有個結構。來讓各自的組件。分別完成自身的組件的功能初始化。

好比基礎功能組件:初始化網絡、圖片框架等,上層的業務組件A,初始化自身的其餘功能操做。各自的組件分別只初始化自身這部分的操做。而不用管所依賴的其餘組件須要進行什麼初始化。

這部分的生命週期派發能夠參考demo中的baselib的delegate包下的類:

demo連接放在了文章末尾。

組件間通訊

路由通訊

組件間通訊的核心是路由框架,這部分框架須要放置在最底層的基礎功能組件中,提供上層進行使用,這裏我使用的是我本身的路由框架Router:一款單品、組件化、插件化全支持的路由框架

此路由框架支持在單品、組件化、插件化中均能使用。若是你想要爲組件化以後,能在後期有須要的狀況下,方便的從組件化切換到插件化的環境中去,建議使用此Router

若是大家項目中已經有使用本身的路由框架,且也直接支持組件化環境使用。建議這塊就最好別考慮換了。實話說換一個路由框架任務仍是挺重的。

由於基本全部的介紹組件化的blog,都對其中的路由框架,作了很是詳細的說明,因此這塊我就不許備展開進行詳細的贅述了,若有感興趣的,能夠參考上方的連接進行了解使用。

事件通訊

與路由通訊不一樣的是:路由主要用於作界面跳轉通訊,對於普通的事件通訊做用不大。好比說我是組件A,須要調組件B中的某個接口,並獲取返回數據進行操做。這個時候,就須要別的方式來進行實現了。

不少人一說到事件通訊。可能就會想起使用EventBus了。的確EventBus是個很好的事件通訊框架,可是相信用過的人都知道。一旦EventBus被濫用。隨着時間的迭代,因爲其獨特的解耦特性,會使得你的代碼很難進行調試、維護。

因此這個時候,咱們摒棄了使用EventBus來做爲組件間時間通訊的橋樑。而是簡單的使用控制反轉的手段。將組件間通訊協議定義在底層基礎組件中,上層的業務組件分別實現底層對應的各自的協議接口來進行通訊。

咱們以登陸組件爲例:

首先,在基礎組件層添加一個協議接口。這個接口用於定義登陸組件所對外提供的時間通訊入口,好比退出登陸、清理cookie等:

public interface LoginPipe extends Pipe{
    void logout();
    void clearCookie();
}
複製代碼

而後。在登陸組件中。實現此協議接口。並註冊入對應的通訊管理器:

// 實現協議接口。
public final class LoginPipeImpl implements LoginPipe {
    @Override
    void logout() {
        // do something
    }
    
    void clearCookie() {
        // do something
    }
}
複製代碼
// 註冊此實現進協議管理器中
// PipeManager也位於基礎組件中。
PipeManager.register(LoginPipe.class, new LoginPipeImpl());
複製代碼

而後便可在別的組件中。經過此PipeManager協議管理器。根據協議類。獲取到對應的實現類進行直接調用了:

PipeManager.get(LoginPipe.class).logout();
複製代碼

上面這種作法,雖然的確很簡單,可是具有如下幾點優勢:

  • 提升各組件協議的內聚性。更適於各自組件對各自的協議接口進行統一管理維護。
  • 實現方案簡單易懂,易於調試。
  • 在組件化拆分進程中,便於方便後期對主app殼無關代碼進行刪除。

最後一條可能相對比較複雜一點。因此下面咱們針對這條進行展開描述:

上面咱們提到了。在對老舊項目進行組件化重構的時候。使用主module做爲的主app殼,而app殼實際上是須要沒有具體的業務代碼的。因此這個地方存在衝突。可是咱們組件化拆分也不是能夠一蹴而就的,只能慢慢一步步、一個頁面一個頁面的進行拆分並測試。因此拆分過程實際上是個漫長的痛苦的過程。

而在拆分過程當中,很難避免的就是新舊代碼均須要同時存在的尷尬場面。而這種尷尬的場面會一直持續到全部組件均拆分完畢以後。

而後拆分過程當中,你也會遇到另一個問題:就是各業務組的拆分計劃實際上是不一樣步的,也就是說極可能你當前拆的業務。須要調用到別的業務組的功能,而這個功能這個時候。極可能還根本沒有被提交到拆分計劃表上來。因此這個時候。你就必需要在你拆分的組件中,仍是先直接調用老項目中的邏輯代碼。

因此使用上面的事件通訊機制。你會須要在主app中建議一個臨時的協議接口。好比:

public interface MainPipe {
    void doSomething();
}
複製代碼

而後主項目實現並註冊它。提供給你的組件進行使用。而其餘組件遇到此種相似問題時,也於此相似。在此MainPipe種繼續添加對應的通訊協議方法並實現便可。

因爲這樣的作法。將全部的主app的臨時協議接口。均放置於此MainPipe中。提高了協議的內聚性。當全部業務組均完成組件化重構以後。那麼就能夠統一的直接對此MainPipe進行重構,將其中各自組件的協議遷移至各自組件的協議類中,而後就能夠安全地進行主app中無關業務代碼統一刪除了。使其成爲真正的主app殼工程。

數據通訊

不少時候,其實組件間通訊。傳遞的數據都是普通的簡單數據,可是也有一些時候。會須要傳遞複雜數據。好比進行跨組件調用api接口並獲取返回數據時,或者說讀取用戶完整數據時。

以讀取用戶完整數據爲例,數據通訊的協議定義仍舊以上方的事件通訊機制做爲實現載體:

public class User {
    String uid;
    String nickname;
    String email;
    String phone;
    ...
}
複製代碼

這個User類包含了全部的用戶信息在裏面。而後如今須要將此user實例進行跨組件傳遞時。你就須要定義一個協議方法。提供獲取此User實例的入口:

public interface LoginPipe {
    User getUser();
}
複製代碼

這是正常的作法,可是這樣作的話,你就須要將此User實例也一塊兒拷貝到協議定製層,即基礎組件中來。

而在開發過程當中,這種現象很常見。並且不少時候,隨着需求一更改,所須要傳遞的數據也不同。也不可能每次都去將對應的實體bean進行遷移,放入協議定製層。這樣就太麻煩了。

因此對於這種跨組件通訊的作法。建議的方式是經過json數據來進行數據通訊

json通訊的機制,便可完美的避免實體bean遷移的問題。也能讓接收方按需解析讀取數據:

好比我接收方的組件。當前只須要nickname與uid兩個數據。其餘數據我無論。那麼我就能夠只解析此兩個字段的數據便可。作到按需解析。

說到這裏。推薦一波個人另外一個框架Parceler, 此框架是封裝的Bundle的存取操做。也支持json的自動轉換功能。具體用法能夠參考我另外一篇博客,有興趣的能夠看看。

Parceler: 優雅的使用Bundle進行數據存取就靠它了!(文章最後有關於組件化、插件化下應該如何使用此框架的說明)

優化加速

隨着組件化拆分重構的進行。你會發現項目下的組件被拆分得愈來愈多,雖然你已經對組件的拆分粒度。進行過把控了。可是組件化後module持續增長是不爭的事實,這個時候。隨着module的持續增長。你的項目編譯時間也會出現暴漲。

咱們知道。項目編譯流程中,第一步會將全部的library module先進行打包編譯。生成對應的aar。提供給app進行使用,app等待全部module打包完畢後,再解壓aar。進行資源、代碼合併,並打包成apk執行運行。

因此咱們製做了一個gradle加速插件。用於提早將module進行aar編譯好。跳過module打包aar的過程。實現編譯加速的效果。

具體原理能夠參考這篇文章:Speedup:專爲項目下Library project過多所設計的加速插件

更多小貼士

由於在組件化開發環境下,你將會遇到的問題遠遠不止以上這麼點,固然上面這些都是很主要的。

因此這裏添加此小貼士環節,用於添加一些平時咱們開發時。可能會遇到的問題。或者說,一些在特定環境下的編碼建議之類的。(這些要點極可能不能在demo中獲得體現,因此請儘可能認真看下描述)

巧用ActivityLifecycleCallbacks作初始化

由於組件化有個特色: 各自業務組能夠任意選擇本身的開發模式,如mvp,mvvm,RN等。

Android組件化demo

相關文章
相關標籤/搜索