likes
comments
collection
share

让弹窗更易于使用~

作者站长头像
站长
· 阅读数 18

标题又名:简单弹窗、多弹窗、复杂弹窗怎么做代码和状态的解耦。

关键字:react / modal

前言

起初在你业务模块中,有一个复杂|简单的弹窗,需要展示,他可能耦合了本组件其他状态。

于是你想到了封装这个Modal组件,可无奈得将 visbitly状态 ,相关的handle方法申明在本业务组件中,又要将它们作为props传给组件。

最终免不得在组件中末尾写上一串<XXModal xxx={xxx} xxx={xxx} xxx={xxx}/>

然而这还没有结束,XXModal中既要申明接收,又要再经过二次封装调用具体handle。

现实情况

见识过许多企业项目,这种情况可谓屡见不鲜,但似乎没有较好的统一处理方案。

以下用简单的例子作说明,论如何有效解决这个问题,并给出稳定可行的企业级解决方案

问题案例

实际业务中,不乏弹窗组件中包含复杂的业务逻辑,如弹窗内的受控表单

  • 代码示意如下:
function Order() {
    // 省略上百行方法状态
    const [visible,setVisible] = useState(false)
    const withModalState = useState<any>()
    
    // 省略 很多相关处理useState 的 handlexx 函数
    
    return (
        <main>
            </* 省略其他业务代码 */>
            <Modal>
                <Input value={withModalState.input1}/>
                <Input value={withModalState.input2}/>
                <Select value={withModalState.select} />
                <Checkbox value={withModalState.checkbox}/>
                ...
            </Modal>
        </main>
    ) 
}

多弹窗的情形也并不少见。

  • 代码示意如下:
function Order() {
    // 省略上百行方法状态
    const [visible1,setVisible1] = useState(false)
    const [visible2,setVisible2] = useState(false)
    const [visible3,setVisible3] = useState(false)
    
    const withModalState1 = useState<any>()
    const withModalState2 = useState<any>()
    const withModalState3 = useState<any>()
    
    // 省略 很多相关处理useState 的 handlexx 函数
    return (
        <main>
            ...
            <Modal1>
               <Input value={withModalState1.input1}/>
                <Input value={withModalState1.input2}/>
                <Select value={withModalState1.select} />
                <Checkbox value={withModalState1.checkbox}/>
                ...
            </Modal1>
            <Modal2></* 省略很多代码 */></Modal3>
            <Modal3></* 省略很多代码 */></Modal3>

        </main>
    ) 
}

即使是你想到了封装组件的方法,还是避免不了冗余代码

import XXXModal from "./XXXModal.tsx"

function Order() {
    // 省略上百行方法状态
    const [visible,setVisible] = useState(false)
    const [other,setOther] = useState<any>()

    // 省略 很多相关处理useState 的 handlexx 函数
    return (
        <main>
            ...
            <XXXModal
                visible={visible}
                other1={xxx1}
                other2={xxx2}
                handleOpen={()=>setVisible(true)}
                handleClose={()=>setVisible(false)}
                handleOhter={()=>{
                    // ... 耦合业务组件本身的一些状态
                    Object.assign(xxx,{ohter1,other2})
                    // ...
                }}
            />
        </main>
    ) 
}

// XXXModal.tsx
export function XXXModal (props) {
    // 接收,冗余代码
    const {visibl,other1,other2,handleOpen,handleClose,handleOhter} = props
    
    const handleXXX = ()=>{
        if(xx) handleClose()
        if(xx) handleOpen()
        if(xx) handleOhter(other1,other2)
        ...
    }
    
    // ...ohter handle fn
    
    return <Modal> ... <Modal/>
}

问题和痛点:

  1. 类似Modal这样的组件,都会有自己的 visibility 状态,需要单独维护,即使封装Modal组件,问题依旧存在,还会有大量冗余代码。
  2. 如果弹窗在处理完内部流程后,又还有返回值,这有又要书写大量的处理函数。
  3. 最重要的是这些代码都写在一个文件中,阅读难度提高,极大的干扰了开发人员。

随着业务断增长,每次遇到类似的问题,都写上一大段 Modal 相关的代码,万一某天需求变动,由于可读性不佳,维护难度大大提高,最终代码越来越难懂

期望的使用方式

因此有没有一种更贴近业务实际的方案。

让弹窗只专注于自己的内部事务,同时又能将控制权交给调用方呢。

或者说我就是不喜欢 Modal 代码堆在一起……

  • 代码示意如下:
// Modal.tsx 弹窗组件单独写作一个文件
export function Modal(props) {
    //... 内部处理逻辑
    return <Modal></* ... */></Modal>
}

// 甚至可以将MOdal中的逻辑做的更聚合。通过2次封再导出给各方使用
export const function Check_XXX_Window(args) {/* ...差异化处理 */ return open(Modal,args)}
export const function Check_XXX_By_Id_Window(args) {/* ...差异化处理 */ return open(Modal,args)}
export const function Check_XXX_Of_Ohter_Window(args) {/* ...差异化处理 */ return open(Modal,args)}

// Order.tsx 在业务组件引入使用
function Order() {
    
    async function xxHandle {
        // 🔴调用方,不关注Modal内部细节。只关注传参,返回值。
        const expect = await Check_XXX_Window(Modal,args) // 🔴调用方法,传入参数,获得预期值 ,完成1个流程
        
        if(expect) ...
    }
    
    return (
        <main>
            ...
            // 不实际挂载 Modal 组件
        </main>
    ) 
}

像这样解耦状态,重新代码组织,更有利于开发和维护的工作。

实现思路

这里的思路如下

  1. 创建占位符组件,放到应用的某处。
  2. 将状态集中管理,并与相应Modal做好关联。
  3. 暴露控制权。

