这是咱们团队号工程化系列文章的第 3 篇,全系列文章如下:

团队尚有HC,感兴趣的小伙伴能够私信~(注明期望岗位城市:北京、上海、杭州)

Modal 办理体操二三事  (实践篇)



前语

“别再弹框了,每次都是弹框,弹框套弹框用户怎么用啊?”

“OK OK,不必弹框你说用什么?”

上述对话在无数个场景下重复产生,也侧面说明在中台项目开发过程中,模态框(ModalDrawer 等)元素在各事务体系中随处可见,遭到广阔产品/设计同学的偏心,咱们前端研制同学对它也是“又爱又恨”,即依靠它处理问题又被随之提高的代码杂乱度提高所困扰。这里有同学就会说了,分明是一个根底组件,运用方法也简略,有啥“杂乱度”可言?

Modal 办理体操二三事  (实践篇)

OK OK,那咱们接着往下看~


Modal 三宗罪

让咱们从一个实践的事务需求出发,剖析一下对着 Modal 组件直接撸代码会有哪些问题。

Modal 办理体操二三事  (实践篇)

假定设计稿有以下几个需求点:

  1. 点击批阅工单按钮,拉起 Modal 弹窗

  2. 弹窗需求展现当时工单的基本信息,以及批阅状况

  3. 弹窗支撑填写补白

  4. 弹窗支撑经过/回绝


当这样一个需求扔过来,基本一切前端都能直接秒了:

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>

所以咱们把需求调整一下,仍然是常见的事务需求,实践代码开发难度仍是不高-用上面的逻辑持续搬砖就行,不过这时分一些问题就逐渐露出出来了。

Modal 办理体操二三事  (实践篇)

其实每个弹窗的杂乱度都不高,可是如果将一切逻辑都堆积在主组件中,将引发以下 3 个问题。

污染事务代码,状况办理杂乱

状况越加越多,而且这些状况都与主流程无关

经过剖析示例代码不难发现,为了完成这个事务模态框,直接在组件中引入了 visiblerecordposting 共 3 个状况,以及一个 form 实例。可实践上,关于主流程来说,批阅流程仅仅一个分支,唯一所需的交互不过是批阅完成后刷新工单状况。

当同一个页面存在多个模态框时,还需求手动办理每一个的状况,包含不能一同展现多个、操控的状况不能互相影响、设置状况的次序等。再加上新增的这些状况如果与主组件有交互,将直接提高代码的保护难度。

能够说一个组件保护的状况越多,它的保护本钱自然也会增多。


功用问题大,简单引发事务代码、弹层内重复烘托

每次弹窗组件状况变更,都会导致页面一切组件从头烘托

因为一切状况都保护在主组件,任何变化都将导致整个页面的从头烘托:

  • 主组件的状况更新引起一切子组件的从头烘托(即便 visible 是 false)
  • 子组件的状况更新也会同步到主组件,主组件全体从头烘托

因为 visible 只操控 Modal,而不操控 Modal 的子组件,所以即便 visible 为 false,Modal 的子组件仍然可能影响功用。

<Modal visible={visible}>
  <LargeComponent />
</Modal>

逻辑分裂,弹层内外的逻辑联动性差

模态框从交互上创建了独立的工作流,也从代码逻辑上带来了分裂感

在实践开发中,开发者会将较为杂乱或者可复用的 Modal 内容组件进行封装,比方 ApproveForm,可是封装后就会面临一个问题,即 ModalApproveForm 没有良好的通讯机制。

比方点击 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 完美结合

在中后台场景中,经常遇到弹窗与表单结合的功用,此时除了根底的弹窗办理之外,需求额定考虑表单办理的问题。以 antdform 为例,咱们通常会用以下方法之一组织代码

  1. 主页面办理 form 实例,并经过参数传递给弹窗子组件

  2. 弹窗子组件内部保护 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 个状况都已经从主组件抹去,其中 visibleposting 都自动由 useFormModal 进行办理,在事务开发中主组件/子组件都不需求重视,而 record 状况本身就仅仅中间状况,它仅仅为了在 Trigger <=> Modal 之间进行信息传递。

一同 form 实例默认也不再需求办理(可是支撑手动办理),开发者主需求联系详细的事务逻辑:比方表单元素、提交接口、刷新列表等详细动作。

如此一来,能够说是完美的处理了前文说到的一切问题。

总结

弹窗的办理本质仍是状况的办理,本文从事务场景中常见的 Modal 组件出发,剖析在日常开发中关于 Modal 状况办理的“三宗罪”:状况办理杂乱、简单引发功用问题、主子组件通讯难,并结合开发经验给出了一些优化技巧。

此外还评论了针对详细事务场景的定制化处理方案,经过 Trigger 封装Ref 办理Hook 调用等技巧,尽量从根本上去除 Modal 组件对主组件代码的状况带来的办理难题。

终究在弹窗内运用表单的场景,参考 Modal.useModal,进一步封装了 useFormModal,在贴合开发者心智的前提下,定向处理了该场景下状况办理困难的问题。

期望本文对我们有所帮助,欢迎留言评论~

参考资料