唠叨一个工作中让人不太爽的场景,现在可以的UI组件库一大堆,但在实际业务开发中他们提供的Dialog组件(有的库叫做Model,文中统一称作Dialog)却没有一个用的顺手。怎么回事呢?
我们先看一个业务场景设计。
一,业务场景
现在有一个弹框注册功能:
- 1,注册信息表单
- 2,可以控制关闭
- 3,可以控制显示
class Register {
isShow = false
show() {
this.isShow = true
this.render()
}
close() {
this.isShow = false
this.render()
}
render() {
if (!this.isShow) return null
console.log('display register form')
}
}
过来一段时间产品来了一个需求,要做一个弹框登录功能:
- 1,登录信息表单
- 2,可以控制关闭
- 3,可以控制显示
现在怎么办呢,接到需求撸起袖子就干的做法:
把Register
类copy一下造一个Login
类,然后修改render()
的逻辑这个需求很快就搞定了。不管后面要增加多少个类似功能都是这样复制粘贴就行了。
从上面的需求实现可以发现两个问题:
- 1,在页面A中注册功能是dialog交互形式,假设现在有个页面B注册功能是直接作为页面元素一直存在的。问题:dialog的交互逻辑和注册业务逻辑耦合
- 2,当要增加一个相同交互的登录功能时,选择直接copy注册功能的代码进行修改。问题:代码复用问题
解决问题
1,逻辑复用
// Dialog 基础类提取共用逻辑
class Dialog {
isShow = false
show() {
this.isShow = true
this.render()
}
close() {
this.isShow = false
this.render()
}
render() {
throw new Error('请实现render')
}
}
// 注册功能;只需实现注册表单信息渲染
class Register extends Dialog {
render() {
if (!this.isShow) return null
console.log('display register form')
}
}
// 登录功能:只需实现登录表单信息渲染
class Login extends Dialog {
render() {
if (!this.isShow) return null
console.log('display login form')
}
}
2,解耦
// 通用注册
class Register {
render() {
console.log('display register form')
}
}
// 通用登录
class Login {
render() {
console.log('display login form')
}
}
// dialog交互逻辑
class Dialog{
isShow = false
form = null
show() {
this.isShow = true
this.render()
}
close() {
this.isShow = false
this.render()
}
render() {
throw new Error('请实现render')
}
}
// 弹窗注册实现
class RegisterDialog extends Dialog {
constructor() {
this.handle = new Register()
}
render() {
if (!this.isShow) return null
this.handle.render()
}
}
// 弹框登录实现
class LoginDialog extends Dialog {
constructor() {
this.handle = new Login()
}
render() {
if (!this.isShow) return null
this.handle.render()
}
}
在进行面向对象开发时,我经常会听到少用继承,为什么呢?扩展性太差(现在是通过
isShow
来控制显示,如果后期要增加一个disabled
的条件则每个子类都要做修改)。在上面的解耦过程中虽然实现了dialog与通用注册功能的解耦,但RegisterDialog类又与Register强关联,在实现login的dialog逻辑时又重新创建了一个LoginDialog,这样有导致了复用的问题。哪怎么办呢?
控制反转
// 通用注册
class Register {
render() {
console.log('display register form')
}
}
// 通用登录
class Login {
render() {
console.log('display login form')
}
}
// 通用dialog
class Dialog {
constructor(handle) {
this.isShow = false
this.handle = handle
}
show() {
this.isShow = true
this.render()
}
close() {
this.isShow = false
this.render()
}
render() {
if (!this.isShow) return null
this.handle && this.handle.render()
}
}
// 注册dialog
const registerDialog = new Dialog(new Register())
registerDialog.show()
// 注册页面
const register= new Register()
register.render()
// 登录dialgo
const loginDialog = new Dialog(new Login())
loginDialog.show()
// 登录页面
const login = new Login()
login.render()
通过上面的场景分析可以总结出Dialog
的设计思路,更重要的是明确了Dialog
的职责。接下来进入本文正题,为什么会BB现有的Dialog
组件设计。
二,拥有什么
下面两张截图来自两个比较知名的vue组件库的官方文档demo
截图1
截图2 使用方式总结如下:<dialog>
<child />
</dialog>
看起来好像和我们前面的设计思路完全吻合
const register = new Register()
const registerDialog = new Dialog(register)
但是。。。
在业务代码中的实际使用情况
具体业务场景
// contenxt.vue
<tempalte>
<div>
<div>hello world!</div>
<Dialog>
<!-- 注册表单信息 -->
<Form>
...
<Input />
...
</Form>
</Dialog>
</div>
</template>
因为注册的逻辑是独立,而且上面的写法让context.vue逻辑比较重,相信大多数同学实际的使用方式如下(将registerDialog抽离为一个单独的组件)
// registerDialog.vue
<template>
<Dialog>
<Form>
....
<Input />
...
</Form>
</Dialgo>
</template>
// context.vue
<template>
<div>
<div>hello world</div>
<RegisterDialog />
</div>
</template>
那么问题就来了,registerDialog让dialog和注册的逻辑变成了强耦合,当我们要实现login的逻辑
// loginDialog.vue
<template>
<Dialog>
<Form>
....
<Input />
...
</Form>
</Dialgo>
</template>
// context.vue
<template>
<div>
<div>hello world</div>
<RegisterDialog />
<LoginDialog />
</div>
</template>
后面每次接到类似的需求都得重复registerDialog
,loginDialog
的逻辑,同时要处理vue组件属性和事件的传递。这种大家普遍使用的registerDialog
方式是设计上的倒退,为什么会出现这种情况呢?
三,理想实现
<template>
<div>
<div>hello world</div>
<!-- 注册 -->
<Dialog>
<Register />
</Dialog>
<!-- 登录 -->
<Dialgo>
<Login />
</Dialog>
</div>
</template>
个人看来这是一种最理想的实现方案,为什么这种方式并没有出现在我们的代码中呢?
原因:目前所有的Dialog组件不支持这种方式
register有提交表单和重置表单的操作,dialog组件有确定和取消两个按钮,在实际业务场景中dialog的确定操作应该和表单的提交、dialog的取消操作应该和表单的重置进行关联。可是现有的dialog组件并有实现这种关联关系
// 现有的方案
class Register {
submit() {
console.log('提交表单')
}
reset() {
console.log('重置表单')
}
render() {
console.log('渲染表单')
}
}
class Dialog {
constructor(handle) {
this.isShow = false
this.handle = handle
}
show() {
this.isShow = true
this.render()
}
cancle() {
this.isShow = false
this.render()
}
ok() {
this.isShow = false
this.render()
}
render() {
if (!this.isShow) return null
this.handle && this.handle.render()
}
}
const registerDialog = new Dialog(new Register())
// dialog 只控制了register的显示和隐藏,无法感知register的submit、reset
register.ok() // 我想提交form然后关闭dialog,但是做不到
这是现阶段我在使用dialog组件时一个比较苦恼的问题,那么这个问题是不是没法解决呢?
改造Dialog
class Dialog {
constructor(handle) {
this.isShow = false
this.handle = handle
}
show() {
this.isShow = true
this.render()
}
cancle() {
this.isShow = false
// 取消按钮执行form的reset
this.handle && this.handle.reset()
this.render()
}
ok() {
this.isShow = false
// 确定按钮执行form的submit
this.handle && this.handle.submit()
this.render()
}
render() {
if (!this.isShow) return null
this.handle && this.handle.render()
}
}
const registerDialog = new Dialog(new Register()
registerDialog.show()
registerDialog.ok() // 会执行register的submit方法
vue实现
伪代码,实际设计应该考虑适应多种场景
// Dialog.vue
<template>
<div>
...
<div class="body">
<slot />
</div>
<div class="footer">
<Button @click="handleCancle">取消</Button>
<Button @click="handleOk">确定</Button>
</div>
</div>
</template>
<script>
export default {
...
mounted() {
this.handle = this.$children.length > 0 ? this.$children[0] : null
},
methods: {
handleCancle() {
this.visible = false
this.handle && this.handle.$emit('onReset')
},
handleOk() {
this.visible = false
this.handle && this.handle.$emit('onSubmit')
}
}
}
</script>
四,最后
目前常见的UI框架,vue系的element-ui、iview,react系的ant-design关注点都只在dialog的交互上,从业务角度出发个人认为他们在设计上是有缺陷的。当然在自己的项目中可以选择高阶组件的方式改造现有Dialog组件。