64

iOS 组件化实践思考

 6 years ago
source link: http://www.cocoachina.com/ios/20181109/25428.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

组件化的应用背景和优势在此不再赘述,下面我们将从实践的角度,讨论一下如何应用组件化的思想,下面将以我自己的理解逐步展开,抛砖引玉。

哪些内容需要组件化

在我的理解中,一个项目可以拆分为以下几种组件:

  • 基础组件;

  • 功能组件;

  • 业务组件;

下面依次来解释几种组件的定义和规则。

基础组件

基本配置

  • 常量;

  • 宏定义;

分类

  • 各种系统类的扩展;

网络

  • 对 AFN 的封装;

  • 对 SDWebImage 的封装;

工具类

  • 文件处理;

  • 设备信息;

  • 时间日期处理;

基础组件的含义就是最基础的东西,每个业务组件都有可能会使用到,基础组件需要抽取的应该是类似上面的代码,举例来说,比如我们定义了一个常量,表示接口的根路径:

let BASEMIRRORURL = "http://rest.mirror.xxxx.com/ios"

那么这个常量在 Home,List,Detail 都有可能会被引用,因此我们将这种最底层的,最下一层的东西归类到基础组件。

又比如分类和扩展,我们给 UIView 的扩展定义一个计算属性:

extension UIView {
  var height {
    set {
      self.frame.size.height = newValue
    }
    get {
      return self.frame.size.height
    }
  }
}

可以想到,也会有很多的业务组件会使用到这个扩展。

功能组件

控件

  • 弹幕;

  • 轮播;

  • 菜单;

  • 瀑布流;

功能

  • 断点续传;

  • 音视频处理;

  • GPUImage 封装;

功能组件分为可见和不可见两种,可见的是控件,不可见的是功能。功能组件的作用顾名思义,就是实现了一个功能。

业务组件

业务组件,也就是业务的具体实现了,比如一个 App 的骨架如下:

  • 首页;

  • 发现;

  • 我的;

首页下又分为这样:

  • 侧滑菜单;

  • Banner;

  • 热门;

这里的每个部分,都可以称为业务组件。

三种组件的关系

FZvuyyV.jpg!web

基础组件规则

基础组件和基础组件之间不应该产生依赖,比如我们使用网络请求组件,希望根路径是一个默认参数,但可以对外暴露和修改,像下面这样:

class NetWork {
  func request(baseUrl: String = BASEMIRRORURL, path: String, param: [String:Any]) {

  }
}

NetWork.request(path: "/g/login.server", param: param)

这时,NetWork 就依赖了 常量 这个基础组件,我们如果使用 NetWork 基础组件,还需要导入 常量 这个基础组件,这是 不应该 的。

但为了代码的简洁性,这样的封装又是必要的,那么应该怎么做呢?这个问题我们下面会讲到。

功能组件规则

功能组件和基础组件之间不应该产生依赖,比如我们做轮播图,会用到 UIView 的扩展和 常量 ,像下面这样:

imageView.width = SCREENWIDTH

其中 .width 和 SCREENWIDTH ,都在基础组件中,但基础组件中不仅仅是这些东西,如果依赖了基础组件,就需要导入基础组件中其他无用的代码,而且其他人使用轮播图组件,也需要导入基础组件。

因此,在功能组件中,不建议依赖基础组件,上面的代码应该改成这样:

imageView.frame.size.width = UIScreen.main.bounds.size.width

或者直接复制代码,将需要的基础组件的功能,复制到功能组件当中。

同基础组件一样,功能组件和功能组件也不应该产生依赖,道理是一样的,我们使用一个功能,不应该将另一个功能也导入进来。

业务组件规则

基础组件和功能组件都是为业务服务的,因此业务组件可以依赖于基础组件和功能组件,快速的实现业务,但是 业务组件和业务组件之间不应该产生依赖。

比如这样一条业务线,我们要求 发现 这个业务组件,点击一条视频,跳转到 视频播放器:

func pushToPlayerVC(model: VideoModel) {
  let vc = PlayerVC(videoModel: model)
  navigationVC.push(vc)
}

这时 发现 就对 视频播放器产生了依赖,如果将 发现 进行组件化进行剥离,能行吗?不行。

其实这个问题和网络请求使用默认参数封装一样,是组件与组件之间的通讯问题,当然,这个问题我们下面会讲到,现在再提一下是为了一会儿往下写的时候忘了填坑 ...

每个组件存在的形式

  • 组件内部;

  • 组件外部;

  • 组件测试;

