一、前言

在日常开发中,Modal使用必不可少,但是如果一个页面Modal弹窗过多,Modal的使用又必须要在render()方法里声明,这无疑是一件很恐怖的事情,就像如下场景

render() {

if (this.state.isLoading) {

return (

);

} else {

return (

style={{flex: 1, backgroundColor: StyleConfig.color_background}}

contentContainerStyle={{paddingTop: 0, paddingBottom: 50 + Global.bottomOfSafeArea}}

automaticallyAdjustContentInsets={false}

keyboardShouldPersistTaps="handled"

showsHorizontalScrollIndicator={false}

removeClippedSubviews={true}

showsVerticalScrollIndicator={false}>

props={this.props}

isAdd={true}

relationSex={this.state.relationSex}

relation={this.state.relation}

imageUrl={this.state.imageUrl}

userInfo={this.state.userInfo}

isFamilyMemeber={RelationShipMap.isFamilyMember(this.state.relation)}

loginType={this.state.loginType}

uploadImgCallBack={(fileId, picUrl) => {

this.fileId = fileId;

this.setState({imageUrl: picUrl});

}}

selectRelationCallback={relation => {

this.isCheckMember = true;

if (relation != null) {

this.setState({relation: Number.parseInt(relation)});

this.setAddress(relation);

this.getFamilyMemberByRelation(relation);

}

}}

idCardChangeCallBack={idCardNo => {

this.setState({relationSex: Common.isMaleFromIdCard(idCardNo) ? 1 : 0});

let value = {};

value[FamilyMemberKey.IDCARD] = idCardNo;

this.props.form.setFieldsValue(value);

}}

/>

{/*是否加入安心管*/}

{RelationShipMap.isFamilyMember(this.state.relation) && (

style={{marginTop: 15}}

isJoinAnxin={this.isJoinAnxin}

promptText={'如将该成员加入安心管,可能引起家庭抗风险能力值发生变化'}

joinAnxinChange={checked => {

this.isJoinAnxin = checked;

}}

/>

)}

onPress={() => {

if (!RelationShipMap.isFamilyMember(this.state.relation)) {

this.isJoinAnxin = false;

}

if (this.checkParams()) {

if (this.isCheckMember) {

this.checkMember();

} else {

let forms = this.props.form.getFieldsValue();

this.updateMember(this.insurantId, forms);

}

}

}}>

保存

style={{

alignSelf: 'center',

marginTop: 15,

marginLeft: 20,

marginRight: 20,

marginBottom: 20,

color: '#666',

fontSize: 13,

}}>

以上信息仅用于新一站保险业务办理,我们会严格保密。

{/*操作是本人提示弹窗*/}

transparent={true}

visible={this.state.modalMeVisiable}

onRequestClose={() => {

this.setState({modalMeVisiable: false});

}}>

message={'身份号已被本人使用'}

isHideCancale={true}

okEvent={() => {

this.setState({modalMeVisiable: false});

}}

/>

{/*覆盖提示弹窗*/}

transparent={true}

visible={this.state.modalCoverVisiable}

onRequestClose={() => {

this.setState({modalCoverVisiable: false});

}}>

message={'该身份证号已存在于家庭成员中,若你选择更新,我们将更新该成员资料,选择按钮为放弃和更新。'}

isHideCancale={false}

cancelEvent={() => {

UMengBridge.log(AddFamilyMember.AddFamilyMemberCancle);

this.setState({modalCoverVisiable: false});

this.insurantId = '';

}}

okEvent={() => {

UMengBridge.log(AddFamilyMember.AddFamilyMemberUpdate);

this.setState({modalCoverVisiable: false});

this.updateMember(this.insurantId, this.forms);

}}

/>

{/*错误弹窗提示*/}

transparent={true}

visible={this.state.modalErrorVisiable}

onRequestClose={() => {

this.setState({modalErrorVisiable: false});

}}>

message={this.state.errorMsg}

isHideCancale={true}

okEvent={() => {

this.setState({modalErrorVisiable: false});

}}

/>

{/*选择已有联系人*/}

transparent={true}

visible={this.state.modalSelectMember}

onRequestClose={() => {

this.setState({modalSelectMember: false});

}}>

{this.getSelectFamilyMember()}

);

}

}

可以看到上面的大部分代码篇幅,全是Modal的声明,代码可读性可谓极差!

二、实现原理

那么我们有没有一种方式可以去优化这种写法呢?答案当然是肯定的,因为有现成的方案,例如的AntDesign的Modal,他的内部有直接调用静态方法即可实现Modal弹窗的效果,我们来看下他是怎么实现的。

首先定位关键代码:Modal.alert()的内部实现

import React from 'react';

import Portal from '../portal';

import AlertContainer from './AlertContainer';

import { Action, CallbackOnBackHandler } from './PropsType';

export default function a(

title: React.ReactNode,

content: React.ReactNode,

actions: Action[] = [{ text: '确定' }],

onBackHandler?: CallbackOnBackHandler,

) {

const key = Portal.add(

title={title}

content={content}

actions={actions}

onAnimationEnd={(visible: boolean) => {

if (!visible) {

Portal.remove(key);

}

}}

onBackHandler={onBackHandler}

/>,

);

return key;

}

可以看到这边有一个关键的类Portal,继续看Portal.add()是如何实现的:

class PortalGuard {

private nextKey = 10000;

add = (e: React.ReactNode) => {

const key = this.nextKey++;

TopViewEventEmitter.emit(addType, e, key);

return key;

};

remove = (key: number) => TopViewEventEmitter.emit(removeType, key);

}

这里可以看到这边发送了一个全局的通知,我们继续定位处理通知的方法:

export default class PortalHost extends React.Component {

static displayName = 'Portal.Host';

_nextKey = 0;

_queue: Operation[] = [];

_manager?: PortalManager;

componentDidMount() {

const manager = this._manager;

const queue = this._queue;

TopViewEventEmitter.addListener(addType, this._mount);

TopViewEventEmitter.addListener(removeType, this._unmount);

while (queue.length && manager) {

const action = queue.pop();

if (!action) {

continue;

}

// tslint:disable-next-line:switch-default

switch (action.type) {

case 'mount':

manager.mount(action.key, action.children);

break;

case 'update':

manager.update(action.key, action.children);

break;

case 'unmount':

manager.unmount(action.key);

break;

}

}

}

componentWillUnmount() {

TopViewEventEmitter.removeListener(addType, this._mount);

TopViewEventEmitter.removeListener(removeType, this._unmount);

}

_setManager = (manager?: any) => {

this._manager = manager;

};

_mount = (children: React.ReactNode, _key?: number) => {

const key = _key || this._nextKey++;

if (this._manager) {

this._manager.mount(key, children);

} else {

this._queue.push({ type: 'mount', key, children });

}

return key;

};

_update = (key: number, children: React.ReactNode) => {

if (this._manager) {

this._manager.update(key, children);

} else {

const op: Operation = { type: 'mount', key, children };

const index = this._queue.findIndex(

o => o.type === 'mount' || (o.type === 'update' && o.key === key),

);

if (index > -1) {

this._queue[index] = op;

} else {

this._queue.push(op);

}

}

};

_unmount = (key: number) => {

if (this._manager) {

this._manager.unmount(key);

} else {

this._queue.push({ type: 'unmount', key });

}

};

render() {

return (

value={{

mount: this._mount,

update: this._update,

unmount: this._unmount,

}}

>

{/* Need collapsable=false here to clip the elevations, otherwise they appear above Portal components */}

{this.props.children}

);

}

}

我们这边可以看到add()最终走到了this._manager.mount(key, children);这个方法里,我们进而分析PortalManager里的方法、

import React from 'react';

import { View, StyleSheet } from 'react-native';

export type State = {

portals: Array

key: number;

children: React.ReactNode;

}>;

};

export type PortalManagerState = {

portals: any[];

};

/**

* Portal host is the component which actually renders all Portals.

*/

export default class PortalManager extends React.PureComponent<

{},

PortalManagerState

> {

state: State = {

portals: [],

};

mount = (key: number, children: React.ReactNode) => {

this.setState(state => ({

portals: [...state.portals, { key, children }],

}));

};

update = (key: number, children: React.ReactNode) =>

this.setState(state => ({

portals: state.portals.map(item => {

if (item.key === key) {

return { ...item, children };

}

return item;

}),

}));

unmount = (key: number) =>

this.setState(state => ({

portals: state.portals.filter(item => item.key !== key),

}));

render() {

return this.state.portals.map(({ key, children }, i) => (

key={key}

collapsable={

false /* Need collapsable=false here to clip the elevations, otherwise they appear above sibling components */

}

pointerEvents="box-none"

style={[StyleSheet.absoluteFill, { zIndex: 1000 + i }]}

>

{children}

));

}

}

到了这边大家大致应该都明白了,我们可以看到这些其实都是在维护portals视图数组,对应的更新这个portals视图数组,来实现Modal的显隐。通过zIndex控制层级的优先级,使用绝对布局StyleSheet.absoluteFill,来浮在最上层。

