脱离文档流的自定义弹层

46次阅读

共计 2232 个字符,预计需要花费 6 分钟才能阅读完成。

背景

web 端日常开发经常会遇到各种弹层需求,弹层内容千奇百怪。根据我们日常的组件化开发经验,我们会把弹层分为 2 个组件

  • 弹层组件:包含 蒙版 , 展示隐藏 逻辑
  • 内容组件:本次需求实际组件

使用

import CitySelect from './components/CitySelect';
import modal from '@/components/modal';

...
openCitySelect() {
    modal.popup({
        content: CitySelect,
        props: {
            data,
            onSave: () => {...}
        }
    });
}
...

说明

这种用法的好处是显而易见的

  1. 调用方 CitySelect都不需要维护一个 visible 变量
  2. 调用方 CitySelect模块分离更加清晰,调用方 只需要触发一次 openCitySelect,之后就和弹层再无关系,CitySelect 只关心自身逻辑不需要关心弹层的状态。

下面来看此 api 在 vuereact上面的实现

Vue 实现

vue 下面实现此 api 的方法较多,可以使用 dom 操作, 动态组件 , render 函数 来实现。我们以 动态组件 为例:

<template>
  <div class="container" v-show="visible">
    <div class="mask"></div>
    <component :is="currentView" v-if="currentView" ref="comp" :v-bind="propsData"></component>
  </div>
</template>

<script>
const Popup = {
  props: {title: { type: String, default: '提示'},
    propsData: {type: Object},
    currentView: {type: Object},
  },
  data () {
    return {visible: false}
  },
  mounted () {
    // 创建后直接打开
    this.$nextTick(this.open)
  },
  methods: {open () {this.visible = true;},
    close () {
      this.visible = false;
      // 移除元素
      if (this.$el.parentNode) {this.$el.parentNode.removeChild(this.$el)
      }
    }
  }
}

export default {popup({ content, title, props}) {let Comp = Vue.extend(Popup);
    let instance = new Comp({
      propsData: {
        propsData: props,
        currentView: content,
        title
      }
    }).$mount();
    document.appendChild(instance.$el);
  } 
};
</script>

React 实现

React 实现需要配合 react-dom,rn 的话可以再 app 入口加一个常驻弹窗容器来实现,原理大致相同。
这里还是以 web 端为例,代码直接从 ant 抄过来的。

// 弹窗组件
class Popup extends Component {componentDidMount() { }

  render() {
    const {
      close,
      props = {},
      visible = false,
    } = this.props;

    if (!visible) {return null;}

    const DlgContent = content;

    return (
      <Router>
        <Provider {...store}>
          <div className={styles.container}>
            <div className="mask"></div>
            <DlgContent close={close} {...props} />
          </div>
        </Provider>
      </Router>
    )
  }
}

// 方法
const popup = function(config) {const div = document.createElement('div');
  document.body.appendChild(div);
  let currentConfig = {...config, visible: true, close};

  function close(...args) {
    currentConfig = {
      ...currentConfig,
      visible: false
    };

    render(currentConfig);
    setTimeout(destroy, 100);
  }


  function destroy() {const unmountResult = ReactDOM.unmountComponentAtNode(div);
    if (unmountResult && div.parentNode) {div.parentNode.removeChild(div);
    }
  }

  function render(config) {ReactDOM.render(<Popup {...config} />, div);
  }

  render(currentConfig);

  return {destroy: close};
};

const modal = {popup} ;
export default modal;

注意

由于弹窗是我们手动 new 出来的,并没有包含在入口的 jsx 引用中,所以类似 routerstorelocale 这些 Provider 需要手动重新包装下,这样才能像页面一样使用相关功能

正文完
 0