Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

从开发体验探讨React模态窗口实现 #30

Open
varHarrie opened this issue Apr 2, 2019 · 0 comments
Open

从开发体验探讨React模态窗口实现 #30

varHarrie opened this issue Apr 2, 2019 · 0 comments
Labels
Milestone

Comments

@varHarrie
Copy link
Owner

varHarrie commented Apr 2, 2019

业务场景

最近的项目出现大量模态窗口的应用场景,大多数业务流程也非常相似:

  • 用户点击某个按钮,弹出模态窗口,并传入一些数据
  • 这些数据可能直接用于窗口内部展示,也可能用于异步请求获取详情
  • 窗口内部有一些表单,详情的数据作为表单的初始值
  • 用户点击取消,直接关闭模态窗口
  • 用户点击确定,对表单数据进行校验或处理,发送请求保存数据,最后关闭模态窗口
  • 为了复用,这个窗口可以同时用于新增和编辑
  • 同一个页面类似功能的窗口可能同时存在多个

举个实际的例子,在一个用户列表的页面,用户列表上方有个新建用户按钮,用户列表的每一项后面都有个编辑用户按钮

  • 点击新建用户按钮,弹出用户模态窗口,里面表单初始值都是空的
  • 点击编辑用户按钮,弹出用户模态窗口,里面表单初始值从后端获取

实现和改进

按照以往做法,结合一些组件库,把每个业务场景的模态窗口封装成一个组件,例如用户窗口UserModal,然后在页面组件中引用和渲染,传入visibleonConfirm等等。

class UserListView extends React.Component {
  state = {
    modalVisible: boolean
    modalUserId: ''
  }

  // 打开新建窗口
  onCreateModalOpen = () => {
  	this.setState({modalVisible: true, modalUserId: ''})
  }
  
  // 打开编辑窗口
  onEditModal = (id) => {
    this.setState({modalVisible: true, modalUserId: id})
  }
  
  // 窗口确定
  onModalConfirm = (data) => {
    // 根据data有没有id,来判断调用新建还是保存的接口
    // ...
    // 根据接口返回信息,若成功,关闭窗口,若失败,弹出提示,保留窗口
    // ...
  }

  // 窗口取消
  onModalCancel = () => {
    this.setState({ modalVisible: false })
  }
  
  render () {
    const { modalVisible, modalUserId } = this.state
    const users = [/** 从后端获取 */]
    
		<div>
      <header>
        <button onClick={this.onCreateModalOpen}>新建用户</button>
      </header>
      <ul>
        {users.map((user) => (
          <li key={user.id}>
        		<span>{user.name}</span>
            <button onClick={() => this.onEditModal(user.id)}>编辑用户</button>
          </li>
        ))}
      </ul>
      <UserModal
        visible={modalVisible}
        id={modalUserId}
        onConfirm={this.onModalConfirm}
        onCancel={this.onModalCancel}
       />
    </div>
  }
}

页面组件UserListView需要维护模态窗口的显示状态visible,还要维护各种事件onConfirmonCancel。这还是仅仅只考虑模态窗口的逻辑的情况,而且该模态窗口依赖的modalUserId也放在的页面组件中,某种程度上说,这不是父组件需要的状态。

假如这时候需求变动原因,需要在编辑用户按钮后面再加一个编辑用户角色的按钮,点击之后弹出用户角色模态窗口,用于展示和勾选用户角色。这时,我们又要维护一份新的模态窗口状态、模态窗口事件,包括对应的命名也都要重新考虑。

经过一番思考,如果我们把模态窗口的状态、事件放进这个模块窗口组件中,会怎么样?

将窗口相关的事件、状态,都交给窗口组件自己去维护,只接受onConfirm事件,再暴露showhide之类的方法,供父组件调用。

class UserListView extends React.Component {
  refModal = React.createRef()

  // 打开新建或编辑窗口
  onModalOpen = (id) => {
  	this.refModal.current.show(id)
  }
  
  // 窗口确定
  onModalConfirm = (data) => {
    // 返回true,阻止窗口关闭
  }
  
  render () {
    const users = [/** 从后端获取 */]
    
		<div>
      <header>
        <button onClick={() => this.onModalOpen()}>新建用户</button>
      </header>
      <ul>
        {users.map((user) => (
          <li key={user.id}>
        		<span>{user.name}</span>
            <button onClick={() => this.onModalOpen(user.id)}>编辑用户</button>
          </li>
        ))}
      </ul>
      <UserModal
        ref={this.refModal}
        onConfirm={this.onModalConfirm}
       />
    </div>
  }
}

这个时候,页面组件UserListView对于UserModal就只需要关注打开窗口并传参(输入),窗口确定事件(输出)。

如果还有其他窗口,也只需要多维护一份refonModalOpenonModalConfirm

进一步改进

后来,在使用antd的modal函数调用时,联想到这个问题,发现后可以有进一步改进的实现方案。

从开发体验上讲,以上的实现方案,还有一些需要改进的地方:

  • 对于onModalOpenonModalConfirm的维护依然觉得繁琐。每个窗口都需要维护两个函数,从逻辑上讲,它们应该是“连续”的,先有onModalOpen触发窗口展示,用户操作完后触发onModalConfirm,获得反馈信息;但是代码上却分隔开来了,不同窗口间的事件还可以混合在一起。
  • ref创建之后,还需要通过在组件上传入ref={this.refModal},已将它们绑定在一起。在结合TypeScript使用时,this.refModal.current有可能是null的(在mounted之前使用会是null),需要在使用前加判断或者断言。

结合函数调用的方法,最终使用方法变成了:

class UserListView extends React.Component {
  modal = createModal(UserModal)

  // 打开新建或编辑窗口
  onModalOpen = (id) => {
    this.modal.show(id, (data) => {
      // 窗口确定回调
      // 返回true,阻止窗口关闭
    })
  }
  
  render () {
    const users = [/** 从后端获取 */]
    
		<div>
      <header>
        <button onClick={() => this.onModalOpen()}>新建用户</button>
      </header>
      <ul>
        {users.map((user) => (
          <li key={user.id}>
        		<span>{user.name}</span>
            <button onClick={() => this.onModalOpen(user.id)}>编辑用户</button>
          </li>
        ))}
      </ul>
      <this.modal.Component /> {/** 渲染窗口组件 */}
    </div>
  }
}

结语

React的魅力就在于它给予了足够广阔的实现空间,你总能发现更优的解决方案,甚至是颠覆性的。

以上功能createModal具体实现请查看gist,此外,通过结合React Hooks,还有一个新的方案@react-hero/modal,可以将<this.modal.Component />都省略了,具体实现就不展开了。

@varHarrie varHarrie added the React label Apr 2, 2019
@varHarrie varHarrie added this to the Posts milestone Aug 5, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant