iOSApp組件化詳解(從0到1實(shí)現(xiàn)一個完整的組件化項(xiàng)目)

何為組件化

一種能夠解決代碼耦合的技術(shù)。項(xiàng)目經(jīng)過組件化的拆分,不僅可以解決代碼耦合的問題,還可以增強(qiáng)代碼的復(fù)用性,工程的易管理性,減少編譯時間等

1.組件化分層架構(gòu)圖

App組件化架構(gòu)分層.png

2.架構(gòu)分層詳解

1.Lib層

基礎(chǔ)模塊跟業(yè)務(wù)無關(guān),只定義接口和基本配置,子類可以重寫,便于擴(kuò)展

  • LibBase
    • LibBaseController
    • LibBaseNavController
    • ...
  • LibCommon基礎(chǔ)公共組件
  • LibFlexBox移動端FlexBox布局

Widget層

  • 由Lib延伸而來,便于組件的擴(kuò)展和復(fù)用

2.Mediator層

  • 采用Bifrost框架,創(chuàng)建調(diào)度組件并定義交互協(xié)議,處理業(yè)務(wù)模塊之間的數(shù)據(jù)傳遞及邏輯交互處理
  • Module層必須依賴該庫

3.Module層

業(yè)務(wù)層,跟業(yè)務(wù)相關(guān)的一些組件,業(yè)務(wù)組件之間互不依賴,且依賴于ModuleCommon層

ModuleCommon

跟業(yè)務(wù)相關(guān)的公共組件部分,例如

  • CommonBaseController
  • CommonLodingController
  • CommonListController
  • 數(shù)據(jù)模型Models
  • Tools工具類
  • Macro常見宏等
  • QMUI常見配置/換膚配置
  • 第三方分享/登錄/支付配置

3.組件化要點(diǎn)羅列

  • 多Target分模塊開發(fā),代碼解耦
  • 單獨(dú)編譯項(xiàng)目
  • 組件之間傳值,通過調(diào)度組件Biforst
  • 組件間訪問公共圖片資源,解決命名沖突
  • 文件夾分層:子組件subspec
  • pod 引入依賴方式
    • 引入本地依賴
    • 引入遠(yuǎn)程依賴
    • 引入指定分支
  • 組件命名方式
  • App路由管理
  • podspec使用及格式校驗(yàn)
  • 組件間依賴管理
  • coapods私有化倉庫搭建

**

3.1多Target分模塊開發(fā),代碼解耦

workspace 依賴多個功能文件開發(fā)

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
def commonPods()
    #基礎(chǔ)宏定義,類別
    pod 'HKMacros', :git => 'https://gitee.com/Steven_Hu/HKMacros.git'
    # 組件化基類-引用本地依賴
    pod 'HKBaseModule', :path => './PrivateRepo/HKBaseModule/HKBaseModule.podspec'
    #pod 'HKBaseModule', :git => 'https://gitee.com/Steven_Hu/HKBaseModule.git'
end

def mediatorPods()
    #pod 'Bifrost', :path => '../'
end

  # Comment the next line if you don't want to use dynamic frameworks
  #use_frameworks!
  #workspace文件名
  workspace 'HKiOSTools.xcworkspace'
  #主工程路徑
  project 'HKiOSTools/HKiOSTools.xcodeproj'

target 'HKiOSTools' do
    project 'HKiOSTools/HKiOSTools.xcodeproj'
    commonPods()
  
    target "HKBaseModule_Example" do
        project 'PrivateRepo/HKBaseModule/Example/HKBaseModule.xcodeproj'
        commonPods()
    end

end

**

3.2單獨(dú)編譯項(xiàng)目

如何區(qū)分你編譯的是主工程項(xiàng)目還是子工程項(xiàng)目

  • 最簡單的方式,通過項(xiàng)目名稱:ProjectName

[[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString *)kCFBundleExecutableKey]

  • 判斷項(xiàng)目名稱是否和子項(xiàng)目名稱一致即可

3.3組件之間傳值Bifrost

image.png

注冊路由(ViewController)

+ (void)load {
    [Bifrost bindURL:kRouteHomePage
           toHandler:^id _Nullable(NSDictionary * _Nullable parameters) {
        return [HKHomeViewController new];
    }];
}

獲取路由(ViewController)

UIViewController * vc = [Bifrost handleURL:kRouteHomePage];
[self.navigationController pushViewController:vc animated:YES];

頁面?zhèn)髦?/h4>
  • 以type為例
//1.當(dāng)前頁面聲明一個type屬性
/// 驗(yàn)證類型
@property (nonatomic, assign) HKVerifyType  type ;
// 2.bindURL
[Bifrost bindURL:kRouteBindPhoneNumPage toHandler:^id _Nullable(NSDictionary * _Nullable parameters) {
  HKBindPhoneNumViewController *vc = [[self alloc] init];
  vc.type = [parameters[kRouteBindPhoneNumParamType] integerValue];
  return vc;
}];
// 3.傳值type
NSString *routeURL = BFStr(@"%@?%@=%@", kRouteBindPhoneNumPage, kRouteBindPhoneNumParamType, @(HKVerifyTypeForgetPassword));
UIViewController * vc = [Bifrost handleURL:routeURL]
[self.navigationController pushViewController:vc animated:YES];

頁面回調(diào)處理

//1.當(dāng)前頁面聲明一個type屬性和Block
/// 驗(yàn)證類型
@property (nonatomic, assign) HKVerifyType  type ;
/// 完成回調(diào)
@property (nonatomic, strong) BifrostRouteCompletion complete ;
// 2.bindURL
[Bifrost bindURL:kRouteBindPhoneNumPage toHandler:^id _Nullable(NSDictionary * _Nullable parameters) {
    HKBindPhoneNumViewController *vc = [[self alloc] init];
    vc.type = [parameters[kRouteBindPhoneNumParamType] integerValue];
    vc.complete = parameters[kBifrostRouteCompletion];
    return vc;
}];
// 3.點(diǎn)擊事件處理
/// 獲取驗(yàn)證
- (void)getVerifyCodeEvent
{
    [self.view endEditing:YES];
    BFComplete(@{kBifrostRouteCompletion:self.complete}, @(self.type));
}
// 4.傳值type
UIViewController *vc = [Bifrost handleURL:kRouteBindPhoneNumPage complexParams:@{kRouteBindPhoneNumParamType:@(HKVerifyTypeForgetPassword)} completion:^(id  _Nullable result) {
    HKVerifyType type = (YNVerifyType)result;
    [HKKeyWindow hk_showWithText:HKStr(@"你點(diǎn)擊的類型是:%@",type)];
}];
[self.navigationController pushViewController:vc animated:YES];

3.4組件間訪問公共圖片資源

主工程有一個a.png的圖片,而pod庫里面也有一個a.png的圖片,此時就產(chǎn)生命名沖突了

思路:不同的組件都有自己獨(dú)立的bundle,組件內(nèi)部資源提供自身的Bundle來獲取,以避免資源重復(fù)

  • 本地資源如圖片等存放位置(Assets文件夾,否則無法訪問)
image.png
  • 指定Bundle名稱
# resource_bundles
s.resource_bundles = {
   'HKLibCommon' => ['HKLibCommon/**/*.{xib,jpg,gif,png,xcassets}']
  }
  • 解決命名沖突:resource_bundles
  • resource_bundles會自動生成bundle把資源文件打包進(jìn)去

加載本地資源方法

工具類抽取
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@interface ModuleBundle : NSObject

/*
 * 根據(jù)bundle的名稱獲取bundle
 */
+ (NSBundle *)bundleWithName:(NSString *)bundleName;

//獲取bundle 每次只要重寫這個方法就可以在指定的bundle中獲取對應(yīng)資源
+ (NSBundle *)bundle;

//根據(jù)xib文件名稱獲取xib文件
+ (__kindof UIView *)viewWithXibFileName:(NSString *)fileName;

//根據(jù)圖片名稱獲取圖片
+ (UIImage *)imageNamed:(NSString *)imageName;

//根據(jù)sb文件名稱獲取對應(yīng)sb文件
+ (UIStoryboard *)storyboardWithName:(NSString *)storyboardName;

//獲取nib文件
+ (UINib *)nibWithName:(NSString *)nibName;

@end
#import "ModuleBundle.h"

@implementation ModuleBundle

+ (NSBundle *)bundleWithName:(NSString *)bundleName {
    if(bundleName.length == 0) {
        return nil;
    }
    NSString *path = [[NSBundle mainBundle] pathForResource:bundleName ofType:@"bundle"];
    NSAssert([NSBundle bundleWithPath:path], @"not found bundle");
    return  [NSBundle bundleWithPath:path];
}

+ (NSBundle *)bundle {
//    NSAssert([NSBundle mainBundle], @"not found bundle");
    return [NSBundle mainBundle];
}

+ (UIView *)viewWithXibFileName:(NSString *)fileName {
    NSAssert([self viewWithXibFileName:fileName inBundle:[self.class bundle]], @"not found view");
    return [self viewWithXibFileName:fileName inBundle:[self.class bundle]];
}

+ (UIImage *)imageNamed:(NSString *)imageName {
    NSAssert([self imageNamed:imageName inBundle:[self.class bundle]], @"not found image");
    return [self imageNamed:imageName inBundle:[self.class bundle]];
}

+ (UIStoryboard *)storyboardWithName:(NSString *)storyboardName {
    NSAssert([self storyboardWithName:storyboardName inBundle:[self.class bundle]], @"not found storyboard");
    return [self storyboardWithName:storyboardName inBundle:[self.class bundle]];
}

+ (UINib *)nibWithName:(NSString *)nibName {
    NSAssert([self nibWithNibName:nibName inBundle:[self.class bundle]], @"not found nib");
    return [self nibWithNibName:nibName inBundle:[self.class bundle]];
}

#pragma mark - private
+ (UIImage *)imageNamed:(NSString *)imageName inBundle:(NSBundle *)bundle {
    if(imageName.length == 0 || !bundle) {
        return nil;
    }
    return [UIImage imageNamed:imageName inBundle:bundle compatibleWithTraitCollection:nil];
}

+ (UIImage *)imageNamed:(NSString *)imageName bundleName:(NSString *)bundleName {
    return [self imageNamed:imageName inBundle:[self bundleWithName:bundleName]];
}

+ (UIView *)viewWithXibFileName:(NSString *)fileName inBundle:(NSBundle *)bundle {
    if(fileName.length == 0 || !bundle) {
        return nil;
    }
    //如果沒有國際化,則直接去相應(yīng)內(nèi)容下的文件
    UIView *xibView = [[bundle loadNibNamed:fileName owner:nil options:nil] lastObject];
    if(!xibView) {
        //文件國際化之后,所有的bundle的文件資源都在base的目錄下
        xibView = [[[NSBundle bundleWithPath:[bundle pathForResource:@"Base" ofType:@"lproj"]] loadNibNamed:fileName owner:nil options:nil] lastObject];
    }
    return xibView;
}

+ (UIView *)viewWithXibFileName:(NSString *)fileName bundleName:(NSString *)bundleName {
    return [self viewWithXibFileName:fileName inBundle:[self bundleWithName:bundleName]];
}

+ (UIStoryboard *)storyboardWithName:(NSString *)storyboardName inBundle:(NSBundle *)bundle {
    if(storyboardName.length == 0 || !bundle) {
        return nil;
    }
    return [UIStoryboard storyboardWithName:storyboardName bundle:bundle];
}

+ (UIStoryboard *)storyboardWithName:(NSString *)storyboardName bundleName:(NSString *)bundleName {
    return [self storyboardWithName:storyboardName inBundle:[self bundleWithName:bundleName]];
}

+ (UINib *)nibWithNibName:(NSString *)nibName inBundle:(NSBundle *)bundle {
    if(nibName.length == 0 || !bundle ) {
        return nil;
    }
    return [UINib nibWithNibName:nibName bundle:bundle];
}

@end
Bundle獲取

創(chuàng)建一個類CommonBundle繼承自ModuleBundle,并實(shí)現(xiàn)以下方法

#import "CommonBundle.h"
@implementation CommonBundle
+ (NSBundle *)bundle{
    //TODO:注意Bundle名字需跟模塊名稱一致,否則會找不到path,直接Crash
    return [self.class bundleWithName:@"HKLibCommon"];
}
@end
使用

替換系統(tǒng)加載圖片的方式:[UIImage imageNamed:@"navigationbar_background"]

[CommonBundle imageNamed:@"navigationbar_background"]
多個包
spec.resource_bundles = {
    'MapBox' => ['MapView/Map/Resources/*.png'],
    'OtherResources' => ['MapView/Map/OtherResources/*.png']
  }

3.5文件夾分層子組件subspec

我們在編寫podspec文件時,sourcefiles只是告訴pods你需要哪些文件是這個項(xiàng)目中需要的,而沒有包括文件的層級結(jié)構(gòu),那么就需要我們來實(shí)現(xiàn)這個層級結(jié)構(gòu):subspec


image.png

比如這里面的每一個文件夾,就是一個子pod,這樣的好處是條理清晰,而且我們可以只用你需要的功能,在編寫podfile時 就可以這樣寫
pod 'HKModuleModels/User' 只使用其中的一個功能。

主podspec

主pod可以是一個頭文件,也可以具有一定的功能,我寫的組件sourcefiles只是一個import子組件的頭文件, sourcebundle是項(xiàng)目中需要的一些圖片


image.png

編寫subspec

  • 讓pods支持子subspec其實(shí)很簡單,只要搞清楚三件事
  1. 文件夾結(jié)構(gòu) subspec sourcefiles的路徑
  2. subspec 所依賴的系統(tǒng)庫
  3. subspec 所依賴的第三方,和其它subspec的路徑
image.png

3.6pod 引入依賴方式

引入本地依賴

pod 'ManageLocalCode', :path => '../ManageLocalCode'
pod 'BioAuthAPI', :path => '../BioAuthAPI'
pod 'HKTool', :path => '../'

遠(yuǎn)程私有庫依賴:

# 基礎(chǔ)宏定義byStevenHu
pod 'HKMacros', :git => 'https://gitee.com/Steven_Hu/HKMacros.git'

指定分支

# 支付代碼測試
  iap_pay:
    git:
      url:  https://gitee.com/Steven_Hu/iap_pay.git
      ref: master

常規(guī)依賴

pod "AFNetworking",  "~>0.2.0"

3.7 組件命名方式

組件性質(zhì) 建議名稱 示例
基礎(chǔ)組件拆分 項(xiàng)目前綴Lib組件名稱 HKLibBase:基類模塊
業(yè)務(wù)組件拆分 項(xiàng)目前綴Module組件名稱 HKModuleHome:首頁模塊
調(diào)度組件拆分 項(xiàng)目前綴Mediator組件名稱 HKMediatorDriver:司機(jī)端調(diào)度組件
Wdiget組件拆分 項(xiàng)目前綴Widget組件名稱 HKWidgetAddressPicker:地址選擇器組件
公共組件 項(xiàng)目前綴_組件名稱_Common HKLibCommon,HKModuleCommon

3.8 App端路由管理

格式

app內(nèi)鏈

舉例yunquedriver://home/homepage/detail?name=steven&age=18
appscheme://moduleName/pageName/secondPageName?key1=value1&key2=value2
其中value中有中文等特殊字符時需要進(jìn)行urlEncode。

app外鏈外鏈

http://xxx
https://xxx

格式說明

  • appscheme分解
    • 項(xiàng)目名稱
      • yunque
    • 業(yè)務(wù)側(cè)
      • driver
      • user
  • moduleName
    • 業(yè)務(wù)模塊名稱
  • pageName
    • 頁面名稱
  • secondPage
    • 二級頁面

scheme

云雀司機(jī): yunquedriver
云雀用戶:yunqueuser

模塊名

lib:工具類,跟業(yè)務(wù)無關(guān)的

頁面

image.png

3.9 podspec使用及格式校驗(yàn)

