likes
comments
collection
share

大前端设计模式MVM for iOS, Android, H5

作者站长头像
站长
· 阅读数 13

MVMDemo

MVM设计模式 for iOS, Android, H5; 大前端的POM设计模式

设计之初是按照MVM(Mediator, View, ViewModel)的思路设计的,完成后搜索了一下网上有没有类似思路, 最后发现了Page Object Model, also known as POM, is a design pattern in Selenium that creates an object repository for storing all web elements.; 在iOS, Android, H5中还没搜到这方面的文章

一、MVM简介

本质上就是一个后端和UI的中间件, 向后端请求接口之后对数据解析, 输出一个UI需要的且可以直接使用的数据模型(包含datafunction),前端直接渲染, UI开发无需关注逻辑,逻辑开发者无需关注UI;

demo中的 start 入门版代码简单移动,初级开发也能快速上手;

具体实现请点击github.com/AblerSong/M…

二、设计思路
  • 创建一个Page (iOS: ViewController; Android: Activity or fragment; Vue:.vue)
  • 将页面UI拆分不同组件, 每个组件对应一个 ViewModel, UI文字定义为 ViewModel 变量, UI点击事件定义成 ViewModel 闭包,
  • 创建一个 Mediator, 接口请求完成后, 根据后端返回的数据,初始化所有 ViewModel 的变量和闭包; 具体看 四、代码实现
  • Mediator给UI开发者, 直接根据Mediator中的数据进行绑定渲染即可
三、具体实现和细节要求(方案参考)
  • 1.按照页面拆分,每个页面有一个 PageMediator
  • 2.将一个页面按照拆分成不同组件, 每种组件对应一种 ViewModel, 通过 PageMediator 管理
  • 3.每个 PageMediator 通过 MediatorManager Singleton (demo中h5用 Vuex 替代); 这样的话所有的数据 keepAlive; UI没有 keepAlive   MediatorManager Singleton 管理 PageMediator, PageMediator 管理 ViewModel 如下:
MediatorManager.getSingleton().mediator = new Mediator
MediatorManager.getSingleton().mediator = null
  • 4.当一个页面组件非常多的时候, PageMediator 肯定会非常复杂; 这个时候可以通过 design patternPageMediator 进行拆分; 具体拆分看个人习惯和架构能力
  • 5.PageMediatorpublic 变量方法 一定要深思熟虑, 如果 PageMediator 封装不是很好, 只要对UI暴露的 api 没问题; 后续重构逻辑不会影响UI, 同样修改UI对逻辑影响很小
  • 6.实际开发中还需要封装其他模块, 方便后期使用 Unit Test 替代 UI Test; 比如Router, Toast, Network等, e.g.Toast:
class ToastViewModel {

    // 在 setter 中 进行Toast, 可以做到全局处理, 后期可以根据 变量值 进行 Unit Test
    // 实际开发中建议使用 Rx 系列框架
    set toast(value) {
        Toast(value)
    }
}
四、代码实现

大前端设计模式MVM for iOS, Android, H5

比如实现登录需求如上图

下面代码可以看出, vue通过绑定; iOS 通过tableView, Android通过 Adapter,直接根据list进行渲染即可; 数据绑定放在每个页面的组件中; 逻辑全部在Mediator

VUE

Mediator {
  username_text = "username"
  password_text = "password"
  _username_str = ""
  _password_str = ""
  login_btn_disabled = true

  constructor() {
    this.init()
  }
  init() {}

  set username_str(value) {
    this._username_str = value
    this.update_login_btn_disabled()
  }
  get username_str() {
    return this._username_str
  }

  set password_str(value) {
    this._password_str = value
    this.update_login_btn_disabled()
  }
  get password_str() {
    return this._password_str
  }

  update_login_btn_disabled() {
    this.login_btn_disabled = !(this.username_str?.length && this.password_str?.length)
  }

  onSubmit() {
    if (this.username_str == "admin" && this.password_str == "123456") {
      router.back()
    } else {
    }
  }
}

Android

class ButtonViewModel (
    val buttonState: BehaviorSubject<Boolean> = BehaviorSubject.createDefault(false),
    var buttonText: BehaviorSubject<String> = BehaviorSubject.createDefault(""),
) {
    var clickItem = {}
}

class InputViewModel (
    val text: BehaviorSubject<String> = BehaviorSubject.createDefault(""),
    val value: BehaviorSubject<String> = BehaviorSubject.createDefault("")
) {}

class Mediator : BaseMediator () {
    val usernameViewModel: InputViewModel = InputViewModel()
    val passwordViewModel: InputViewModel = InputViewModel()
    val buttonViewModel: ButtonViewModel = ButtonViewModel()

