iOS 组件化实践

一、前言

什么是组件化

组件化就是将APP拆分成各个组件,然后通过主工程将项目所需要的组件组合起来,比如首页,个人中心,某个复杂的二级页面,都可以作为组件,同时解除这些组件之间的耦合。这样组件化过后的项目就变成了很多小组件,如果新项目中有类似的需求,直接将组件引入稍作修改就能使用了。其实就是把我们开发的组件变成一个第三方库引入,我们以后开发就相当在维护一个第三方库

组件化的优点:
  1. 组件可独立运行,提高的代码的复用性,组件化的颗粒度越细,可复用度就越高。
  2. 当组件库的数量足够庞大时,项目只需要组合组件即可完成大部分的开发工作。
  3. 组件化后项目的代码结构更加清晰,追踪问题、修复bug、增加需求更方便
  4. 不同业务组件相互独立,明确团队开发的业务边界,增加团队协作效率
组件化的缺点:
  1. 增加开发人员的学习成本
  2. 增加了代码的冗余,组件化颗粒度越细,中间代码越多
  3. 增加了项目的复杂度,复杂度越高越容易出问题
  4. 项目复杂、臃肿、庞大,编译时间过长
下面说说我的一点积累,会有很多不足,多多包涵。
  1. 并不是所有工程都适合组件化,如果你的工程规模不大的或单人开发,都不建议组件化,反而降低你的开发效率。如果工程规模大,有多个APP业务有重复的,团队开发的,组件化确实助力很大。
  2. 组件的拆分,不是所有页面都要做成组件的,一个工程有200多个页面,不可能有这么多的组件,只是某一个大功能会成为一个组件,比如首页组件,个人中心组件,某个功能的二级页面,基础组件(网络请求等 各种封装),一个组件内肯定有多个页面和功能的,这个根据自己公司的业务模块来进行合理的划分即可。
  3. 组件是一个单独工程,在没有任何交互的情况下可以独立运行,但这是理想化。一个工程一定会有网络请求,会有一些和其他组件工程共用的类或文件。这些东西不能每个组件都写一遍,所以要有一个基础组件存放网络请求,公共类,图片等。所以组件单独运行实际情况是:业务组件+基础组件 才能运行
  4. 组件化一般是有一个主工程(壳子)和很多子工程组成,子工程的管理可以使用Workspace ,Workspace是xcode 自带的一种工程方式,多个工程存放在一起,一个Workspace管理多个子工程,这简直就是为组件化而设计的,如果不使用Workspace管理子工程,子工程要每个都要podspec 文件,还是麻烦。Workspace只创建一个podspec 文件就可以了。
  5. 路由或中间件,没接触组件化的同学很多都认为路由或中间件就是组件化 或者是组件化的最重要的,其实路由或中间件只是组件化的一个跳转通信的工具,我觉得怎样把组件搭建起来才是最重要的,因为可选择的通信工具很多,很多大神都写过各种思路的路由或中间件,我们demo选择具有代表性的 MGJRouter和CTMediator 同时集成给大家展示,大家组件化成功后,可以更换其他路由或中间件,试试不同的通信思路。

二、创建Workspace

  • 工程的创建
    TestApp
    TestAppModule

在github或码云上创建工程 并clone本地工程,或本地创建在上传。组件都是私有库的,不对外暴露,我这个是demo都是公开库了

D3769502-9B50-44B7-B39E-BC028BB87C03.png

TestAppModule 为各组件集合的工程 (Workspace)
TestApp 为主要工程(俗称壳子)最后所有的组件会在这个工程进行联调,并打包。

  • 先从TestAppModule 组件的工程 开始
    创建Workspace 名为 TestAppModule


    42EB8AD3-E9D5-422E-9B10-A04D5224C176.png

    存储在桌面的TestAppModule 文件夹里

在桌面的TestAppModule文件夹下创建多个项目工程 例:TestA TestB...


B0776FFF-4B81-4432-A362-5408EFAEC049.png

打开TestAppModule.xcworkspace,点击左下角+号 添加TestA等工程到xcworkspace


16AEE4A1-D442-4D73-81BE-7FD4B1C924FD.png

要选择工程的xcodepro 添加进来


6C647007-B569-4C4D-A8C7-998E5B0078CE.png

最终是这样的


7DE22084-928F-49B4-BE1F-25BF07E4CBF1.png

三、配置组件工程

  • 组件化大家的印象中,单个组件是可以独立运行的,互不干扰,但是有的公共方法如 网络请求,共用的宏,图片等,如何处置。这时候就需要一个Basis组件来放置,每个组件都可能使用的公用类都要放在Basis组件里,Basis组件也负责所有pods第三方的引用,所以每次运行组件并不是单独的,应该是:TestA+TestBasis 才能运行。

  • TestBasis工程里 举例 如下配置,一些公共类不用细说,有变动的是图片读取,因为图片会跨组件读取(跨工程),正常图片读取 [UIImage imageNamed:@""]是不起作用的,这里写了个NSBundle的分类 ,后面会有调用


    A8442080-EACE-4F9F-83F3-41ABE4F802D6.png

    TestBasisHeader 是对暴露的类,里面包括所有公共类,第三方,路由URL,图片路径,接口文件等。test工程 因为引用的少我都写在一起了(2019.12.9更新 ,基础组件内的 某个功能可单独集成,具体配置看组件工程的podspec文件和Podfile文件)。


    2554CFC0-536B-404D-A3DC-5588248D149B.png

TestA 等其他工程里如下配置,因为我们同时使用了MGJRouter和CTMediator 演示,出现了两种通信类


F437F80C-31B6-4722-A0EE-DEF41CDB0C57.png
  • podspec文件
    本地工程创建podspec文件
    cd到TestAppModule文件夹目录 pod spec create 工程名
    和工程 xcodepro 一样添加进来


    815ECA25-F3D4-4898-996A-B12C287CC98B.png

    这时候可以先往github上传一次,因为下面我们配置Podfile和podspec文件 里面都要写github地址

podspec 配置十分重要,如果我们要集成第三方

重点:Podfile 文件进行配置 ,podspec也要改动

podspec里面的东西比较多 我只截取一部分,更多去工程TestAppModule

![C81AF073-4A7A-4222-8C06-9952C831FD7D.png](https://upload-images.jianshu.io/upload_images/1830250-d4040636aeaa4128.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

Podfile 文件创建
本地工程创建Podfile文件
cd到TestAppModule文件夹目录 vim Podfile按下图编辑完
和工程 xcodepro 一样添加进来


A9111B8A-3D32-4FB8-8AB6-AACAE27D996D.png

配置好了 我们pod install ,这个步骤可能会有多次的不成功,因为Podfile 文件和 podspec文件 都要写对

再次重申:Podfile 文件进行配置引用第三方 ,podspec也要对应改动

成功后下面打开工程先运行TestA 看看TestBasis与其他工程之间是否关联上
TestA 引用TestBasis 工程里的TestBasisHeader.h类


6E6FEF4D-F9FB-4302-903B-FDEF51FA942E.png

看到log 打印了,证明我们的业务组件+基础组件的模式成功了

这里要注意一点 我们在配置podspec文件的时候指定了每个组件最后到壳子工程上的文件,只有在Classses文件下的类才能集成进壳子


C81AF073-4A7A-4222-8C06-9952C831FD7D.png

TestA 为例Classses文件 下的类才才能被集成的壳子内,其他的不行,但是每个组件单独运行时没有区别,所以ViewController可以引用TestBasisHeader.h,并使用TestBasis的方法。但无法集成到壳子工程内。


193681A2-AE4A-458F-BFFA-5B7085549485.png

Classses文件 的pch 要解释一下


A8FE247B-021D-4FE6-B8EE-DE2DF10FCED9.png

这个文件用不用都可以。pch文件在这的作用就是解耦,可以看到我把TestBasisHeader.h 文件放到这里,TestA工程内都可以使用了。这是优点,弊端也很明显引用TestBasisHeader.h 相当于大量的头文件和宏定义放到pch里边,导致编译时间过长。到后期壳子编译会很慢。
如果 你的某个组件没有使用pch,在组件里podspec 文件,要把对应的 prefix_header_file 注释掉。否则主工程pods会报错

我先在各工程中简单的布局,
因为我们同时使用了MGJRouter和CTMediator 演示,出现了两种通信类
MGJRouter需要在 TestARoute.m、TestBRoute.m、TestCRoute.m中把路由注册上。


9D7F64AA-222B-4E09-9DC6-AF2B9609517B.png

CTMediator 需要 Target_TestA.m、Target_TestB.m 、Target_TestC.m中配置对应的类


C6C9909C-80CA-4B53-8C2D-0CE9714B6374.png

这时候我们可能会添加新的文件,类,或图片,需要git上 save 并push,
32217239-B5C8-42CD-B2F4-A233ABC72938.png
  • 如果TestBasis 创建新的文件,并且别的组件需要使用这个新的文件,会引用报错,在save 并push之后 要pod install,别的组件才能使用。pod install这个操作在组件化中很频繁。
    如果之后运行 还出现引用报错,未必是真的。可以(command+shift+k ) clean一下。clean这个操作 在组件化中经常用到,当我们添加一个新的文件,类,或图片在pod install 后都可以 clean一下再运行。

  • 虽然我们把路由都配置完了,但是组件这个工程路由是无法进行跳转的。下面我们进行主工程(壳子)的配置。

四、主工程的联调

  • TestApp 是我们的主app工程,最后所有的组件会集成到这个工程进行联调,并打包。
    我们在主工程创建TabBarController 引用TestA TestB TestC,这个TabBarController,我没有设计成组件,因为也没几行代码,如果工程很大有多个TabBarController,可以考虑根据业务模块来进行合理的划分。
    Podfile文件


    52266469-761B-453D-A137-7FF0F1BFE8D5.png

    可以看到 主工程pod 引用组件是本地的路径,并不是pod私有库的形式。这样的好处是,当你在组件更改代码主工程同步生效。在开发阶段建议使用本地的路径。稳定之后可以把组件pod私有库,在进行引用。
    编写完毕 pod install

  • 这个很大几率会不成功,因为组件工程的Podfile文件 和podspec 里面如果有不符合就会报错,比如我就遇到了 我的组件工程的podspec文件 只有testA有pch文件 但是testC工程编写了c.prefix_header_file = 'TestC/TestC/Classes/TestC.pch'',但实际我的工程里没有这个TestC.pch文件,就报错了,把这行注释掉。重新pod install 成功


    F3B95490-B05C-4E94-B619-A95B7427E5A8.png

    我可以看下内部结构


    D8597A6E-5C07-420C-A57C-9A40884D1BAE.png

上面讲过 我们的组件是在集成了每个工程的 Classes文件下的所有文件,所看起来以非常整洁。
接下来我们创建TabBarController 并引用组件的类最后运行起来


module.gif

五、pod 集成 本地framework(2021.7.9更新)

  1. 遇到了第三方sdk 是framework,并本地集成


    CC1624E4-A0B2-4DBF-B0B5-8F6326DB95B8.png

    如果是常规工程按照文档集成是没问题的。但是组件化会有问题,一个组件使用该sdk,按照文档集成,组件是没问题的,但是主工程在集成该组件无法找到对应sdk.。
    正确的做法是 为该sdk 创建一个组件,并查看framework sdk自带的podspec


    40ADE9BC-6209-44CC-8D85-6A5BE2A2C386.png

    把这段复制出来 拷贝到我们组件的 .podspec上
    14C96AEC-BBFA-43AD-B92B-DE2182F9AFA1.png

    这样我们需要使用sdk的组件工程引用 该组件pod install ,主工程在pod install 就可以了


    61B00260-D876-46BE-BCF5-85BCA68EC6BB.png
  2. SDK自带资源文件和图片的读取出来
    wb.ios.resource 这个需要重点说一下,因为sdk 里面有很资源文件和图片等等,按照我们之前说的 组件化读取文件和图片系统方法 比如正常图片读取 [UIImage imageNamed:@""]是不起作用的,都需要使用组件的路径读取。资源文件也是一样。framework sdk内使用的获取文件的方法是
    NSString *filePath_= [[NSBundle mainBundle] pathForResource:@"file" ofType:@"txt"]; ,使用的路径都是[NSBundle mainBundle] ,虽然知道这么写的但是 framework都是闭源的,无法改代码,导致使用sdk 图片和文件都读取不到。 wb.ios.resource作用是指向该组件下的sdk里的 bundle文件,可以使用[NSBundle mainBundle],bundle包含了图片和 资源文件。

六、git仓库的容量问题

一般组件化开发的,业务多 ,代码量基本都很大,在使用的某个组件Workspace仓库爆仓了。一般我们使用github或者码云 ,的单个仓库上限是一个1个G的容量 ,仓库包含了 组件本身代码和pod的第三方库还有git提交的历史记录,随着时间增加仓库爆仓了,1个G 满满的,无法在提交。所以我们重新创建了仓库,并不在提交pod相关的第三方代码,提交仓库的只有我们自己开发的代码文件,这样节约了大量空间,基本上减少了80%以上的空间。 具体可以使用.gitignore配置忽略文件 ,这个文件网上有很多。


340632C9-67F7-4F41-A70E-AD205BE3D543.png

90138A27-8CFE-4EDD-9EE5-19AF409FA4B5.png

.gitignore主要是过滤pod文件夹下的文件不在上传git,但是我们podfile 文件是提交的git的,这样团队其他人及知道第三方的变动,而不用担心pod的变动冲突问题,所以就算你的git仓库是公司自己创建的,无上限,也建议你过滤pod。

七、最后说下注意事项:

  1. 如在组件内新建或删除 文件夹 、类、图片等,确保不出错之后,一定要git提交,这时候运行主工程会报错或加载不到 ,主工程要pod install ,如果是图片等资源文件还要clean,运行成功好也要及时的提交git,因为组件化一般都是多人团队开发,git 操作冲突了,会很伤神。
  2. 加载资源文件,如图片等原来方法都会失效,需要路径加载。
  3. 设计模式不限制 MVC,MVP,MVVM 每个组件都可以不一样。
  4. 有的组件未必使用基础组件内的某个功能,我的例子却一起引用了,这个解决很灵活可单独引用,也可在pod中操作单独集成,也可把基础组件再次拆分多个组件,根据业务模块来进行合理的划分即可。

工程地址

注意 因为GitHub 下载工程默认会加-master,导致主工程报错,是因为工程名变了,找到不组件的造成的,可以把TestAppModule-master 的 -master删掉,在主工程运行,如还报错 主工程 pod install。
TestAppModule 组件工程
TestApp 主工程