背景
web端日常开发经常会遇到各种弹层需求,弹层内容千奇百怪。根据我们日常的组件化开发经验,我们会把弹层分为2个组件
- 弹层组件:包含
蒙版
,展示隐藏
逻辑 - 内容组件:本次需求实际组件
使用
import CitySelect from './components/CitySelect';
import modal from '@/components/modal';
...
openCitySelect() {
modal.popup({
content: CitySelect,
props: {
data,
onSave: () => {...}
}
});
}
...
说明
这种用法的好处是显而易见的
调用方
与CitySelect
都不需要维护一个visible
变量调用方
与CitySelect
模块分离更加清晰,调用方
只需要触发一次openCitySelect
,之后就和弹层再无关系,CitySelect
只关心自身逻辑不需要关心弹层的状态。
下面来看此api在vue
和react
上面的实现
Vue实现
vue下面实现此api
的方法较多,可以使用dom操作
, 动态组件
, render函数
来实现。我们以动态组件
为例:
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 (
)
}
}
// 方法
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( , div);
}
render(currentConfig);
return {
destroy: close
};
};
const modal = { popup } ;
export default modal;
注意
由于弹窗是我们手动new
出来的,并没有包含在入口的jsx引用中,所以类似router
,store
,locale
这些Provider
需要手动重新包装下,这样才能像页面一样使用相关功能
补充一个vue2的完整实现
import Vue from 'vue'
import { Vue as VueDecorator } from 'vue-property-decorator'
import Dialog from './modalDialog.vue'
import Popup from './popup.vue'
let store: Record
let router: Record
let i18n: Record
let apolloProvider: Record
interface CommonConfig {
title?: string
popType?: 'popup' | 'dialog'
width?: string
}
interface ModalConfig extends CommonConfig {
content?: Record & {
kxDialogConfig?: CommonConfig
}
props?: Record
context?: VueDecorator
}
/*
popType: 弹窗类型 custom_popup: 完全自定义弹层 | dialog: el-dialog | popup: 没有footer的el-dialog
*/
const modal = function (cfg: ModalConfig) {
const config = Object.assign({}, cfg)
const { content, context } = config
Reflect.deleteProperty(config, 'content')
Reflect.deleteProperty(config, 'context')
const popComp = [config.popType, content?.kxDialogConfig?.popType].includes('popup') ? Popup : Dialog
let Component = Object.assign({}, popComp, {
store,
router,
i18n,
apolloProvider,
}) as any
// 加入provide支持
if (context) {
Object.assign(Component, {
provide() {
return (context as any)._provided
},
})
}
Object.assign(Component.components, {
innerMain: content,
})
Component = Vue.extend(Component)
const { props, ...prosData } = config
const instance = new Component({
propsData: {
innerProps: props,
compName: 'innerMain',
...prosData,
...(content?.kxDialogConfig || {}),
},
})
document.body.appendChild(instance.$mount().$el)
return instance
}
modal.setProvider = function (data: any) {
if (data.store) {
store = data.store
}
if (data.router) {
router = data.router
}
if (data.i18n) {
i18n = data.i18n
}
if (data.apolloProvider) {
apolloProvider = data.apolloProvider
}
}
export default modal
注意:store 和上下文无关,可以单独赋值。provide和上下文下关需要每次调用modal时传递一下context