    init {
        usernameViewModel.text.onNext("username")
        passwordViewModel.text.onNext("password")

        val isNotEmpty: (String, String) -> Boolean = { name: String, age: String ->
            name.isNotEmpty() && age.isNotEmpty()
        }
        val d1 = Observable.combineLatest(usernameViewModel.value, passwordViewModel.value, isNotEmpty).subscribe {
            buttonViewModel.buttonState.onNext(it)
        }

        buttonViewModel.clickItem = {
            val username = usernameViewModel.value.value
            val password = passwordViewModel.value.value
            if (username == "admin" && password == "123456") {
                routerSubject.onNext(R.layout.activity_main)
            } else {
                ToastManager.toastSubject.onNext("input error")
            }
        }

        compositeDisposable.add(d1)
    }

    val dataList by lazy { initList() }

    private fun initList(): List<Map<String, Any>> {
        val m1 = mapOf(Pair("viewType", R.layout.input_item), Pair("viewHolder", InputViewHolder::class.java), Pair("ViewModel", usernameViewModel))
        val m2 = mapOf(Pair("viewType", R.layout.input_item), Pair("viewHolder", InputViewHolder::class.java), Pair("ViewModel", passwordViewModel))
        val m3 = mapOf(Pair("viewType", R.layout.button_item), Pair("viewHolder", ButtonViewHolder::class.java), Pair("ViewModel", buttonViewModel))

        return listOf<Map<String, Any>>(m1, m2, m3)
    }
}

iOS

class ButtonCellViewModel: BaseViewModel {
    let login_btn_disabled = BehaviorRelay(value: false)
    var onSubmit = {}
}

class TextFieldCellViewModel: BaseViewModel {
    let text = BehaviorRelay(value: "")
    let value = BehaviorRelay(value: "")
}

class Mediator: BaseMediator {
    let usernameViewModel = TextFieldCellViewModel()
    let passwordViewModel = TextFieldCellViewModel()
    let buttonCellViewModel = ButtonCellViewModel()
    
    lazy var list: [[[String : Any]]] = {
        let arr: [[[String : Any]]] = [
            [
                ["model":usernameViewModel,"reuseIdentifier":textFieldCellReuseIdentifier],
                ["model":passwordViewModel,"reuseIdentifier":textFieldCellReuseIdentifier],
            ],
            [
                ["model":buttonCellViewModel,"reuseIdentifier":buttonCellReuseIdentifier]
            ]
        ]
        return arr
    }()
    
    override init() {
        super.init()
        
        initPasswordViewModel()
        initUsernameViewModel()
        initButtonCellViewModel()
    }
    
    func initUsernameViewModel() {
        usernameViewModel.text.accept("username")
    }
    func initPasswordViewModel() {
        passwordViewModel.text.accept("password")
    }
    func initButtonCellViewModel() {
        let combineLatest = Observable.combineLatest(usernameViewModel.value, passwordViewModel.value)
        
        combineLatest.map { (username: String, password: String) -> Bool in
            return username.count > 0 && password.count > 0
        }.bind(to: buttonCellViewModel.login_btn_disabled).disposed(by: disposeBag)
        
        
        buttonCellViewModel.onSubmit = {
            combineLatest.subscribe( onNext: { (username: String, password: String) in
                if username == "Admin", password == "123456" {
                    RouterBehaviorSubject.onNext(RouterModel(type: .pop))
                } else {
                    ToastBehaviorSubject.onNext("input error")
                }
            }).dispose()
        }
    }
}
五、优缺点

优点 :

  • 相比VIPER, MVI 等框架, 核心思想简单, 方便理解
  • UI 和 逻辑拆分, 方便任务拆解组合, 提高代码复用性
  • 理论上拆解合适, 可以通过对 Mediator 进行 unit test 替代 UI test; 非常容易进行白盒自动化测试
  • 由于 MediatorManager Singleton 存在; 相当于所有的数据 keepAlive; UI没有keepAlive; 数据唯一, 方便管理
  • 业务代码结构统一, 开发人员可以快速接手其他人的代码

缺点 :

  • 开发者不注意容易内存泄露,且不易定位
六、总结

从实际开发来看, 该框架非常非常适合h5; 比如demo(vue)中拆分成.vue, .scss, .js; 由于css文件的独立性, 极大的提高了代码的复用率;

对于h5,iOS和Android, 可以通过 Mediator 进行 Unit test 替代 UI test, 减少错误提高测试效率;

本人非常喜欢, 可以大幅提高自动化测试效率, 写UI Test太麻烦,还是 Unit Test方便;