使用 async/await 获取弹出框交互结果

5,092 阅读5分钟

利用async/await 关键字,我们可以优雅的处理异步逻辑。

比如调用网络接口可以直接await fetch(……)

事实上,在处理页面交互逻辑时使用async/await,也可以起到很好的解耦作用,写出更清晰的代码。

比如获取Modal弹框的交互结果:

直接看效果: DEMO on github

后台系统在交互设计上经常使用模态弹框。很多UI框架都提供了方便易用的Modal组件,比如Ant design: Antd Modal

先来看一下antd官方的demo是怎么做的:

import { Modal, Button } from 'antd';

class App extends React.Component {
  state = { visible: false } // 还是需要维护一个visible state用来控制Modal的显示状态

  showModal = () => {
    this.setState({
      visible: true,
    });
  }

  handleOk = (e) => {
    this.setState({
      visible: false,
    });
  }

  handleCancel = (e) => {
    this.setState({
      visible: false,
    });
  }

  render() {
    return (
      <div>
        <Button type="primary" onClick={this.showModal}>
          Open Modal
        </Button>
        <Modal
          title="Basic Modal"
          visible={this.state.visible}
          onOk={this.handleOk}
          onCancel={this.handleCancel}
        >
          <p>Some contents...</p>
          <p>Some contents...</p>
          <p>Some contents...</p>
        </Modal>
      </div>
    );
  }
}

ReactDOM.render(<App />, mountNode);

以上代码可以看出:受制于React通常的写法,在频繁使用Modal的场景下Antd的封装使用起来还是有些繁琐

  • 还是需要维护一个visible state,用来控制Modal是否显示
  • 如果外层组件只需要获取Modal内操作的结果,需要用props传递一个onChange方法进来,或者借助笨重的全局状态管理机制
  • 大量使用Modal时,会产生海量的重复代码
  • ……

当然,作为中后台系统的优秀解决方案,Antd的小伙伴帮我们封装了Modal.methods可以快速的创建一些简易的弹窗:

Modal.confirm({
  title: 'Confirm',
  content: 'Bla bla ...',
  okText: '确认',
  cancelText: '取消',
});

这是一个不错的思路,但是Antd官方提供的methods功能非常有限。

我们来看一看能否在Antd Modal的基础上做一些改进:

希望能够实现result = await Modal.open()这样的调用方式。

一种思路是在实现一个Modal class,上面有static open这样一个方法,当调用open时,向页面插入一个modal,类似上面Modal.confirm的做法。

另一种办法,可以先在页面实例化一个modal,需要open的时候利用ref attribute获取到已经实例化的modal,并调用它的open 方法。 如下:

import React from 'react';
import { Modal, Input } from 'antd';

export default class AsyncModalDemo extends React.Component {
  state = {
    visible: false,
    text: ''
  }

  open = () => {
    this.setState({
      visible: true,
    });
    return new Promise((resolve, reject) => {
      this.resolve = resolve;
      this.reject = reject;
    });
  }

  close = () => {
    this.setState({
      visible: false,
    });
    if (this.reject && typeof this.reject === 'function') {
      this.reject('cancel');
    }
  }

  handleOk = () => {
    if (typeof this.resolve === 'function') {
      this.resolve(this.state.text);
    }
    delete this.reject;
    this.close();
  }

  render() {
    return <Modal
      visible={this.state.visible}
      onCancel={this.close}
      onOk={this.handleOk}
    >
      <div>
        hello
      </div>
      <div>
        you can check result in devtool
      </div>
      <Input
        onChange={e => {
          const v = (e && e.target && e.target.value) || '';
          this.setState({
            text: v,
          })
        }}
        value={this.state.text}
     ></Input>
    </Modal>
  }
}

调用方式如下:

import React from 'react';
import './App.css';
import { Button } from 'antd';
import AsyncModal from './components/async_modal';

class App extends React.Component {

  handleOpenModal = async () => {
    if (this.refs.asyncModal) {
      const result = await this.refs.asyncModal.open();
      console.log(result)
    }
  }

  render() {
    return (
      <div className="App">
        <Button
          onClick={this.handleOpenModal}
        >
          Open Modal
        </Button>
        <AsyncModal ref="asyncModal"></AsyncModal>
      </div>
    );
  }
}

export default App;

这样,我们就可以使用await 获取到Modal内到交互结果了,并且做到了较好的解耦。

但是每次都要在Modal类里面写一堆open、close、handleOk这样的method,感觉很烦。那么有没有比较好的方式,实现代码复用呢?

首先尝试一下继承:

import React from 'react';
import { Modal, Input } from 'antd';

class AsyncModalBase extends React.Component {
  state = {
    visible: false,
  }

  open = () => {
    this.setState({
      visible: true,
    });
    return new Promise((resolve, reject) => {
      this.resolve = resolve;
      this.reject = reject;
    });
  }

  close = () => {
    this.setState({
      visible: false,
    });
    if (this.reject && typeof this.reject === 'function') {
      this.reject('cancel');
    }
  }
}

export default class AsyncModalDemo extends AsyncModalBase {

  state = {
    text: ''
  }

  handleOk = () => {
    if (typeof this.resolve === 'function') {
      this.resolve(this.state.text);
    }
    delete this.reject;
    this.close();
  }

  render() {
    return <Modal
      visible={this.state.visible}
      onCancel={this.close}
      onOk={this.handleOk}
    >
      <div>
        hello
      </div>
      <div>
        you can check result in devtool
      </div>
      <Input
        onChange={e => {
          const v = (e && e.target && e.target.value) || '';
          this.setState({
            text: v,
          })
        }}
        value={this.state.text}
     ></Input>
    </Modal>
  }
}

open 和 close method 在 AsyncModalBase内实现,在AsyncModalDemo内只需要实现handleOk就可以了。这样就复用了open和close两个method。

然后尝试一下HOC:

import React from 'react';
import { Modal, Input, Button } from 'antd';

const ModalHoc = (modalProps = {}) => DefaultComponent => class extends React.Component {
  state = {
    visible: false,
  }

  open = () => {
    this.setState({
      visible: true,
    });
    return new Promise((resolve, reject) => {
      this.resolve = resolve;
      this.reject = reject;
    });
  }

  close = () => {
    this.setState({
      visible: false,
    });
    if (this.reject && typeof this.reject === 'function') {
      this.reject('cancel');
    }
  }

  hanldeResolve = (...params) => {
    if (typeof this.resolve === 'function') {
      this.resolve(...params);
    }
    delete this.reject;
    this.close();
  }

  render() {
    return <Modal
      {...modalProps}
      visible={this.state.visible}
      footer={null} // 这里做了一点改动,footer在子组件内实现
      closable
      onCancel={this.close}
    >
      <DefaultComponent
        {...this.props}
        hanldeResolve={this.hanldeResolve}
        hanldeClose={this.close}
      ></DefaultComponent>
    </Modal>
  }
}

class Demo extends React.Component {
  state = {
    text: '',
  }

  handleOk = () => {
    this.props.hanldeResolve(this.state.text)
  }

  render() {
    return <div>
      <div>
        hello
        </div>
      <div>
        you can check result in devtool
        </div>
      <Input
        onChange={e => {
          const v = (e && e.target && e.target.value) || '';
          this.setState({
            text: v,
          })
        }}
        value={this.state.text}
      ></Input>
      <div style={{
        marginTop: 10,
        textAlign: 'right'
      }}>
        <Button style={{marginRight: 5}} onClick={this.props.hanldeClose}>取消</Button>
        <Button type="primary" onClick={this.handleOk}>确定</Button>
      </div>
    </div>
  }
}

export default ModalHoc()(Demo);

很容易封装出了一个ModalHOC,同时做到了比较好的代码复用和解耦。这样,我们只需要实现modal内的业务逻辑,如表单、选择器……。外层的调用方式还是保持一致。

对于使用Vue的同学,也可以封装一个AsyncModal。下面以element-ui/el-dialog为基础做一些改进:

modal_mixin:

export default {
  data() {
    return {
      visible: false,
    }
  },
  watch: {
    visible(val) {
      if (!val) {
        if (typeof this.handleClear === 'function') {
          this.handleClear();
        }
        if (this.reject) {
          this.reject('放弃操作');
        }
      }
    }
  },
  methods: {
    open(...params) {
      this.visible = true;
      if (typeof this.handleInit === 'function') {
        this.handleInit(...params);
      }
      return new Promise((resolve, reject) => {
        this.resolve = resolve;
        this.reject = reject;
      });
    },
    handleResolve(...params) {
      delete this.reject;
      this.visible = false;
      if (this.resolve) {
        this.resolve(...params);
      }
    },
    close() {
      this.visible = false;
    }
  }
}

async modal:

<template>
  <el-dialog :visible.sync="visible">
    <div>
      you can check result in devtool
    </div>
    <el-input v-model="text"></el-input>
     <div style="text-align: right; margin-top: 15px">
      <el-button size="small" @click="close">取消</el-button>
      <el-button size="small" type="primary" @click="handleOk">确认</el-button>
    </div>
  </el-dialog>
</template>

<script>
import modal_mixin from './modal_mixin';

export default {
  name: 'AayncModal',
  mixins: [modal_mixin],
  data() {
    return {
      text: ''
    }
  },
  methods: {
    handleOk() {
      this.handleResolve(this.text);
    }
  }
}
</script>

调用:

<template>
  <div id="app">
    <div>
      <el-button @click="openModal">open modal</el-button>
    </div>
    <HelloWorld ref="asyncModal"/>
  </div>
</template>

<script>
import HelloWorld from './components/AsyncModal.vue'

export default {
  name: 'app',
  components: {
    HelloWorld
  },
  methods: {
    async openModal() {
      if (this.$refs.asyncModal) {
        const result = await this.$refs.asyncModal.open();
        console.log(result)
      }
    }
  }
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

使用mixin做代码复用。看起来似乎比react版本更简单一些。

更多细节请见:DEMO on github