整理: MVVM 设计模式: 用 mobx 建立模型
目录
距离上一次初学的 MVVM 和 mobx,我已经把它们运用到生产环境2个月了,现再次整理一下,记下我的编码习惯和风格。
MVVM 的结构
MVVM 类似 MVC 结构,但 MVVM 更适合现时前后端分离解决方案的情形。它有 Model、View、ViewModel 3个结构:
-
Model: 提供业务接口,与后端通信,或调用系统接口
-
ViewModel: 持有 Model,处理界面事件,管理界面的变量
-
Model: 持有 ViewModel,用 vm 的变量来绘制界面,把界面事件移交给 vm。
例子: CDKey 兑换界面
一个 CDKey 的例子: 输入 CDK,点击确定后,提交到后端查看兑换结果。
构建 Model
Model 的行为很简单,只是把 CDKey 传送到后端并获取结果。这里写了一个抽象类,是因为这个它可能走 在线HTTP 方式请求后端,也可能走离线激活的方式验证 CDKey,据此可以把这个 Model 作为父类,派生出不同的实现方法。
abstract class CDKeyModel {
abstract redeem(cdKey: string): Promise<boolean>
static Factory = { // implementation
createOnlineVerifier (url: string): CDKeyModel {
return new class extends CDKeyModel {
async redeem(cdKey: string): Promise<boolean> {/* 发起 HTTP 请求获取结果 */}
}()
},
createOfflineVerifier (seed: string): CDKeyModel {
return new class extends CDKeyModel {
async redeem(cdKey: string): Promise<boolean> {/* 计算系统识别码计算结果 */}
}()
}
}
}
构建 ViewModel
ViewModel 对界面行为建模,从需求可以看到,vm 管理 CDKey 的输入状态,并处理提交事件。我更喜欢 ViewModel 通过事件发送一次性通知,比如弹窗通知3秒,这样可以不直接引用UI组件库,实现 vm 的可移植。
ViewModel 要求成员发生变化时,通知 View 层更新界面,这个过程相当于观察者模式,在这个实验用 mobxjs
实现,mobx 是一个强大的状态管理库,我们可以用它来对 ViewModel 建模。
mobx 提供 @observable、@action、@computed 等易用的装饰符。mobx 是 0依赖 的
@observable
被装饰的变量更新时,访问者会被通知@action
被装饰的方法执行后,访问者会被通知@computed
装饰 getter 方法makeObservable(object)
把 object 变成观察源
import { observable, action } from 'mobx'
enum Msg { RedeemOK, RedeemFail }
class CDKeyViewModel {
constructor (protected model: CDKeyModel) {
makeObservable(this)
}
channel = new EventEmitter<'msg'>()
@observable
cdKey = '';
@computed
get canSubmit() {
return !!this.cdKey
}
@action
inputCDKey (cdKey: typeof this.cdKey) {
this.cdKey = cdKey
}
@action
submit () {
this.model
.redeem(this.cdKey)
.then(() => this.channel.emit('msg', Msg.RedeemOK))
.catch((err) => this.channel.emit('msg', Msg.RedeemFail))
}
}
构建 View
这个阶段即是搭建界面,实现 CDKey 兑换界面千变万化,而这里则用最简单的输入框+按钮。
界面层可以用 Vue 实现,也可以用 React 实现,甚至 Flutter,View 有这些好处是因为 业务层model 和 界面控制器层vm 都分开来了,对应地,View 层则强依赖于所用的 UI 平台和组件库。
针对 React 平台
mobx 在 React 提供了观察者连接库 mobx-react-lite
,在 React 端可直接使用。
import React from 'react';
import { observer } from 'mobx-react-lite';
const CDKeyView = observer(({ vm }: { vm: CDKeyViewModel }) => {
React.useEffect(() => { // 处理一次性通知
const handleMsg = (msg: Msg) => {
switch (msg) {
case Msg.RedeemOK:
return alert("兑换成功")
case Msg.RedeemFail:
return alert("兑换失败")
}
}
vm.channel.addListener('msg', handleMsg)
return () => {
vm.channel.removeListener('msg', handleMsg)
}
}, [vm])
// 渲染界面
const { cdKey, canSubmit } = vm
return (
<form>
<label>CDKey:</label>
<input
name="cdkey" type="text"
value={cdKey} onChange={e => vm.inputCDKey(e.currentTarget.value)}
/>
<input
name="submit" type="submit"
disabled={canSubmit} onClick={() => vm.submit()}
/>
</form>
)
})
// 组装 Model、View 和 ViewModel
const CDKeyIndex = () => {
const model = React.useRef(
CDKeyModel.Factory.createOnlineVerifier("https://example.org")
).current
const vm = React.useRef(new CDKeyViewModel(model)).current;
return <CDKeyView vm={vm}>
}
针对 微信小程序平台
对于小程序,mobx 没有直接的组件支持,但可以直接用 mobx 的核心功能把 ViewModel 和 View 连起来。
在自行连接 mobx 对象的情况,必须要在适当的时候绑定观察、解除观察,否则会出现内存泄漏。
mobxjs 提供
autorun(callback): Function
方法,如果 callback 中访问的 mobx 对象有变化,则会触发 callback。返回一个 cancel 方法,可以取消观察。
import { autorun } from 'mobx'
Page({
data: { cdKey: '' },
vm: null,
dispose: null,
onLoad() { // 页面加载阶段,生成并连接 ViewModel
const handleMsg = (msg: Msg) => {
switch (msg) {
case Msg.RedeemOK:
return wx.showToast({ title: '兑换成功', icon: 'success', duration: 2000 });
case Msg.RedeemFail:
return wx.showToast({ title: '兑换失败', icon: 'error', duration: 2000 });
}
}
this.vm = new CDKeyViewModel(
CDKeyModel.Factory.createOnlineVerifier("https://example.org")
)
const unsubscribe = autorun(() => {
this.setData({ cdKey: vm.cdKey })
})
vm.channel.addListener('msg', handleMsg)
this.dispose = () => { // 布置解除连接
unsubscribe()
vm.channel.removeListener('msg', handleMsg)
this.vm = null
}
}
onUnload () { // 页面卸载阶段,调用布置好的方法
this.dispose?.()
}
onCDKeyInput (e) {
this.vm?.inputCDKey(e.detail.value)
}
onSubmitTap () {
this.vm?.submit()
}
})
<view>
<view>CDKey:</view>
<input bindinput="onCDKeyInput" value="{{ cdKey }}" />
<button bindtap="onSubmitTap">Submit</button>
</view>
其他平台
通过上方2个 View 的例子来看,React 有 mobx-react-lite
,而小程序没有连接库一样能手写连接 vm,所以在绝大多数情况下,mobxjs 可以在非常多的平台下实现业务移植,包括 Vue、React Native、Flutter、Electron,甚至 REPL 环境,etc。这正是我喜爱用 mobx 构建 MVVM 的原因所在。
Vue 版本有连接库
mobx-vue
什么时候使用 MVVM?
这个 CDKey 输入和提交的例子,当然可以直接几行代码解决完,但问题是,很多业务并不会像输个 CDKey 那么简单,有时候还会包括多语言,主题注入。这时候还坚持 KISS 原则,则在重构上会越来越辛苦。
我的总结是,在CRUD场景,直接用 CRUD 整合库代替 MVVM 可以省去大量时间,直到需要重构时,再用 MVVM 构建。而在其他有针对性的业务场景,还是直接 MVVM 构建吧,如果觉得分得太多,也可以把 ViewModel 和 Model 合起来,看这个页面的复杂程度。
我为什么比 Vue 更倾向于 React
我觉得 Vue 的语法绑得太死,虽然 Vue 本身就是 MVVM 的典型代表且支持双向绑定。但是 Vue 的模板渲染机制导致它的编码灵活性没有 React 高。
React 的弹框可以直接用代码实现,并支持不同的 content 展示方式。
showModal({
title: '提示',
content: <>
<p>Alert</p>
</>
})
Vue 则需要把 modal 侵入到页面代码中,以预编译的模板展示出来,同时占用页面逻辑一个 visible
data。当然这是 Vue 比 React 渲染得更快的原因。
<template>
<!-- 页面布局 -->
<!-- ... -->
<modal v-model="visible">
<modal-title>提示</modal-title>
<modal-content>
<p>Alert</p>
</modal-content>
</modal>
</template>
附录
[StackOverflow] Model 和 ViewModel 的区别
Model 包含业务代码,而 ViewModel 处理界面的回调,并对数据输入做处理。