这样一来,调用方,只在乎传参和结果。其他都由弹窗内部自己去处理。

基于不同的状态管理方案,实现是多种多样的。相信很多大佬都自己内部封装过不少了。

但是在此基础上,我还想要3点。

  1. API使用简单,但也允许一定配置。
  2. 返回值类型推导,除了帮我管理 close 和 open 还要让我在传参,和接受返回值时得到类型提示。
  3. 无入侵性,不依赖框架以外的东西。

ez-modal-react

emm...苦于市面上找不到这类产品(感觉同类并不多……讨论的也不多?)

于是我自己开源了一个。回应上文实现思路,可以点击看代码,几百行而已。

其相关特性早已经受住了真实场景的考验,在百万级别的产品上稳定使用四五年了。我受前辈和社区启发写了这个库。

基本特性

  1. 基于Promise封装,方便异步调用。
  2. 提供传参类型约束,返回值类型推导。
  3. 没有入侵性(基于react context 通信),体积小。
  4. 不依赖特定UI库,任何有显示/隐藏的组件,都可以接入。

使用画面

import EasyModal, { InnerModalProps } from 'ez-modal-react';

+ interface IProps extends InnerModalProps<'fybe?'> /*传入返回值类型*/ {
+   age: number;
+   name: string;
+ }

export const InfoModal = EasyModal.create(
+ (props: IProps) => {
  return (
    <Modal
      title="Hello"
      open={props.visible}
      onOk={() => {
+       props.hide(); // warn 应有 1 个参数,但获得 0 个。 (property) hide: (result: "fybe?") => void ts(2554)
      }}
      onCancel={() => {
       props.hide(null); //safe hide 接受 null 作为参数。它兼具 hide resolve 两种功能。
      }}
    >
      <h1>{props.age}</h1>
    </Modal>
  );
});

+ /* 在任何其他地方引入并使用 */
+ // warn 类型 "{ name: string; }" 中缺少属性 "age",但类型 "ModalProps<Props, "fybe?">" 中需要该属性。
EasyModal.show(InfoModal, { name: 'foo' }).then((resolve) => {
+  console.log(resolve); //输出 "fybe?" 
});

也支持用 hook

import EasyModal, { useModal, InnerModalProps } from 'ez-modal-react';

interface IProps extends InnerModalProps<'苏振华'>/* 指定返回值类型 */ {
  age: number;
  name: string;
}

export const Info = EasyModal.create((props: IProps) => {
  const modal = useModal<IProps>(); 

  function handleOk(){
      modal.hide(); // ts(2554) (property) hide: (result: "苏振华") => void ts(2554)
  } 

  return  <Modal open={modal.visible} onOk={handleOk} onCancel={modal.hide}></Modal>
});


EasyModal.show(Info,{age:18,}) // 缺少属性 "name"

可以看到,如果希望得到类型推导,那么在书写组件props接口时,继承 InnerModalProps<T> 并传入泛型参数即可。

这样在调用 EasyModal.show()方法时 时就可以得到类型提示,和返回值推导

支持配置弹窗隐藏时的默认行为

还有一些特性如支持配置 hide 弹窗时的默认行为。(我认为大多数情况下可能用不上)

import EasyModal, { useModal, InnerModalProps } from 'ez-modal-react';

+interface IProps extends InnerModalProps<'苏振华'>/* 指定返回值类型 */ {}
    
export const Info = EasyModal.create((props: Props) => {
  const modal = useModal<Props>(); 

  function handleOk(){
      modal.hide(); 
+     modal.resolve('苏振华') // 需要手动抛出成功
+     modal.remove() // 需要手动注销组件。可用于反复打开弹窗,但是不希望状态被清除的场景。
  } 

  return  <Modal open={modal.visible} onOk={handleOk} onCancel={modal.hide}></Modal>

// Ohter.tsx
EasyModal.open(Info, {},
+  config:{
+    resolveOnHide:false, // 默认为 true ,为false意味着隐藏弹窗时,不会自动resolve该Promise
+    removeOnHide:false,  // 默认为 true ,为false意味着隐藏弹窗时,不会自动注销该弹窗组件(仍在DOM树上)
+  }
).then(resolve=>{
+   console.log(resolve) // 弹窗内部手动调用 resolve 方法,得到 '苏振华'
})

以上是针对弹窗内有复杂业务场景的状况。

简易弹窗

大部分场景都是调用方只在乎 open或close 状态,仅解耦状态一项好处,就可以让代码组织更加优雅。

如开发中常见的,展示、设置等弹窗,不需要使用类型推导。

import EasyModal, { useModal } from 'ez-modal-react';

inferface Props {
    [key:string]:any
}

export const Info = EasyModal.create((props: Props) => {
  const modal = useModal<Props>(); 
  return  
      <Modal open={modal.visible} onOk={modal.hide} onCancel={modal.hide}>
          ...
      </Modal>

// Ohter.tsx
EasyModal.open(Info);

当然,还有不需要考虑维护和代码组织的情况,自由书写即可,开发一切视情况而定。

仓库

其他就不一一介绍了,主要的已经说完了,想了解更多,可以看看仓库。github

🎮 Codesandbox Demo

Codesandbox是一个线上集成环境,可以直接打开玩玩。点击 Demo Link 前往

初衷

让弹窗使用更轻松。

ez ,在DOTA2东南亚服意为——“就这?太简单了”,通常在对局结束后用于嘲讽败方如"ez mid",这就是 ez-modal-react的取名的由来。

授之于鱼叉

GitHub仓库地址 :ez-modal-react

觉得有用帮我点个star⭐~~~ 感恩,有什么问题请随时提出。我也会持续维护该项目。

下列诸神,望您不吝赐教!