简介 git仓库:https://github.com/eBay/nice-modal-react
nice-modal-react是ebay开源的一个非常好用的React弹窗管理库,适用于MUI,Antd等热门的组件库
源码分析 注册组件 要使用这个库,首先需要将应用用<NiceModal.Provider> 包裹
1 2 3 4 5 6 7 8 9 import NiceModal from '@ebay/nice-modal-react' ;ReactDOM .render ( <React.StrictMode > <NiceModal.Provider > <App /> </NiceModal.Provider > </React.StrictMode > , document .getElementById ('root' ), );
下面是<NiceModal.Provider> 的源码部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 const initialState : NiceModalStore = {};let dispatch : React .Dispatch <NiceModalAction > = () => { throw new Error ('No dispatch method detected, did you embed your app with NiceModal.Provider?' ); }; const InnerContextProvider : React .FC = ({ children } ) => { const arr = useReducer (reducer, initialState); const modals = arr[0 ]; dispatch = arr[1 ]; return ( <NiceModalContext.Provider value ={modals} > {children} <NiceModalPlaceholder /> </NiceModalContext.Provider > ); }; export const Provider : React .FC <Record <string , unknown >> = ({ children, dispatch: givenDispatch, modals: givenModals, }: { children: ReactNode; dispatch?: React.Dispatch<NiceModalAction>; modals?: NiceModalStore; } ) => { if (!givenDispatch || !givenModals) { return <InnerContextProvider > {children}</InnerContextProvider > ; } dispatch = givenDispatch; return ( <NiceModalContext.Provider value ={givenModals} > {children} <NiceModalPlaceholder /> </NiceModalContext.Provider > ); };
这个Provider主要做了两件事:
设置状态管理函数,如果没传入自定义的,就使用useReducer。管理的是所有弹窗的数据
添加一个弹窗的占位<NiceModalPlaceholder/>
当<NiceModalContext.Provider>的value更新时,触发子元素的更新,当然包括<NiceModalPlaceholder>,这样弹窗才会更新显影状态
那么看看modals是怎么被更新的,我们去看看reducer函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 export const reducer = ( state : NiceModalStore = initialState, action : NiceModalAction , ): NiceModalStore => { switch (action.type ) { case 'nice-modal/show' : { const { modalId, args } = action.payload ; return { ...state, [modalId]: { ...state[modalId], id : modalId, args, visible : !!ALREADY_MOUNTED [modalId], delayVisible : !ALREADY_MOUNTED [modalId], }, }; } } };
看来当dispatch触发type为nice-modal/show的action时,会往modals里添加或修改一项弹窗数据
看下源码里,发现show()函数会dispatch这个action,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 function showModal (modalId : string , args ?: Record <string , unknown > ): NiceModalAction { return { type : 'nice-modal/show' , payload : { modalId, args, }, }; } export function show ( modal : React .FC<any > | string , args ?: NiceModalArgs <React .FC<any >> | Record <string , unknown >, ) { const modalId = getModalId (modal); if (typeof modal !== 'string' && !MODAL_REGISTRY [modalId]) { register (modalId, modal as React .FC ); } dispatch (showModal (modalId, args)); }
register函数就简单了,就是将传入的数据记录在对象里。 所有注册过的弹窗都会被渲染在<NiceModalPlaceholder/>里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const MODAL_REGISTRY : { [id : string ]: { comp : React .FC <any >; props ?: Record <string , unknown >; }; } = {}; export const register = <T extends React .FC <any >>( id : string , comp : T, props ?: Partial <NiceModalArgs <T>>, ): void => { if (!MODAL_REGISTRY [id]) { MODAL_REGISTRY [id] = { comp, props }; } else { MODAL_REGISTRY [id].props = props; } };
来看看<NiceModalPlaceholder/>做了什么 当<NiceModalContext.Provider>的value更新时,触发子元素的更新,当然包括<NiceModalPlaceholder>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 const NiceModalPlaceholder : React .FC = () => { const modals = useContext (NiceModalContext ); const visibleModalIds = Object .keys (modals).filter ((id ) => !!modals[id]); visibleModalIds.forEach ((id ) => { if (!MODAL_REGISTRY [id] && !ALREADY_MOUNTED [id]) { console .warn ( `No modal found for id: ${id} . Please check the id or if it is registered or declared via JSX.` , ); return ; } }); const toRender = visibleModalIds .filter ((id ) => MODAL_REGISTRY [id]) .map ((id ) => ({ id, ...MODAL_REGISTRY [id], })); return ( <> {toRender.map((t) => ( <t.comp key ={t.id} id ={t.id} {...t.props } /> ))} </> ); };
创建弹窗组件 当然,在这里渲染的弹窗组件也并非随便一个组件就行,据官方的文档,这里的弹窗组件需要用create方法创建
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import { Modal } from 'antd' ;import NiceModal , { useModal } from '@ebay/nice-modal-react' ;export default NiceModal .create (({ name }: { name: string } ) => { const modal = useModal (); return ( <Modal title ="Hello Antd" onOk ={() => modal.hide()} visible={modal.visible} onCancel={() => modal.hide()} afterClose={() => modal.remove()} > Hello {name}! </Modal > ); });
下面是create函数的源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 export const create = <P extends {}>( Comp : React .ComponentType <P>, ): React .FC <P & NiceModalHocProps > => { return ({ defaultVisible, keepMounted, id, ...props } ) => { const { args, show } = useModal (id); const modals = useContext (NiceModalContext ); const shouldMount = !!modals[id]; useEffect (() => { if (defaultVisible) { show (); } ALREADY_MOUNTED [id] = true ; return () => { delete ALREADY_MOUNTED [id]; }; }, [id, show, defaultVisible]); useEffect (() => { if (keepMounted) setFlags (id, { keepMounted : true }); }, [id, keepMounted]); const delayVisible = modals[id]?.delayVisible ; useEffect (() => { if (delayVisible) { show (args); } }, [delayVisible, args, show]); if (!shouldMount) return null ; return ( <NiceModalIdContext.Provider value ={id} > <Comp {... (props as P )} {...args } /> </NiceModalIdContext.Provider > ); }; };
这部分稍微复杂,我们来分段看一下
首先从整体来说,create函数的作用就是在传入的组件外层套了个Provider,并且给组件添加了一些额外的props
1 2 3 4 5 6 7 8 9 10 11 12 13 export const create = <P extends {}>( Comp : React .ComponentType <P>, ): React .FC <P & NiceModalHocProps > => { return ({ defaultVisible, keepMounted, id, ...props } ) => { return ( <NiceModalIdContext.Provider value ={id} > <Comp {... (props as P )} {...args } /> </NiceModalIdContext.Provider > ); }; };
注意到这里从外部接收id这个props并且将其用上下文的形式传给子组件,那这个id是从哪来的?
实际上就是从<NiceModalPlaceholder>传入的,其他的props也是。create函数创建的组件会通过register函数注册最后在<NiceModalPlaceholder>渲染并传入一些props
1 2 3 4 5 6 7 8 9 10 11 const NiceModalPlaceholder : React .FC = () => { return ( <> {toRender.map((t) => ( <t.comp key ={t.id} id ={t.id} {...t.props } /> ))} </> ); };
再看看主体部分,第一行用useModal来获取了一些方法
1 const { args, show } = useModal (id);
那这个useModal又做了什么
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 export function useModal (modal ?: any , args ?: any ): any { const modals = useContext (NiceModalContext ); const contextModalId = useContext (NiceModalIdContext ); let modalId : string | null = null ; const isUseComponent = modal && typeof modal !== 'string' ; if (!modal) { modalId = contextModalId; } else { modalId = getModalId (modal); } if (!modalId) throw new Error ('No modal id found in NiceModal.useModal.' ); const mid = modalId as string ; useEffect (() => { if (isUseComponent && !MODAL_REGISTRY [mid]) { register (mid, modal as React .FC , args); } }, [isUseComponent, mid, modal, args]); const modalInfo = modals[mid]; const showCallback = useCallback ((args ?: Record <string , unknown > ) => show (mid, args), [mid]); const hideCallback = useCallback (() => hide (mid), [mid]); const removeCallback = useCallback (() => remove (mid), [mid]); const resolveCallback = useCallback ( (args ?: unknown ) => { modalCallbacks[mid]?.resolve (args); delete modalCallbacks[mid]; }, [mid], ); const rejectCallback = useCallback ( (args ?: unknown ) => { modalCallbacks[mid]?.reject (args); delete modalCallbacks[mid]; }, [mid], ); const resolveHide = useCallback ( (args ?: unknown ) => { hideModalCallbacks[mid]?.resolve (args); delete hideModalCallbacks[mid]; }, [mid], ); return useMemo ( () => ({ id : mid, args : modalInfo?.args , visible : !!modalInfo?.visible , keepMounted : !!modalInfo?.keepMounted , show : showCallback, hide : hideCallback, remove : removeCallback, resolve : resolveCallback, reject : rejectCallback, resolveHide, }), [ mid, modalInfo?.args , modalInfo?.visible , modalInfo?.keepMounted , showCallback, hideCallback, removeCallback, resolveCallback, rejectCallback, resolveHide, ], ); }
上面是useModal的源码,因为useModal不仅提供给create函数使用,所以参数和判断逻辑较多。 我们先看create函数里调用的useModal的用法,即不传任何参数,下面是精简后的结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 export function useModal (modal ?: any , args ?: any ): any { const modals = useContext (NiceModalContext ); const contextModalId = useContext (NiceModalIdContext ); let modalId : string | null = null ; modalId = contextModalId; if (!modalId) throw new Error ('No modal id found in NiceModal.useModal.' ); const mid = modalId as string ; const modalInfo = modals[mid]; const showCallback = useCallback ((args ?: Record <string , unknown > ) => show (mid, args), [mid]); return useMemo ( () => ({ args : modalInfo?.args , show : showCallback, }), [ ], ); }
再来看看create函数,同样精简一下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 const ALREADY_MOUNTED = {};export const create = <P extends {}>( Comp : React .ComponentType <P>, ): React .FC <P & NiceModalHocProps > => { return ({ defaultVisible, keepMounted, id, ...props } ) => { useEffect (() => { ALREADY_MOUNTED [id] = true ; return () => { delete ALREADY_MOUNTED [id]; }; }, [id, show, defaultVisible]); const delayVisible = modals[id]?.delayVisible ; useEffect (() => { if (delayVisible) { show (args); } }, [delayVisible, args, show]); }; };
精简后就两个useEffect,不过在看这部分逻辑前,再回顾下前文提到的reducer函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 export const reducer = ( state : NiceModalStore = initialState, action : NiceModalAction , ): NiceModalStore => { switch (action.type ) { case 'nice-modal/show' : { const { modalId, args } = action.payload ; return { ...state, [modalId]: { ...state[modalId], id : modalId, args, visible : !!ALREADY_MOUNTED [modalId], delayVisible : !ALREADY_MOUNTED [modalId], }, }; } } };
重点关注visible字段和delayVisible字段,可以看到逻辑上他们是相反的,取决于ALREADY_MOUNTED里维护的对应弹窗的值(状态)
那么再看看create函数,首先第一个useEffect里将ALREADY_MOUNTED里弹窗的状态改为true,不过改了visible字段也不会立刻变为true,delayVisible字段也不会立刻变成false
接下来获取delayVisible字段的值,此时是true,那么就会触发show函数
1 2 3 4 5 6 7 const { args, show } = useModal (id);if (delayVisible) { show (args); }
上面看过useModal的代码,暴露出来的show函数实际上就是最外层的show函数包了一层 那么就会再触发dispatch,就会更新modals里的数据,visible和delayVisible字段也随之更新
而且modals数据的变动会触发<NiceModalPlaceholder/>的更新,并将最新的props传入弹窗组件,因为visible变更为true了,所以弹窗出现
弹窗的Promise 在官方示例里可以看到,show函数实际返回的是个Promise
1 2 3 4 5 6 7 8 9 10 NiceModal .show (AddUserModal ) .then (() => { fetchUsers () }) .catch (err => { });
那么现在就需要看看上文中未分析的show函数另一半代码了 当调用show函数时,会创建一个Promise,并将其记录下来(在调用hide函数时会删掉)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const modalCallbacks : NiceModalCallbacks = {}export function show ( modal : React .FC<any > | string , args ?: NiceModalArgs <React .FC<any >> | Record <string , unknown >, ) { if (!modalCallbacks[modalId]) { let theResolve!: (args ?: unknown ) => void ; let theReject!: (args ?: unknown ) => void ; const promise = new Promise ((resolve, reject ) => { theResolve = resolve; theReject = reject; }); modalCallbacks[modalId] = { resolve : theResolve, reject : theReject, promise, }; } return modalCallbacks[modalId].promise ; }
可以在useModal中获取到上面的resolve和reject方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 export function useModal (modal ?: any , args ?: any ): any { const resolveCallback = useCallback ( (args ?: unknown ) => { modalCallbacks[mid]?.resolve (args); delete modalCallbacks[mid]; }, [mid], ); const rejectCallback = useCallback ( (args ?: unknown ) => { modalCallbacks[mid]?.reject (args); delete modalCallbacks[mid]; }, [mid], ); const resolveHide = useCallback ( (args ?: unknown ) => { hideModalCallbacks[mid]?.resolve (args); delete hideModalCallbacks[mid]; }, [mid], ); return useMemo ( () => ({ resolve : resolveCallback, reject : rejectCallback, resolveHide, }), [ ], ); }
组件库的帮助方法 官方也包装好了一些帮助方法供组件库使用 这是官方示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import NiceModal , { muiDialog, muiDialogV5, antdModal, antdModalV5, antdDrawer, antdDrawerV5, bootstrapDialog } from '@ebay/nice-modal-react' ; const modal = useModal ();<Dialog {...muiDialog(modal)}> // For MUI V5 <Dialog {...muiDialogV5(modal)}> // For ant.design <Modal {...antdModal(modal)}> // For ant.design v4.23.0 or later <Modal {...antdModalV5(modal)}> // For antd drawer <Drawer {...antdDrawer(modal)}> // For antd drawer v4.23.0 or later <Drawer {...antdDrawerV5(modal)}> // For bootstrap dialog <Dialog {...bootstrapDialog(modal)}>
实际上就是兼容了一些组件库之间有差异的props命名,并且在弹窗关闭的时候调用resolve
1 2 3 4 5 6 7 8 9 10 11 12 13 14 export const antdModal = ( modal : NiceModalHandler , ): { visible : boolean ; onCancel : () => void ; onOk : () => void ; afterClose : () => void } => { return { visible : modal.visible , onOk : () => modal.hide (), onCancel : () => modal.hide (), afterClose : () => { modal.resolveHide (); if (!modal.keepMounted ) modal.remove (); }, }; };
杂项
defaultVisible这个参数仅在以<MyNiceModal defaultVisible />这种方式声明时生效,在register函数传入是不生效的。这个写法也不是官方推荐的写法,略过。