这是咱们团队号工程化系列文章的第 3 篇,全系列文章如下:
团队尚有HC,感兴趣的小伙伴能够私信~(注明期望岗位城市:北京、上海、杭州)
前语
“别再弹框了,每次都是弹框,弹框套弹框用户怎么用啊?”
“OK OK,不必弹框你说用什么?”
上述对话在无数个场景下重复产生,也侧面说明在中台项目开发过程中,模态框(Modal
、Drawer
等)元素在各事务体系中随处可见,遭到广阔产品/设计同学的偏心,咱们前端研制同学对它也是“又爱又恨”,即依靠它处理问题又被随之提高的代码杂乱度提高所困扰。这里有同学就会说了,分明是一个根底组件,运用方法也简略,有啥“杂乱度”可言?
OK OK,那咱们接着往下看~
Modal 三宗罪
让咱们从一个实践的事务需求出发,剖析一下对着 Modal 组件直接撸代码会有哪些问题。
假定设计稿有以下几个需求点:
-
点击批阅工单按钮,拉起
Modal
弹窗 -
弹窗需求展现当时工单的基本信息,以及批阅状况
-
弹窗支撑填写补白
-
弹窗支撑经过/回绝
当这样一个需求扔过来,基本一切前端都能直接秒了:
const [visible, setVisible] = useState(false)
const [posting, setPosting] = useState(false)
const [record, setRecord] = useState({})
const [form] = Form.useForm()
const handleOk = () => {...}
return <div>
<Button onClick={() => }>{...}</Buton>
<Modal visible={visible} confirmLoading={posting} {...}>
<div>{record.title}</div>
<Form form={form}>
<Form.Item><Input /></Form.Item>
</Form>
</Modal>
</div>
所以咱们把需求调整一下,仍然是常见的事务需求,实践代码开发难度仍是不高-用上面的逻辑持续搬砖就行,不过这时分一些问题就逐渐露出出来了。
其实每个弹窗的杂乱度都不高,可是如果将一切逻辑都堆积在主组件中,将引发以下 3 个问题。
污染事务代码,状况办理杂乱
状况越加越多,而且这些状况都与主流程无关
经过剖析示例代码不难发现,为了完成这个事务模态框,直接在组件中引入了 visible
、record
、posting
共 3 个状况,以及一个 form
实例。可实践上,关于主流程来说,批阅流程仅仅一个分支,唯一所需的交互不过是批阅完成后刷新工单状况。
当同一个页面存在多个模态框时,还需求手动办理每一个的状况,包含不能一同展现多个、操控的状况不能互相影响、设置状况的次序等。再加上新增的这些状况如果与主组件有交互,将直接提高代码的保护难度。
能够说一个组件保护的状况越多,它的保护本钱自然也会增多。
功用问题大,简单引发事务代码、弹层内重复烘托
每次弹窗组件状况变更,都会导致页面一切组件从头烘托
因为一切状况都保护在主组件,任何变化都将导致整个页面的从头烘托:
- 主组件的状况更新引起一切子组件的从头烘托(即便
visible
是 false) - 子组件的状况更新也会同步到主组件,主组件全体从头烘托
因为 visible
只操控 Modal
,而不操控 Modal
的子组件,所以即便 visible
为 false,Modal
的子组件仍然可能影响功用。
<Modal visible={visible}>
<LargeComponent />
</Modal>
逻辑分裂,弹层内外的逻辑联动性差
模态框从交互上创建了独立的工作流,也从代码逻辑上带来了分裂感
在实践开发中,开发者会将较为杂乱或者可复用的 Modal
内容组件进行封装,比方 ApproveForm
,可是封装后就会面临一个问题,即 Modal
和 ApproveForm
没有良好的通讯机制。
比方点击 Modal
提交按钮,怎么获取 ApproveForm
的表单状况?一同,visible
特点仅仅操控 Modal
的,在不额定操控的情况下,Modal
的生命周期和 ApproveForm
的生命周期是独立的:
-
ApproveForm
默认会执行初始化逻辑,即便此时visible
仍是 false -
ApproveForm
永久不会走到毁掉状况,无法在封闭弹窗的时分清除内部状况
高雅体操办理
初露锋芒
状况多?那就兼并!
经过将一个弹窗所需的相关状况兼并到目标中,既直接减少了状况量,一同也可避免一个个设置状况可能带来的额定了解本钱,降低误操作的可能性。
const [editModalInfo, setEditModalInfo] = useState({
visible: false,
posting: false,
record: {},
})
<Modal visible={editModalInfo.visible} {...}>
<Edit detail={editModalInfo.record} {...} />
</Modal>
逻辑分裂?仍是兼并!!
已然 Modal
组件与内容组件通讯困难,不如直接将他们兼并封装为一个组件,这总没问题了吧~
const [editModalInfo, setEditModalInfo] = useState({
visible: false,
posting: false,
record: {},
})
const handleOk = () => {...}
<EditModal visible={editModalInfo.visible} record={editModalInfo.record} onOk={handleOk} {...} />
还能够把 posting
等内部特点移到 EditModal
组件内部办理,如此主组件又少保护了一个状况。
// EditModal.tsx
const handleOk = () => {
// 先处理内部逻辑,再调用传入的onOk
setPosting(true)
// do something
await props.onOk(...)
setPosting(false)
}
<Modal {...} onOk={handleOk} />
封装为 EditModal 之后,前面说的生命周期的问题就会再度呈现,即 EditModal 的 visible 为 false 时,仍然会执行初始化逻辑,可是通常这个时分一些必填参数是拿不到的-通常再 visible 为 true 的时分一同传入,同样还会呈现功用问题,或者弹窗封闭时无法清除状况的问题。
这种情况下,能够加个 HOC,直接在 visible 为 false 时毁掉这个事务弹窗,轻松处理这些问题。
// visible改变时直接毁掉组件,不需求保护生命周期
export const oneTimeHoc = <Props extends Record<string, any>>(Component: React.FC<Props>) => {
return (props: Props & { visible?: boolean }) => {
return props?.visible !== false ? <Component {...props} /> : null
}
}
// EditModal.tsx
export default oneTimeHoc(EditModal) // 此时EditModal的入参需求移除visible
还杂乱?那就持续兼并!!!
如果有多个弹窗组件,那么仍是需求保护多份xxxModalInfo
状况以及实例化多个XxxModal
组件,有几个弹窗就得保护几组状况,杂乱度仍然很高,怎么办?那就加个中间层!
新增 ActionModal
链接主组件与对应的多个事务 Modal
,这样主组件只需求保护一个大状况-一般情况下同一时间只会存在一个激活的 Modal
,经过 ActionModal
再来做一层转发,成功将代码杂乱度涣散,然后降低主组件的保护杂乱度。
// index.tsx
const [modalInfo, setModalInfo] = useState({
type: '', // edit | update | approve
visible: false,
modalProps: {},
})
return <>
<ActionModal modalInfo={modalInfo} />
</>
// ActionModal.tsx
const { type, visible, modalProps } = props
if (!visible) return null
return <>
{type === 'edit' && <EditModal {...modalProps} />
{type === 'update' && <UpdateModal {...modalProps} />
</>
小结
以上能够看作咱们日常开发中处理问题的常见手段,的确能处理部分问题,可是显得有些治标不治本,不论怎么样,终究在主组件中仍然仍然需求办理弹窗的 visible
状况。
其实在多数场景下,都是由用户交互(如点击按钮)才唤起弹窗,那是否能够进一步,把交互元素(如按钮)与弹窗组件封装到一同,设计一种更为定制化的处理方案。
进阶技巧
初春 – Trigger 封装
关于主页面需求办理 visible 的问题,可经过cloneElement
对指定元素进行拓宽,屏蔽主页面临visible
的感知。
完成思路比较简略,主要给传入的元素加个onClick
特点,用来操控visible
展现,然后在onOk
的时分操控visible
封闭。
// TriggerModal.tsx
const { children, trigger } = props
const [visible, setVisible] = useState(false)
const onClick = () => {
setVisible(true)
}
const handleOk = async () => {
// do something
setVisible(false)
}
return <>
{React.cloneElement(trigger, { onClick })}
<Modal {...} visible={visible} onOk={handleOk} />
</>
// index.tsx
return <>
<TriggerModal trigger={<Button />}> // 仅拓宽Modal
{...}
</TriggerModal>
// 封装事务逻辑:EditModal = TriggerModal + 事务逻辑
// <EditModal trigger={<Button />} />
</>
长处:结合触发的元素一同封装,逻辑更内聚,合适特定事务场景(如批阅按钮)
缺陷:运用限制较大,不是通用的处理方案
半夏 – Ref 办理
关于在父组件中操作子组件状况这种事情,咱们自然而然的就会想到运用 ref
,下面就让咱们来看看要怎么用 ref
完成。
// EditModal.tsx
const [visible, setVisible] = useState(false)
const modalPropsRef = useRef({})
useImperativeHandle(ref, {
open: (props) => {
modalPropsRef.current = props
setVisible(true)
},
close: () => setVisible(false),
})
return <Modal {...} visible={visible} onOK={modalPropsRef.current?.onOk} />
// index.tsx
const editModalRef = useRef(null)
editModalRef.current.open(props)
editModalRef.current.close()
return <>
<EditModal {...} ref={editModalRef} />
</>
长处:简略好使,了解本钱低
缺陷:限制了父子组件的完成逻辑以及调用方法,用起来不行高雅
秋实 – Hook 调用
在方案三ref
的根底上,优化调用方法,从ref.current.open
优化成hook
回来的函数调用,即Modal.useModal
的回来值modal.open
,各组件库已经供给modal.confirm
等函数,可是没有open
,咱们能够简略封装一下:
// useNextModal.tsx 随意起的名字,别介意
const [modal, context] = Modal.useModal()
const [visible, setVisible] = useState(false)
const modalPropsRef = useRef({})
const nextModal = {
// 已有,直接用
confirm: (props) => {
return modal.confirm(props)
},
// 自行封装
open: (props) => {
modalPropsRef.current = props
setVisible(true)
},
}
const modalRender = () => {
if (!visible) return null
return <Modal {...modalPropsRef.current} visible={visible} />
}
return { nextModal, context, content: modalRender() }
// index.tsx
const { nextModal, context, content } = useNextModal()
nextModal.open(...)
nextModal.confirm(...)
return <>
{context} // modal.confirm的上下文
{content} // modal.open的dom
</>
长处:简略好使,调用方法更直接
缺陷:仅合适较简略的场景;hook
回来了 DOM,有点争议
不回来 DOM 的 Hook
const { formProps, modalProps } = useFormModal()
return <>
<Modal {...modalProps}>
<Form {...formProps} />
</Modal>
</>
瑞雪 – Modal 与 Form 完美结合
在中后台场景中,经常遇到弹窗与表单结合的功用,此时除了根底的弹窗办理之外,需求额定考虑表单办理的问题。以 antd
的 form
为例,咱们通常会用以下方法之一组织代码:
-
主页面办理
form
实例,并经过参数传递给弹窗子组件 -
弹窗子组件内部保护
form
实例,经过回调将表单的值露出出去
办理 form 实例
// index.tsx
const [form] = Form.useForm()
const [visible, setVisible] = useState(false)
const handleOk = async () => {
const values = await form.validateFields()
await service.submit(values)
setVisible(false)
}
const handleOpen = () => {
form.setFieldsValue({ ... }) // 灵活操控form
setVisible(true)
}
return <>
<Button onClick={handleOpen}>Open</Button>
<EditModal visible={visible} onOk={handleOk} form={form} />
</>
// EditModal.tsx
const { form, visible, onOk } = props
return <Modal visible={visible} onOk={onOk}>{...}</Modal>
长处:运用简略,方便运用 form
办理弹窗组件内的表单
缺陷:主页面需求多保护一个 form
实例,有一定杂乱度
优化一下
在仍支撑主页面操控 form
的前提下,将一部分逻辑放到弹窗组件内部处理,减少主页面的代码量。
// index.tsx
const handleOk = async (values) => {
await service.submit(values)
setVisible(false)
}
// EditModal.tsx
const { form: outerForm, visible, onOk } = props
const [form] = Form.useForm(outerForm)
const handleOk = async () => {
const values = await form.validateFields() // 尽量把相似的逻辑放在弹窗组件内部
onOk?.(values)
}
return <Modal visible={visible} onOk={handleOk}>{...}</Modal>
最佳实践 – useFormModal
回到最开始举的例子,如果是现在,那咱们就能够这样来完成:
// index.tsx
const [form] = Form.useForm()
const { formModal, content } = useFormModal({ form }) // form参数可选
// 能够将事务逻辑再封装
const { handleCreate, handleDelete } = useOtherActions({ formModal, refreshList })
const handleEdit = (info) => {
formModal.open({
title: 'Edit',
content: <EditForm info={info} />, // 不需求传form,只用传组件需求的参数
onOk: async (values) => {
await service.approve(values)
message.success('edit success')
},
onCancel: () => console.log('click cancel'),
})
}
const handleApprove = (info) => {
formModal.confirm({
title: 'Approve',
content: <ApproveForm info={info} form={form} />, // 手动操控form
onOk: async (values) => {
await service.update(values)
message.success('approve success')
},
onCancel: () => console.log('click cancel'),
})
}
return <>
<Button onClick={handleCreate}>Create</Button>
<List>
{list.map(item) => {
return <List.Item info={item} onClick={handleEdit} ...>
{item.name}
</List.Item>
}}
</List>
{content} // 组件dom
</>
// EditForm.tsx
const { form, info } = props
return <>
<div>ID: {info.name}</div>
<Form.Item label="Address" reruired>
<Input placeholder="Please input something" />
</Form.Item>
</>
从代码中不难看出,前文所说的 3 个状况都已经从主组件抹去,其中 visible
、posting
都自动由 useFormModal
进行办理,在事务开发中主组件/子组件都不需求重视,而 record
状况本身就仅仅中间状况,它仅仅为了在 Trigger
<=> Modal
之间进行信息传递。
一同 form
实例默认也不再需求办理(可是支撑手动办理),开发者主需求联系详细的事务逻辑:比方表单元素、提交接口、刷新列表等详细动作。
如此一来,能够说是完美的处理了前文说到的一切问题。
总结
弹窗的办理本质仍是状况的办理,本文从事务场景中常见的 Modal
组件出发,剖析在日常开发中关于 Modal
状况办理的“三宗罪”:状况办理杂乱、简单引发功用问题、主子组件通讯难,并结合开发经验给出了一些优化技巧。
此外还评论了针对详细事务场景的定制化处理方案,经过 Trigger 封装
、Ref 办理
、Hook 调用
等技巧,尽量从根本上去除 Modal
组件对主组件代码的状况带来的办理难题。
终究在弹窗内运用表单的场景,参考 Modal.useModal
,进一步封装了 useFormModal
,在贴合开发者心智的前提下,定向处理了该场景下状况办理困难的问题。
期望本文对我们有所帮助,欢迎留言评论~