组件内部

组件的内部应该使用设计模式划分文件夹的结构,例如 MVVM 结构:

---- PlayerView
  -- View
  -- Model
  -- ViewModel

组件外部

组件的外部应该是一个远程私有 pod 库,使用 CocoaPods 进行管理。

组件测试

单独的测试工程。

怎样集成各个组件

E7RJbyN.jpg!web

组件的集成应该像上面的图一样,基础组件和功能组件互不依赖,制作远程 pod 私有库,业务组件依赖于这些 pod 私有库开发,同样制作成远程 pod 私有库,壳工程依赖于 CocoaPods 管理这些私有库,完成整个项目。

当然还有另外的方式,比如将壳工程作为主工程,组件创建为子工程,这方式的缺点是子工程可以修改,缺少约束性,目录结构也比较凌乱。

还有将组件制作为 FrameWork,壳工程中导入一个个 FrameWork 库,这种方式个人感觉比上一种好一些,但是在物理上,组件和壳还是没能做到分离。

因此,我个人还是更倾向于 pod 库的形式。

组件之间的通讯

  • 对外公开 API 接口;

  • 通过中间件的中转;

上面我们有两个遗留的问题,归纳为组件之间的通讯问题,下面就通过这两个问题,讨论一下组件之间的通讯。

网络请求默认参数

下面的思路就是暴露出 baseUrl 参数,通过中间件 NetWorkMW 将 NetWork 和 常量 两个基础组件组合,完成默认参数网络请求的封装。

// 基础组件 - 常量
let BASEMIRRORURL = "http://rest.mirror.xxxx.com/ios"

// 基础组件 - 网络请求
class NetWork {
  func request(baseUrl: String, path: String, param: [String:Any]) {

  }
}

//壳工程 - 网络请求中间件
class NetWorkMW {
  func request(baseUrl: String = BASEMIRRORURL, path: String, param: [String:Any]) {
    NetWork.request(baseUrl: baseUrl, path: path, param: param)
  }
}

NetWorkMW.request(path: "/g/login.server", param: param)

发现跳转视频播放

这个思路是使用代理,对外暴露点击事件,通过中间件,导入 视频播放 业务组件,topVC 基础组件,完成向 视频播放 的跳转:

// 业务组件 - 发现
func pushToPlayerVC(model: VideoModel) {
  delegate?.pushToPlayerVC?(videoModel: model)
}

// 中间件 - 发现
func pushToPlayerVC(model: VideoModel) {
  let vc = PlayerVC(videoModel: model)
  topVC.navigationVC.push(vc)
}

以上实际上是怎么样把多个组件组合使用起来,这种组合是确定的,还有一些是不确定的,例如有一个组件的状态改变了,我要让其他组件知道我的变化,但是我不知道都要告诉谁,怎么办?

眼珠一转,对外暴露状态变化,中间件在变化时发送通知。但是同时我想附带一个模型过去,通知的接收方怎样正确的使用这个模型呢?如果要使用模型,势必要和发送通知的业务组件产生耦合,怎么办?

以后再办,先埋个坑,这些场景我们会在以后再讲到。

组件分离的难点

组件分离的重点和难点也就是解耦,比如我们现在负责一个项目,其中的一个业务或者功能,希望实现组件化,但是它依赖于项目中的其他公共功能,该如何处理呢?这里提供两种思路:

  1. 拷代码,简单粗暴,摆脱依赖,对于一些不重要的工具方法,可以直接拷贝到内部来使用;

  2. 把组件依赖的代码先做一个 pod 库,然后依赖这个 pod 库;

上面讲到的是代码方面的依赖,还有一种情况是功能方面的依赖,比如我们有一个菜单,这个菜单涉及到网络图片的加载,那么怎样将这个菜单进行组件化呢?

使用 Block 或者代理,将网络图片加载这部分的职责交给外部控制;

举例来说,像下面这样:

// 业务组件 - 菜单
self.imageView.sd_setImage(with: url, completed: completed)

那么如果现在将它组件化,这个组件就要依赖于 SDWebImage,我们应该修改成这样:

// 业务组件 - 菜单
setImage?(for: imageView, completed: ImageLoadCompletedBlock)

// 中间件 - 菜单
menu.setImage = { (imageView, completed) in 
  imageView.sd_setImage(with: url, completed: completed)
}

现在菜单就摆脱了对 SDWebImage 的依赖。

作者:薛定諤

链接:https://juejin.im/post/59f2cc5d6fb9a04525775bce


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK