场景复现
在后台管理系统的 CRUD 开发当中,免不了需要校验表单的操作。比如,校验表单的参数是否合法,当然,很多组件库的表单组件都支持表单校验,码农们只需要写好校验规则即可。
今天遇到的需求场景就是在编辑一条表格中的数据的时候要判断用户是否修改了表单的数据,如果没有,那么就提示没有发生修改,无法提交! 如果用户修改了数据才可以提交。
这时候你可能会想到拷贝一份表单的备份数据,然后在提交的时候判断两个表单的数据是否一样。如果一样,那么就没有发生修改。否则,就发生了修改。
代码演示
接着上一篇文章的案例,对一个用户的数据编辑。
点击提交按钮提交表单,点击重置按钮恢复表单修改前的状态。给提交按钮绑定一个 submitForm
函数传入表单引用:
js
<el-dialog v-model="dialogProps.visible" :title="dialogProps.title" width="500" @closed="handleClosed(dialogFormRef)">
<el-form :model="dialogForm" ref="dialogFormRef" label-width="100" :disabled="dialogProps.viewMode">
<el-form-item label="姓名: " required prop="name">
<el-input v-model="dialogForm.name" />
</el-form-item>
<el-form-item label="年龄: " required prop="age">
<el-input v-model="dialogForm.age" />
</el-form-item>
<el-form-item label="地址: " required prop="address">
<el-input v-model="dialogForm.address" />
</el-form-item>
<el-form-item label="" required>
<el-col v-show="!dialogProps.viewMode">
<el-button type="primary" round @click="submitForm(dialogFormRef)">提交</el-button>
<el-button round @click="restoreForm">重置</el-button>
</el-col>
</el-form-item>
</el-form>
</el-dialog>
const dialogProps = ref({
addOrUpdate: true,
visible: false,
title: '',
viewMode: false
})
const dialogForm = ref({
name: '',
age: '',
address: ''
})
const dialogFormBackup = ref({})
function handleEdit(row) {
dialogProps.value.visible = true
dialogProps.value.title = '修改用户数据'
dialogProps.value.viewMode = false
dialogProps.value.addOrUpdate = false
nextTick(() => {
Object.assign(dialogForm.value, row)
dialogFormBackup.value = cloneDeep(dialogForm.value)
})
}
function submitForm(formRef) {
if (!formRef) {
return
}
formRef.validate(valid => {
if (!valid) {
return
}
// 新增
if (dialogProps.value.addOrUpdate) {
// 调用后端API...
ElMessageBox.alert('提交成功', '提示', {
confirmButtonText: 'OK'
})
// 修改
} else {
if (dialogForm.value.name === dialogForm.value.name &&
dialogForm.value.age === dialogForm.value.age &&
dialogForm.value.address === dialogForm.value.address) {
return ElMessageBox.alert('没有发生修改,提交失败!', '错误', { confirmButtonText: 'OK', type: 'error' })
}
// 调用后端API...
ElMessageBox.alert('提交成功', '提示', {
confirmButtonText: 'OK'
})
}
})
}
最终的效果如下,提示没有发生修改:
这里使用了 lodash 库的深拷贝函数,拷贝了一份表单的备份数据,最后在提交表单的时候进行比较。但是这肯定是有缺点的,虽然实现了功能但是十分地不优雅。
试想一下,如果一个表单有十几个字段甚至更多,那你这个 if 条件得要写多少行?最终写成了屎山代码:
js
if (dialogForm.value.name === dialogForm.value.name &&
dialogForm.value.age === dialogForm.value.age &&
dialogForm.value.address === dialogForm.value.address &&
dialogForm.value.address === dialogForm.value.address &&
dialogForm.value.address === dialogForm.value.address &&
dialogForm.value.address === dialogForm.value.address &&
dialogForm.value.address === dialogForm.value.address &&
dialogForm.value.address === dialogForm.value.address &&
dialogForm.value.address === dialogForm.value.address &&
dialogForm.value.address === dialogForm.value.address
......
) {
// ...
}
这时候必须得要优化一下,堆屎一时爽,重构火葬场。等到有需求变动的时候,码农们只能加班加点搞了,美好的周三和周五只能与你说拜拜咯。。。
优化思路
优化这种屎山代码必然要想到设计模式,设计模式就是为了让你好好地堆屎,不至于后面需求有大变动时你要爆肝做需求,可以让你只做扩展不做大的修改,符合软件设计开闭原则。
23 种设计模式中,备忘录模式正好是专门针对需要记录状态的场景。例如,保存游戏的存档,富文本编辑器撤销操作都会用到。
在这个需求场景中,我们需要记住原有表单的状态,也就是字段的值,然后在 if 条件中避免写一大堆比较条件。
那么我们就需要定义一个变量来保存表单的状态,其实和上面代码的思路大差不差。定义一个 FormMemo
类:
js
import { cloneDeep, isEqual } from 'lodash'
export class FormMemo {
_formBackup
constructor(form) {
this._formBackup = cloneDeep(form)
}
get formBackup() {
return this._formBackup
}
/**
* 保存表单状态
* @param form
*/
save(form) {
this._formBackup = cloneDeep(form)
}
/**
* 判断表单是否发生了修改
* @param form
* @return {boolean}
*/
hasChanged(form) {
Object.entries(this._formBackup).forEach(([key, value]) => {
if (!isEqual(form[key], value)) {
return true
}
})
return false
}
/**
* 重置表单
* @param form
*/
restore(form) {
Object.entries(this._formBackup).forEach(([key, value]) => {
form[key] = value
})
}
}
这边还是借助了 lodash 库来实现深拷贝和比较是否相等(递归比较)的操作,FormMemo
类的成员变量 _formBackup
是私有的无法直接访问,只能通过 get
方法访问。save
方法其实相当于 set
方法,深拷贝一份表单数据。hasChanged
方法用于判断表单是否发生了修改,遍历保存的表单变量然后判断新表单的字段值是否和对应的旧表单字段的值一致,只要有一个不一样那么就返回 true
,否则最后就返回 false
。restore
方法用于重置表单,把新表单恢复到旧表单的状态,相当于撤销用户当前的操作。
最终优化代码
来看看代码怎么写。
js
const dialogFormMemo = new FormMemo()
function handleEdit(row) {
dialogProps.value.visible = true
dialogProps.value.title = '修改用户数据'
dialogProps.value.viewMode = false
dialogProps.value.addOrUpdate = false
nextTick(() => {
Object.assign(dialogForm.value, row)
dialogFormMemo.save(dialogForm.value)
})
}
function submitForm(formRef) {
if (!formRef) {
return
}
formRef.validate(valid => {
if (!valid) {
return
}
// 新增
if (dialogProps.value.addOrUpdate) {
// 调用后端API...
ElMessageBox.alert('提交成功', '提示', {
confirmButtonText: 'OK'
})
// 修改
} else {
if (!dialogFormMemo.hasChanged(dialogForm.value)) {
return ElMessageBox.alert('没有发生修改,提交失败!', '错误', { confirmButtonText: 'OK', type: 'error' })
}
// 调用后端API...
ElMessageBox.alert('提交成功', '提示', {
confirmButtonText: 'OK'
})
}
})
}
function restoreForm() {
dialogFormMemo.restore(dialogForm.value)
}
可以看到最终效果还是一样的,这大幅简化了校验操作。来看一下重置操作,可以把表单还原:
思考
当然,这可能不是最标准的符合 GoF 23 种设计模式的备忘录模式,但是应付这种业务场景足矣。你有兴趣也可以使用标准的备忘录模式来重构这部分代码。主要是三个角色:Originator(发起者)
、Memento(备忘录)
、CareTaker(管理者)
。