在 React 中命令式加载组件

1,950 阅读2分钟

前端开发中,为了避免新开窗口,经常需要弹出模态框展示详情或者让用户操作。通常我们这样实现:

import Modal from './Modal'

class Demo extends React.Component {
  constructor (props) {
    super(props)
    this.state = { showModal: false }
  }

  dispalyModal = () => this.setState({ showModal: true })

  removeModal = () => this.setState({ showModal: false })

  render () {
    return (
      <div>
        <button onClick={ this.dispalyModal }>showModal</button>
        { this.state.showModal ? <Modal onClose={ this.removeModal }/> : null }
      </div>
    );
  }
}

如果一个页面的模态框框较多,就会出现:

import Modal1 from './Modal1'
import Modal1 from './Modal2'
import Modal1 from './Modal3'

class Demo extends React.Component {
  constructor (props) {
    super(props)
    this.state = {
      showModal1: false,
      showModal2: false,
      showModal3: false,
    }
  }

  dispalyModal1 = () => this.setState({ showModal1: true })
  dispalyModal2 = () => this.setState({ showModal2: true })
  dispalyModal3 = () => this.setState({ showModal3: true })

  removeModal1 = () => this.setState({ showModal1: false })
  removeModal2 = () => this.setState({ showModal2: false })
  removeModal3 = () => this.setState({ showModal3: false })

  render () {
    const { showModal1, showModal2, showModal3 } = this.state;
    return (
      <div>
        <button onClick={ this.dispalyModal1 }>showModal1</button>
        <button onClick={ this.dispalyModal2 }>showModal2</button>
        <button onClick={ this.dispalyModal3 }>showModal3</button>
        { showModal1 ? <Modal1 onClose={ this.removeModal1 }/> : null }
        { showModal2 ? <Modal2 onClose={ this.removeModal2 }/> : null }
        { showModal3 ? <Modal3 onClose={ this.removeModal3 }/> : null }
      </div>
    );
  }
}

这样即使我们按功能拆分了组件,但组合时仍然显得十分复杂。典型的场景是文件列表,每个文件有重命名、复制、移动、删除、收藏等功能,且几乎每个功能都以模态框的形式呈现。

上面其实是“声明式”加载组件的,那么能不能用“命令式”,实现如下形式的调用呢?

import copyFile from './copyFile'
import moveFile from './moveFile'
import deleteFile from './deleteFile'

class Demo extends React.Component {
  render () {
    // files-i 是文件节点
    const files = [file-0, file-1, file-2]
    return (
      <div>
        <button onClick={ () => copyFile(files) }>copy</button>
        <button onClick={ () => moveFile(files) }>move</button>
        <button onClick={ () => deleteFile(files) }>delete</button>
      </div>
    );
  }
}

为了实现上面的效果,我们需要一个能动态加载组件的函数。参考 ant-design 的 notification,经过探索,我实现了如下 mountAnywhere 函数:

import React from 'react'
import ReactDOM from 'react-dom'

function mountAnywhere(Comp, root) {
  const div = document.createElement('div')
  ;(root || document.body).appendChild(div)

  // 向组件注入 onClose 方法,以便组件能调用关闭
  const Clone = React.cloneElement(Comp, {
    onClose: () => {
      ReactDOM.unmountComponentAtNode(div)
      div.parentNode.removeChild(div)
    }
  });

  ReactDOM.render(Clone, div)
}

export default mountAnywhere;

于是实现 copyFile 如下:

import mountAnywhere from './mountAnywhere'

class CopyModal extends React.Component {
  onClose = () => {
    // 这是 mountAnywhere 自动注入的方法,以便组件能触发关闭
    this.props.onClose()
  }
  
  doCopy = () => {
    const { files } = this.props;
    // XXX: copy files...
  }
  
  render () {
    const { files } = this.props;
    return (
      <div>
        <button onClick={ this.onClose }>Cancel</button>
        <button onClick={ this.doCopy }>OK</button>
      </div>
    )
  }
}

function copyFile (files) {
  mountAnywhere(<CopyModal files={ files }/>)
}

export default copyFile

这里主要用到了两个 ReactDOM API,ReactDOM.renderReactDOM.unmountComponentAtNode,由此实现了在 React 中命令式载入组件。