注意事項(xiàng)

  • podspec版本號和git倉庫代碼的tag版本必須一致,否則找不到
    • 比如:pod lib 默認(rèn)生成的是0.1.0,我們根據(jù)業(yè)務(wù)需要改成0.0.1
  • 索引倉庫和組件倉庫關(guān)聯(lián)
    • cd到組件倉庫目錄下
    • pod repo push #{本地關(guān)聯(lián)到遠(yuǎn)程倉庫的名字} #{第三方私有庫的podspec文件} --verbose --allow-warnings 示例: pod repo push YNSpecs YNLibDemo.podspec --verbose --allow-warnings
  • 項(xiàng)目引入
# 遠(yuǎn)程私有倉庫
source 'https://gitlab.com/xxxx/xxSpecs'

# 引入使用
pod 'xxLibNetwork'

3.10 組件間依賴管理

通過 git submodule add https://gitee.com/Steven_Hu/hk-module-components.git 添加submodule
會在git上添加一個.gitmodules 文件,可以查看各種依賴

[submodule "PrivateRepo/hk-lib-base"]
    path = PrivateRepo/hk-lib-base
    url = https://gitee.com/Steven_Hu/hk-lib-base.git
[submodule "PrivateRepo/hk-lib-common"]
    path = PrivateRepo/hk-lib-common
    url = https://gitee.com/Steven_Hu/hk-lib-common.git
[submodule "PrivateRepo/hk-lib-network"]
    path = PrivateRepo/hk-lib-network
    url = https://gitee.com/Steven_Hu/hk-lib-network.git
[submodule "PrivateRepo/hk-mediator"]
    path = PrivateRepo/hk-mediator
    url = https://gitee.com/Steven_Hu/hk-mediator.git
[submodule "PrivateRepo/hk-lib-keyboard"]
    path = PrivateRepo/hk-lib-keyboard
    url = https://gitee.com/Steven_Hu/hk-lib-keyboard.git
[submodule "PrivateRepo/hk-lib-avoid-app-crash"]
    path = PrivateRepo/hk-lib-avoid-app-crash
    url = https://gitee.com/Steven_Hu/hk-lib-avoid-app-crash.git
[submodule "PrivateRepo/hk-lib-load-picture"]
    path = PrivateRepo/hk-lib-load-picture
    url = https://gitee.com/Steven_Hu/hk-lib-load-picture.git
[submodule "PrivateRepo/hkmodule-models"]
    path = PrivateRepo/hkmodule-models
    url = https://gitee.com/Steven_Hu/hkmodule-models.git
[submodule "PrivateRepo/hk-module-uikit"]
    path = PrivateRepo/hk-module-uikit
    url = https://gitee.com/Steven_Hu/hk-module-uikit.git
[submodule "PrivateRepo/hk-module-components"]
    path = PrivateRepo/hk-module-components
    url = https://gitee.com/Steven_Hu/hk-module-components.git
[submodule "PrivateRepo/hk-module-main"]
    path = PrivateRepo/hk-module-main
    url = https://gitee.com/Steven_Hu/hk-module-main.git

最終結(jié)果如下圖所示,點(diǎn)擊跳轉(zhuǎn)新的submodule倉庫地址


image.png

本地Clone方式

git clone https://gitee.com/Steven_Hu/hk-iostools.git
git submodule init && git submodule update

#下面這一句的效果和上面三條命令的效果是一樣的,多加了個參數(shù)  `--recursive`
git clone https://gitee.com/Steven_Hu/hk-iostools.git --recursive

常見問題

error: Server does not allow request for unadvertised object b22e0a5a2a8dce1d0454bdead70353bf45f33f81

Fetched in submodule path 'PrivateRepo/hk-module-main', but it did not contain b22e0a5a2a8dce1d0454bdead70353bf45f33f81. Direct fetching of that commit failed.

原因分析:

未拉取到該submodule的最新代碼

解決

git submodule foreach git checkout master
git submodule foreach git submodule update

3.11 coapods私有化倉庫搭建

準(zhǔn)備工作

  • 安裝cocoapods
  • 準(zhǔn)備gitlab/gitee/getlab(下文統(tǒng)一稱為gitlab)等代碼倉庫賬號

