likes
comments
collection
share

NiceModal 源码分析, React 非常舒服的Modal

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

前言

在项目中会遇到某个页面,需要配备多个弹窗页,然而为了受控每个弹窗,那么就需要定义多个 visible/open ,然后在HTML结构中还要定义多个 <Modal> 标签,虽说可以封装组件啥的,但也还是贼特么难顶。。。

NiceModal

点击前往 NiceModal Github

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.
})

这写法有啥好处?

  1. 不需要再在每个页面中去挂载 Modal 组件

  2. 利用 Promise,多个弹窗关联的时候,会非常舒服

    // 当点击 ok 时,显示下一个弹窗
    await NiceModal.show(Modal1)
    await NiceModal.show(Modal2)
    await NiceModal.show(Modal3)
    
  3. 调用处显而易见

    // 举个栗子
    
    // 普通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>
  )
})

这里代码非常简单,createModalOne 包装了 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 去做,这里就用到了之前 markNiceModalIdContext

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 的主要实现,感兴趣的哥们可以移步源码。

下面说下大概流程操作。

  1. Provide 挂载,内部创建 dispatch
  2. create 创建 Modal 组件
  3. register(注册 id)
  4. 页面中使用 NiceModal.show(id)调用
  5. 使用 dispatch 修改该 id 的属性
  6. dispatch 通知 Provide 内部的组件渲染
转载自:https://juejin.cn/post/7170667418795114533
评论
请登录