前言
在项目中会遇到某个页面,需要配备多个弹窗页,然而为了受控每个弹窗,那么就需要定义多个 visible/open
,然后在HTML
结构中还要定义多个 <Modal> 标签,虽说可以封装组件啥的,但也还是贼特么难顶。。。
NiceModal
NiceModal
调用后会返回一个 Promise
,当点击 OK
时,调用 resolve
,点击 cancel
时,调用 reject
import NiceModal from '@ebay/nice-modal-react'
import MyModal from './MyModal'
// 直接调用组件
NiceModal.show(MyModal, { someProp: 'hello' }).then(() => {
// do something if the task in the modal finished.
})
// 使用注册名调用
NiceModal.register('my-modal', MyModal)
NiceModal.show('my-modal', { someProp: 'hello' }).then(() => {
// do something if the task in the modal finished.
})
这写法有啥好处?
-
不需要再在每个页面中去挂载
Modal
组件 -
利用 Promise,多个弹窗关联的时候,会非常舒服
// 当点击 ok 时,显示下一个弹窗 await NiceModal.show(Modal1) await NiceModal.show(Modal2) await NiceModal.show(Modal3)
-
调用处显而易见
// 举个栗子 // 普通Modal: 有一个按钮,其内部 onClick 事件单击之后是打开弹窗。 const onClick = () => { // 一般都是利用 useState 创建一个受控的属性 // 但他不够直观,你有时候还需要去找找 visible 是受控给了哪个 Modal 上了 setVisible(true) } // NiceModal: 非常直观清楚我是展开了哪个 Modal const onClick = () => { NiceModal.show(Modal1) }
源码分析
咱这分析的不一定好,如果有啥不对的地方,指出来咱相互探讨下~
先来看看它的几个重要的内部属性:
export interface NiceModalState {
id: string
args?: Record<string, unknown>
visible?: boolean
delayVisible?: boolean
keepMounted?: boolean
}
// Modal 的Context
const NiceModalContext = React.createContext<NiceModalStore>({})
// Id 的Context,只用来读取当前Modal对应的id, 很重要,建议mark一下,到后面代码就可以理解了
const NiceModalIdContext = React.createContext<string | null>(null)
// 弹窗注册列表, 弹窗组件会在这里
const MODAL_REGISTRY: {
[id: string]: {
comp: React.FC<any>
props?: Record<string, unknown>
}
} = {}
// useReducer 返回值,因为多个地方共用,所以先在外部声明
let dispatch
Provider
首先咱先自己捋一捋,如果让我们自己实现,第一步大概会如何做?
- Provide 挂载到 App.tsx 中,然后利用 reducer 去改变触发渲染,NiceModal 也是如此实现的
export const Provider = ({ children }) => {
const [modals, _dispatch] = useReducer(reducer, {})
dispatch = _dispatch
return (
// 该 value 最终用 reducer 去控制
<NiceModalContext.Provider value={modals}>
{children} {/* children 一般为 App.tsx 中的 App组件 */}
<NiceModalPlaceholder /> {/* 该占位组件循环使用 modals 去创建组件 */}
</NiceModalContext.Provider>
)
}
// 当 dispatch 往里塞了一个组件信息时,会重新渲染该组件
const NiceModalPlaceholder = () => {
// 需要展示的弹窗都会在 modals 里
const modals = useContext(NiceModalContext)
// 获取 ModalId 的数组
const visibleModalIds = Object.keys(modals).filter((id) => !!modals[id])
// MODAL_REGISTRY 存放了弹窗组件
// 之所以把组件放 MODAL_REGISTRY 里,而不是 dispatch 时候,塞进 modals 里,个人认为是考虑到:
// 1. 弹窗组件是固定的,不固定的是弹窗组件的属性,如果 dispatch 频繁往里塞入,移除组件这种大体量的东西,那可能对性能啥的也有影响
// 2. 需要一个存放所有弹窗组件的东西, modals 只表示当前展示的弹窗。
const toRender = visibleModalIds
.filter((id) => MODAL_REGISTRY[id])
.map((id) => ({
id,
...MODAL_REGISTRY[id],
}))
return (
<>
{toRender.map((t) => {
// 记住这里的 id={t.id} mark 一下,后面要用。
return <t.comp key={t.id} id={t.id} {...t.props} />
})}
</>
)
}
MODAL_REGISTRY
挂载好了,那么接下来就是通信方面的问题了。
MODAL_REGISTRY
是什么时候有数据?- 又是什么时候调用
dispatch
?
从上面的案例 NiceModal.register('my-modal', MyModal)
可知,从一开始 NiceModal
调用了自身的 register
创建了一个 id
,让其与 MyModal
绑定。
// 在这里赋值了 MODAL_REGISTRY
// 这里的 comp 还不是一般的 组件,他是由 NiceModal.create 创建的组件
export const register = (id, comp, props) => {
if (!MODAL_REGISTRY[id]) {
MODAL_REGISTRY[id] = { comp, props }
} else {
MODAL_REGISTRY[id].props = props
}
}
为什么注册的组件是 create
包裹返回的 HOC
?
实际上如果是单纯调用 Modal.show('xxx')
, Modal.hide('xxx')
,那么可以不需要这个 HOC
,但是,如果要实现, show
了之后,隐藏或者其他的操作由组件内部自己完成,不需要用户多添加代码,那岂不是更妙不可言~
那么,来瞅瞅 create ,为了便于兄弟盟理解,我先写一个简单版(props
传递 hide
方法)的
export const create = (Comp) => {
return ({ defaultVisible, keepMounted, id, ...props }) => {
const args = useModal(id)
return <Comp {...props} {...args} />
}
}
// 利用 create 实现一个 弹窗组件
export const ModalOne = create((props) => {
const onCancel = () => {
props.hide()
}
return (
<Modal title="ModalOne" onCancel={onCancel} {...props}>
这里是 ModalOne
</Modal>
)
})
这里代码非常简单,create
给 ModalOne
包装了 useModal
返回的 args
, 让其 props
内有了 hide,show
等方法可以供组件调用。
useModal
内部肯定是使用拿到的 id
,去创建了一些方法,那些方法调用了 dispatch
先不看这个 useModal
,咱们先瞅瞅这个 id ,这个 id
怎么来的?
这个
id
怎么来的?个
id
怎么来的?
id
怎么来的?
重要的事,说三遍……恕我太菜,当时真的看这个看了半天没发现。。
首先排除 ModalOne
, 因为这是 儿子, create
是 爸爸,我们需要再往上找,找到爷爷
=.=
细心的哥们可能会发现, register(id, comp)
之前的这个注册方法里,有这么一个 id, 其内部实现是 MODAL_REGISTRY[id] = { comp, props }
然后在 NiceModalPlaceholder
里有这么一段实现,<t.comp key={t.id} id={t.id} {...t.props} />
是的,这个 t.comp
就是 create 包裹返回的 HOC
=.=
useModal
那么 Ok,知道了 id 从哪来的,现在看看 useModal
又做了什么操作?
// 注: 这里源码 modal 可能为3种值: id字符串, modal组件, undefined
export function useModal(modal, args) {
// 用来获取当前id的 modalInfo 的
const modals = useContext(NiceModalContext)
// 我上个例子中的 create 还没用到,先 mark 一下
const contextModalId = useContext(NiceModalIdContext)
// ================================ 处理Id Start ========================
let modalId = null
// 这里就是判断,如果存在modal,且不为字符串,那么就说明是组件了
const isUseComponent = modal && typeof modal !== 'string'
// 这里如果 modal 为 undefined ,那么会使用 context 上的 id
if (!modal) {
modalId = contextModalId
} else {
// getModalId 会根据你给的类型,如果是 string, 那么直接返回,如果是 modal,那么会帮你生成一个 id
modalId = getModalId(modal)
}
// ================================ 处理Id End =========================
const mid = modalId
useEffect(() => {
// 如果是组件,且未注册,那么给他注册一下
if (isUseComponent && !MODAL_REGISTRY[mid]) {
register(mid, modal, args)
}
}, [isUseComponent, mid, modal, args])
// show 和 hide 的方法, 这里还有一些 resolve ,reject 等方法,我没copy过来,想了解的可以源码深入下,其实都差不多就是了
const showCallback = useCallback((args) => show(mid, args), [mid])
const hideCallback = useCallback(() => hide(mid), [mid])
const modalInfo = modals[mid]
return {
id: mid,
args: modalInfo?.args,
visible: !!modalInfo?.visible,
show: showCallback,
hide: hideCallback,
}
}
迂回 create 方法
刚刚我说的 create 方法利用了 props 去传递 hide 等方法。那么实际上 NiceModal 用了 Context 去做,这里就用到了之前 mark
的 NiceModalIdContext
export const create = (Comp) => {
return ({ defaultVisible, keepMounted, id, ...props }) => {
const { args, show } = useModal(id)
return (
<NiceModalIdContext.Provider value={id}>
<Comp {...props} {...args} />
</NiceModalIdContext.Provider>
)
}
}
// 利用 create 实现一个 弹窗组件
export const ModalOne = create((props) => {
const modal = useModal()
const onCancel = (e) => {
modal.hide()
}
return (
<Modal title="ModalOne" onCancel={onCancel} {...modal} {...props}>
这里是 ModalOne
</Modal>
)
})
这里简单说下, 我们刚刚也知道 useModal
的实现, 他有一段我注释 mark
的代码 const contextModalId = useContext(NiceModalIdContext)
, 当 ModalOne
调用 useModal()
时(这里不传),会利用 NiceModalIdContext
获取到 id
并返回 modal(show,hide 等方法)
总结
我只检举了一些 NiceModal 的主要实现,感兴趣的哥们可以移步源码。
下面说下大概流程操作。
- Provide 挂载,内部创建 dispatch
- create 创建 Modal 组件
- register(注册 id)
- 页面中使用 NiceModal.show(id)调用
- 使用 dispatch 修改该 id 的属性
- dispatch 通知 Provide 内部的组件渲染
暂无评论内容