本文最后更新于:2022年7月13日 凌晨
                  
                
              
            
            
              
                
                
最近在项目上尝试了 eBay/nice-modal-react  来处理态框,相对于我之前使用 Modal 的方式更加优雅。它是一个零依赖,小巧的模态框管理库,实现思路让我眼前一亮,值得学习。
 
概述 我们先来看看 nice-modal-react  的使用。
可以将它的使用分为三步,创建 Modal、注册 Modal 和使用 Modal。
创建 Modal 通过 NiceModal.create 高阶函数来创建可使用的 Modal,同时在内部使用 useModal 来管理当前 Modal 的相关状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import  { Modal  } from  'antd' ;import  NiceModal , { useModal } from  '@ebay/nice-modal-react' ;const  UserInfoModal  = NiceModal .create (({ name } ) =>  { 	 	const  modal = useModal (); 	return  ( 		<Modal  			title ="Hello Antd"  			onOk ={()  =>  modal.hide()}			onCancel={() => modal.hide()} 			afterClose={() => { 				modal.hideResolve(); 				modal.remove(); 			}} 		> 			Hello ${name}! 		</Modal >   	); });export  default  UserInfoModal ;
 
注册 Modal 有三种方式注册我们的 Modal。
1 2 3 4 5 6  <UserInfoModal  id="/user/info/edit"  />NiceModal .register ('/user/info/edit' , UserInfoModal ) <ModalDef  id='/user/info/edit'  component={UserInfoModal } />
 
当然这一步是可选的,不注册直接使用情况下,它会自动帮我们注册成全局 Modal。
接下来我们可以在任意地方使用它。
使用 Modal 首先,我们需要将加入全局上下文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' ) );
 
然后我们就可以通过 hooks 来解决各种 Modal 业务场景了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import  NiceModal , { useModal } from  '@ebay/nice-modal-react' ;import  UserInfoModal  from  './UserInfoModal' ;NiceModal .show (UserInfoModal , { userId : 666  });const  modal = NiceModal .useModal (UserInfoModal );const  modal = NiceModal .useModal ('/user/info/edit' ); modal.show ({ userId : 666  }); modal.show ({ userId : 666  }).then (refreshUserList);await  modal.hide ();
 
源码 接下来我们看看它的源码,核心文件只有一个 index.tsx  ,总共 500 多行,非常小巧。
创建 我们先来看看创建, NiceModal.create是一个高阶组件,它其实做的是根据 ModalId 从 Context 中获取当前 Modal 状态。如果当前 ModalId 不再 Context 中就不渲染当前 Modal,如果存在就将相关状态(props)和参数(args)传入对应 Modal。
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 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 >   		); 	}; };
 
注册 这里非常简单,就是加入一个全局变量(MODAL_REGISTRY)中,之后都将在 placeholder 中呈现。
1 2 3 4 5 6 7 export  const  register = <T extends  React .FC <any >>(id : string , comp : T, props?: NiceModalArgs <T>): void  =>  { 	if  (!MODAL_REGISTRY [id]) { 		MODAL_REGISTRY [id] = { comp, props }; 	} else  { 		MODAL_REGISTRY [id].props  = props; 	} };
 
使用 Provider 提供注册的 Modal 上下文,这里有一个非常核心的地方就是NiceModalPlaceholder,在这里实现所需要 Modal 的渲染。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 export  const  Provider : React .FC <Record <string , unknown >> = ({  	children, 	dispatch: givenDispatch, 	modals: givenModals, }: { 	children: ReactNode; 	dispatch?: React.Dispatch<NiceModalAction>; 	modals?: NiceModalStore; } ) =>  { 	 	return  ( 		<NiceModalContext.Provider  value ={givenModals} > 			{children} 			<NiceModalPlaceholder  />  		</NiceModalContext.Provider >   	); };
 
NiceModalPlaceholder的实现非常简单,从 context 中获取需要展示的 ModalId,同时从 MODAL_REGISTRY 中获取 Modal 信息,过滤后进行渲染。
当我们调用modal.show()时,会添加 Modal 信息,就会渲染对应 Modal。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const  NiceModalPlaceholder : React .FC  = () =>  { 	const  modals = useContext (NiceModalContext ); 	 	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 } />  			))} 		</>   	); };
 
useModal 从 context 中获取 ModalId 对应的状态和参数,如果不存在当前 ModalId,就注册一个。同时返回对应的 props。
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 export  function  useModal<T extends  React .FC <any >>( 	modal : T, 	args?: NiceModalArgs <T> ): NiceModalHandler <NiceModalArgs <T>>;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); 	} 	 	const  mid = modalId as  string ; 	 	useEffect (() =>  { 		if  (isUseComponent && !MODAL_REGISTRY [mid]) { 			register (mid, modal as  React .FC , args); 		} 	}, [isUseComponent, mid, modal, args]); 	 	return  { 		id : mid, 		args : modalInfo?.args , 		visible : !!modalInfo?.visible , 		keepMounted : !!modalInfo?.keepMounted , 		show : showCallback, 		hide : hideCallback, 		remove : removeCallback, 		resolve : resolveCallback, 		reject : rejectCallback, 		resolveHide, 	}; }
 
这里有个细节就是 getModalId 会在每个 Modal 组件上写入一个 SymbolId,也就是说组件即使重复注册,会使用同一个 Id。
1 2 3 4 5 6 7 const  getModalId = (modal : string  | React .FC <any >): string  =>  { 	if  (typeof  modal === 'string' ) return  modal as  string ; 	if  (!modal[symModalId]) { 		modal[symModalId] = getUid (); 	} 	return  modal[symModalId]; };
 
三方组件库支持 它对于第三方组件也有支持,实现非常简单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 export  const  antdDrawer = ( 	modal : NiceModalHandler  ): { visible : boolean ; onClose : () =>  void ; afterVisibleChange : (visible: boolean  ) =>  void  } => { 	return  { 		visible : modal.visible , 		onClose : () =>  modal.hide (), 		afterVisibleChange : (v: boolean  ) =>  { 			if  (!v) { 				modal.resolveHide (); 			} 			!v && !modal.keepMounted  && modal.remove (); 		}, 	}; };
 
总结 nice-modal-react  设计上的核心是 NiceModalPlaceholder,通过它来灵活调用 Modal,非常的巧妙,值得学习。