MVMDemo
h5,iOS,Android, software architecture, MVM (Mediator, View, ViewModel)
相比MVC, MVVM, MVP等一些框架, MVM (Mediator, View, ViewModel)框架的思路非常简单, 基本上知道原理自己可以搭建,
核心思想是:
- 每个页面对应一个
Model
,Model
包含data
&function
;- 将页面UI和
data
双向绑定;- 页面
click event
调用对应function
;- toast, 页面跳转等等UI事件, 则根据subject进行跳转;(需要Rx框架基础,当然可以自己设计类似的方案)
一、MVM简介
本质上就是一个后端和UI的中间件, 向后端请求接口之后对数据解析, 输出一个UI需要的且可以直接使用的数据模型(包含data
和function
),前端直接渲染, UI开发无需关注逻辑,逻辑开发者无需关注UI;
demo中的 start
入门版,代码简单,初级开发也能轻易看懂
let ToastSubject = PublishSubject<String>
inputView.text <-bind-> mediator.text
button.click = function() {
mediator.click()
}
// vue
created() {
mediator.created()
ToastSubject.subscribe((value) => {
Toast(value)
})
}
// Android
fun onCreate(savedInstanceState: Bundle?) {
mediator.onCreate()
ToastSubject.subscribe {
Toast(value)
}
}
// iOS
func viewDidLoad() {
mediator.viewDidLoad()
ToastSubject.subscribe(onNext: {value in
Toast(value)
})
}
二、设计思路
-
- 创建一个Page(iOS: ViewController; Android: Activity or fragment; Vue:.vue)
-
- 将页面UI拆分不同组件, 每个组件对应一个
viewModel
,可以将UI需要的点击事件定义成ViewModel
中的闭包,
- 将页面UI拆分不同组件, 每个组件对应一个
-
- 创建一个Mediator, 向后端请求网络数据, 请求完成后, 初始化所有的ViewModel,给对应的变量赋值; 在
Mediator
中对ViewModel
中的闭包,进行初始化
- 创建一个Mediator, 向后端请求网络数据, 请求完成后, 初始化所有的ViewModel,给对应的变量赋值; 在
-
- 将
Mediator
给UI开发者, 直接根据Mediator
中的数据进行绑定渲染即可
- 将
三、具体实现和细节要求(方案参考)
-
- 按照页面拆分,每个页面有一个
Mediator
, 每个Mediator
通过MediatorManager Singleton
(demo中用Vuex
替代)
- 按照页面拆分,每个页面有一个
-
- 将一个页面按照行拆分成不同组件, 每种组件对应一种
ViewModel
, 通过Mediator
管理
- 将一个页面按照行拆分成不同组件, 每种组件对应一种
-
- 当一个页面组件非常多的时候,
Mediator
肯定会非常复杂; 这个时候可以通过Design pattern
对Mediator
进行拆分; 具体拆分看个人习惯和架构能力
- 当一个页面组件非常多的时候,
-
- public的
变量
和方法
一定要深思熟虑, 如果Mediator
封装不是很好, 只要对UI暴露的 api 没问题; 后续重构逻辑不会影响UI, 同样修改UI对逻辑影响很小
- public的
-
- 实际开发中还需要封装其他模块, 比如Router, Toast, Network等
根据需要进行
MediatorManager.getSingleton().mediator = new Mediator
MediatorManager.getSingleton().mediator = null
四、代码实现
下面代码可以看出, vue通过v-for; iOS 通过tableView; Android通过 Adapter直接进行渲染即可; 数据绑定放在每个页面的组件中; 逻辑全部在Mediator
中
具体实现请点击demo; github.com/AblerSong/M…
vue
<template>
<div class="advanced">
<van-cell-group>
<component :is="item.name" :model="item.model" v-for="(item, index) in list" :key="index" :class="item.cls" />
</van-cell-group>
</div>
</template>
<script>
import { mapState } from "vuex"
import Checkbox from "./components/Checkbox"
import Radio from "./components/Radio"
import Province from "./components/Province"
import Calculator from "./components/Calculator"
import Submit from "./components/Submit"
import Expand from "./components/Expand"
import Stretch from "./components/Stretch"
export default {
created() {
this.$store.dispatch("initAdvancedMediator")
import("./css/style.scss")
},
components: {
Radio,
Checkbox,
Province,
Calculator,
Submit,
Expand,
Stretch,
},
computed: {
...mapState({
advancedMediator: (state) => state.advancedMediator,
}),
list() {
let list = this.advancedMediator.list
return list
},
},
}
</script>
iOS
class AdvancedVC: UITableViewController {
let disposeBag = DisposeBag()
var mediator: AdvancedMediator {
get {
guard let mediator = MediatorManager.shared.advancedMediator else {
let mediator = AdvancedMediator()
MediatorManager.shared.advancedMediator = mediator
return mediator
}
return mediator
}
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(TextFieldCell.self, forCellReuseIdentifier: textFieldCellReuseIdentifier)
tableView.register(ExpandCell.self, forCellReuseIdentifier: "ExpandCell")
tableView.register(MoreCell.self, forCellReuseIdentifier: "MoreCell")
tableView.register(ProvinceCell.self, forCellReuseIdentifier: "ProvinceCell")
mediator.reloadTableView.subscribe(onNext: {[unowned self] value in
if value {
tableView.reloadData()
}
}).disposed(by: disposeBag)
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return mediator.list.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
let list = mediator.list[section]
return list.count
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let list = mediator.list[indexPath.section]
if let model = list[indexPath.row]["model"] as? ExpandCellViewModel {
return model.height
}
return 80
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let list = mediator.list[indexPath.section]
let reuseIdentifier = list[indexPath.row]["reuseIdentifier"] as! String
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! MyTableViewCell
let model = list[indexPath.row]["model"]
cell.viewModel = model
return cell
}
}
Android
class MyAdapter(var dataList: List<Map<String, Any>>) : RecyclerView.Adapter<BaseViewHolder>() {
private fun initViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
val inflater = LayoutInflater.from(parent.context)
val itemView = inflater.inflate(viewType, parent, false)
return when (viewType) {
R.layout.button_item -> ButtonViewHolder(itemView)
R.layout.input_item -> InputViewHolder(itemView)
R.layout.expand_item -> ExpandViewHolder(itemView)
R.layout.more_item -> MoreViewHolder(itemView)
R.layout.province_item -> ProvinceViewHolder(itemView)
else -> BaseViewHolder(itemView)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
return initViewHolder(parent, viewType)
}
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
val data = dataList[position]
holder.bind(data)
}
override fun getItemViewType(position: Int): Int {
val data = dataList[position]
return data["viewType"] as Int
}
override fun getItemCount(): Int {
return dataList.size
}
}
五、优缺点
优点 :
-
- 相比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方便
网上搜索了一下, 和MVM思想最近的应该是POM设计模式, 在iOS, Android, H5中还没搜到这方便的文章;
POM(Page Object Model):页面对象模型
Page Object Model, also known as POM, is a design pattern in Selenium that creates an object repository for storing all web elements.
- 在POM下,应用程序的每一个页面都有一个对应的model
- 每个model都维护着该web页面的元素集和操作这些元素的方法