让弹窗更易于使用~
标题又名:简单弹窗、多弹窗、复杂弹窗怎么做代码和状态的解耦。
关键字: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/>
}
问题和痛点:
- 类似Modal这样的组件,都会有自己的 visibility 状态,需要单独维护,即使封装Modal组件,问题依旧存在,还会有大量冗余代码。
- 如果弹窗在处理完内部流程后,又还有返回值,这有又要书写大量的处理函数。
- 最重要的是这些代码都写在一个文件中,阅读难度提高,极大的干扰了开发人员。
随着业务断增长,每次遇到类似的问题,都写上一大段 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>
)
}
像这样解耦状态,重新代码组织,更有利于开发和维护的工作。
实现思路
这里的思路如下
- 创建占位符组件,放到应用的某处。
- 将状态集中管理,并与相应Modal做好关联。
- 暴露控制权。
这样一来,调用方,只在乎传参和结果。其他都由弹窗内部自己去处理。
基于不同的状态管理方案,实现是多种多样的。相信很多大佬都自己内部封装过不少了。
但是在此基础上,我还想要3点。
- API使用简单,但也允许一定配置。
- 返回值类型推导,除了帮我管理 close 和 open 还要让我在传参,和接受返回值时得到类型提示。
- 无入侵性,不依赖框架以外的东西。
ez-modal-react
emm...苦于市面上找不到这类产品(感觉同类并不多……讨论的也不多?)
于是我自己开源了一个。回应上文实现思路,可以点击看代码,几百行而已。
其相关特性早已经受住了真实场景的考验,在百万级别的产品上稳定使用四五年了。我受前辈和社区启发写了这个库。
基本特性
- 基于Promise封装,方便异步调用。
- 提供传参类型约束,返回值类型推导。
- 没有入侵性(基于react context 通信),体积小。
- 不依赖特定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⭐~~~ 感恩,有什么问题请随时提出。我也会持续维护该项目。
下列诸神,望您不吝赐教!
转载自:https://juejin.cn/post/7238917620849246263