那么这边只是找到了绘制的地方,我们在哪边去绘制这个PortalManager的呢?通过IDE一层层的往上找,最后定位到这边:

import * as React from 'react';

import LocaleProvider, { Locale } from '../locale-provider';

import Portal from '../portal';

import { Theme, ThemeProvider } from '../style';

export interface ProviderProps {

locale?: Partial;

theme?: Partial;

}

export default class Provider extends React.Component {

render() {

return (

{this.props.children}

);

}

}

答案呼之欲出,这个就是AntDesign最顶层的Provider视图,进而思考,我们要在项目里实现AntDesign那种直接调用静态方法的弹出视图的方法,我们必须要使用Provider控件去包裹我们的App,就像这样:

render() {

const Router = this.Router;

return (

{Platform.OS === 'ios' ? : null}

screenProps={this.props}

{...getPersistenceFunctions(pageName)}

renderLoadingExperimental={() => }

onNavigationStateChange={this.onNavigationStateChange.bind(this)}

/>

);

}

三、优化实现

上个章节分析到,如果要实现AntDesign那种直接调用静态方法的弹出视图的方法,我们只需要使用Provider控件去包裹我们App的最顶层的视图,然后在任意地方调用Portal.add()方法即可实现这种效果。

为了更简单的实现这个弹窗,我们定义了一个工具类:

import {Portal} from '@ant-design/react-native';

import BaseDialog from '../components/views/BaseDialog';

import React from 'react';

/**

* 注释: 通用弹窗构建工具

* 时间: 2020/9/30 0030 15:37

* @author 郭翰林

*/

export default class DialogUtil {

public containView: JSX.Element;

public okText: string;

public okEvent: Function;

public cancelText?: string;

public cancelEvent?: Function;

/**

* 注释: 视图Key

* 时间: 2020/9/30 0030 15:35

* @author 郭翰林

*/

private key: number;

/**

* 注释: 显示

* 时间: 2020/9/30 0030 15:36

* @author 郭翰林

*/

public show() {

this.key = Portal.add(

cancelEvent={this.cancelEvent}

cancelText={this.cancelText}

containView={this.containView}

okEvent={this.okEvent}

okText={this.okText}

/>,

);

}

/**

* 注释: 隐藏

* 时间: 2020/9/30 0030 15:36

* @author 郭翰林

*/

public hidden() {

Portal.remove(this.key);

}

}

这边的BaseDialog是我们业务中通用的一个Modal弹窗控件,这边你可以传入任何你想自定义的视图。实现如下:

/**

* 注释: 通用基础弹窗

* 时间: 2020/5/19 0019 13:46

* @author 郭翰林

*/

export default function BaseDialog(props: Props) {

const [modalVisible, setModalVisible] = useState(true);

return (

animationType={'none'}

onRequestClose={() => {

setModalVisible(false);

}}

transparent={true}

visible={modalVisible}>

style={{

backgroundColor: 'rgba(0,0,0,0.5)',

alignItems: 'center',

justifyContent: 'center',

flex: 1,

}}>

{/*渐变*/}

start={{x: 0.0, y: 0.5}}

end={{x: 1.0, y: 0.5}}

colors={['#fc704e', '#ff9547']}

style={{

flexDirection: 'row',

borderTopLeftRadius: 8,

borderTopRightRadius: 8,

overflow: 'hidden',

}}>

{/*内容区域*/}

{props.containView}

{/*确认按钮*/}

style={styles.okButtonStyle}

onPress={() => {

setModalVisible(false);

props.okEvent && props.okEvent();

}}>

style={styles.okLinearGradientStyle}

start={{x: 0.0, y: 0.5}}

end={{x: 1.0, y: 0.5}}

colors={['#fc704e', '#ff9547']}>

{props.okText}

{/*取消按钮*/}

onPress={() => {

setModalVisible(false);

props.cancelEvent && props.cancelEvent();

}}>

{props.cancelText ? props.cancelText : '取消'}

);

}

最后我们要在代码中显示弹窗,就不必像第一章节那样,显式的去声明Modal弹窗,只需像如下方式调用即可:

const dialog = new DialogUtil();

dialog.okText = '去开启';

dialog.okEvent = () => {

CommonBridge.gotoNotificationSetting();

dialog.hidden();

};

dialog.containView = renderNotificationTip();

dialog.cancelEvent = () => {

RouterPageBridge.gotoRouterSkipSystem(RouterUri.MessageCenterPage);

dialog.hidden();

};

dialog.show();

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