創(chuàng)建組建索引Spec倉庫(用于存放組件庫的索引)

pod repo add `specFileName(給spec倉庫在本地的命名)` `spec(倉庫的地址)`
實(shí)例:
pod repo add XXSpecs https://gitee.com/Steven_Hu/objective-c-specs

~/.cocoapods/repos目錄中可以看到創(chuàng)建的文件夾

image.png

  • 創(chuàng)建組件庫
    • pod lib create #{庫的名稱}
    • 實(shí)例 pod lib create XXLibDemo
  • 修改podspec文件
  • 將組件push到gitlab
  • 將索引倉庫和組件倉庫關(guān)聯(lián)
  • 項(xiàng)目引入
# 遠(yuǎn)程私有倉庫
source 'https://gitlab.com/xxxx/xxSpecs'

# 引入使用
pod 'xxLibNetwork'

4.常見問題補(bǔ)充

4.1target has transitive dependencies that include statically linked binaries:

解決辦法

s.static_framework = true

4.2CocoaPods did not set the base configuration of your project because your project already...

使用CocoaPods安裝三方庫后有兩個警告,如下所示:


image.png

解決辦法:
將第三方庫 的 PROJECT → Info → Configurations 下Debug和Release下的.debug和.release選項(xiàng)替換為None,如下圖所示:


image.png

然后在pod install即可

4.3清除 CocoaPods 本地緩存

特殊情況下,由于網(wǎng)絡(luò)或者別的原因,通過CocoaPods下載的文件可能會有問題。
這時候您可以刪除CocoaPods的緩存(~/Library/Caches/CocoaPods/Pods/Release目錄),再次導(dǎo)入即可。

4.4pod spec lint編譯時報(bào)error: include of non-modular header inside framework module

解決辦法

pod lib lint --allow-warnings --use-libraries --verbose

有警告??可以使用-allow-warnings忽略。

4.5推送到本地LocalRepo

  • 使用了第三方庫
pod repo push driver_spec XXLibBase.podspec --allow-warnings --verbose --use-libraries
  • 未使用第三方庫
pod repo push driver_spec XXLibBase.podspec --allow-warnings --verbose

4.6pod lib lint 可選參數(shù)

pod lib lint SPEC_NAME.podspec
可選參數(shù):
--verbose : 顯示詳細(xì)信息
--allow-warnings: 是否允許警告,用到第三方框架時,用這個參數(shù)可以屏蔽講稿
--fail-fast: 在出現(xiàn)第一個錯誤時就停止
--use-libraries:如果用到的第三方庫需要使用庫文件的話,會用到這個參數(shù)
--sources:如果一個庫的podspec包含除了cocoapods倉庫以外的其他庫的引用,則需要改參數(shù)指明,用逗號分隔。
--subspec=Name:用來校驗(yàn)?zāi)硞€子模塊的情況。

注意:如果庫用到了第三方的話要帶上 --use-libraries,否則會報(bào)錯,上傳不上去。

4.7--sources 使用

1.私有pod的驗(yàn)證

使用pod spec lint去驗(yàn)證私有庫能否通過驗(yàn)證時應(yīng)該,應(yīng)該要添加--sources選項(xiàng),不然會出現(xiàn)找不到repo的錯誤。

pod spec lint --sources='私有倉庫repo地址,https://github.com/CocoaPods/Specs'

2.私有庫引用私有庫的問題

在私有庫引用了私有庫的情況下,在驗(yàn)證和推送私有庫的情況下都要加上所有的資源地址,不然pod會默認(rèn)從官方repo查詢。

pod spec lint --sources='私有倉庫repo地址,https://github.com/CocoaPods/Specs'
pod repo push 本地repo名 podspec名 --sources='私有倉庫repo地址,https://github.com/CocoaPods/Specs'

4.組件化案例

1.效果圖

組件化案例.gif

2.代碼地址

https://gitee.com/Steven_Hu/hk-iostools

搬磚不易,轉(zhuǎn)載請注明出處,謝謝!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容