基于AntDesign改造优化的Modal弹窗

2,757 阅读4分钟

一、前言

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

   render() {
    if (this.state.isLoading) {
      return (
        <View style={{flex: 1}}>
          <LoadingView />
        </View>
      );
    } else {
      return (
        <SafeAreaView style={styles.main} forceInset={{top: 'never', bottom: 'always'}}>
          <ScrollView
            style={{flex: 1, backgroundColor: StyleConfig.color_background}}
            contentContainerStyle={{paddingTop: 0, paddingBottom: 50 + Global.bottomOfSafeArea}}
            automaticallyAdjustContentInsets={false}
            keyboardShouldPersistTaps="handled"
            showsHorizontalScrollIndicator={false}
            removeClippedSubviews={true}
            showsVerticalScrollIndicator={false}>
            <View>
              <PersonalDataView
                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) && (
                <JoinAnxinView
                  style={{marginTop: 15}}
                  isJoinAnxin={this.isJoinAnxin}
                  promptText={'如将该成员加入安心管可能引起家庭抗风险能力值发生变化'}
                  joinAnxinChange={checked => {
                    this.isJoinAnxin = checked;
                  }}
                />
              )}
              <View style={{height: 44, alignItems: 'center', marginTop: 30}}>
                <XLargeButton
                  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);
                      }
                    }
                  }}>
                  保存
                </XLargeButton>
              </View>
            </View>
            <Text
              style={{
                alignSelf: 'center',
                marginTop: 15,
                marginLeft: 20,
                marginRight: 20,
                marginBottom: 20,
                color: '#666',
                fontSize: 13,
              }}>
              以上信息仅用于新一站保险业务办理,我们会严格保密。
            </Text>
          </ScrollView>
          {/*操作是本人提示弹窗*/}
          <Modal
            transparent={true}
            visible={this.state.modalMeVisiable}
            onRequestClose={() => {
              this.setState({modalMeVisiable: false});
            }}>
            <View style={styles.modalStyle}>
              <AnXinDialog
                message={'身份号已被本人使用'}
                isHideCancale={true}
                okEvent={() => {
                  this.setState({modalMeVisiable: false});
                }}
              />
            </View>
          </Modal>
          {/*覆盖提示弹窗*/}
          <Modal
            transparent={true}
            visible={this.state.modalCoverVisiable}
            onRequestClose={() => {
              this.setState({modalCoverVisiable: false});
            }}>
            <View style={styles.modalStyle}>
              <AnXinDialog
                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);
                }}
              />
            </View>
          </Modal>
          {/*错误弹窗提示*/}
          <Modal
            transparent={true}
            visible={this.state.modalErrorVisiable}
            onRequestClose={() => {
              this.setState({modalErrorVisiable: false});
            }}>
            <View style={styles.modalStyle}>
              <AnXinDialog
                message={this.state.errorMsg}
                isHideCancale={true}
                okEvent={() => {
                  this.setState({modalErrorVisiable: false});
                }}
              />
            </View>
          </Modal>
          {/*选择已有联系人*/}
          <Modal
            transparent={true}
            visible={this.state.modalSelectMember}
            onRequestClose={() => {
              this.setState({modalSelectMember: false});
            }}>
            {this.getSelectFamilyMember()}
          </Modal>
          <XWaitingHUD ref="hud" isVisible={this.state.showHUD} />
        </SafeAreaView>
      );
    }
  }

可以看到上面的大部分代码篇幅,全是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(
    <AlertContainer
      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<PortalHostProps> {
  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 (
      <PortalContext.Provider
        value={{
          mount: this._mount,
          update: this._update,
          unmount: this._unmount,
        }}
      >
        {/* Need collapsable=false here to clip the elevations, otherwise they appear above Portal components */}
        <View style={styles.container} collapsable={false}>
          {this.props.children}
        </View>
        <PortalManager ref={this._setManager} />
      </PortalContext.Provider>
    );
  }
}

我们这边可以看到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) => (
      <View
        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}
      </View>
    ));
  }
}

到了这边大家大致应该都明白了,我们可以看到这些其实都是在维护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<Locale>;
  theme?: Partial<Theme>;
}
export default class Provider extends React.Component<ProviderProps> {
  render() {
    return (
      <LocaleProvider locale={this.props.locale}>
        <ThemeProvider value={this.props.theme}>
          <Portal.Host>{this.props.children}</Portal.Host>
        </ThemeProvider>
      </LocaleProvider>
    );
  }
}

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

    render() {
      const Router = this.Router;
      return (
        <View style={{flex: 1}}>
          <Provider>
            {Platform.OS === 'ios' ? <StatusBar barStyle={this.state.barStyle} /> : null}
            <Router
              screenProps={this.props}
              {...getPersistenceFunctions(pageName)}
              renderLoadingExperimental={() => <ActivityIndicator />}
              onNavigationStateChange={this.onNavigationStateChange.bind(this)}
            />
          </Provider>
        </View>
      );
    }

三、优化实现

上个章节分析到,如果要实现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(
      <BaseDialog
        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 (
    <Modal
      animationType={'none'}
      onRequestClose={() => {
        setModalVisible(false);
      }}
      transparent={true}
      visible={modalVisible}>
      <View
        style={{
          backgroundColor: 'rgba(0,0,0,0.5)',
          alignItems: 'center',
          justifyContent: 'center',
          flex: 1,
        }}>
        <View style={styles.mainStyle}>
          {/*渐变*/}
          <LinearGradient
            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',
            }}>
            <View style={{flex: 1, height: 4}} />
          </LinearGradient>
          {/*内容区域*/}
          {props.containView}
          {/*确认按钮*/}
          <TouchableOpacity
            style={styles.okButtonStyle}
            onPress={() => {
              setModalVisible(false);
              props.okEvent && props.okEvent();
            }}>
            <LinearGradient
              style={styles.okLinearGradientStyle}
              start={{x: 0.0, y: 0.5}}
              end={{x: 1.0, y: 0.5}}
              colors={['#fc704e', '#ff9547']}>
              <Text style={{fontSize: 16, color: '#ffffff', lineHeight: 22}}>{props.okText}</Text>
            </LinearGradient>
          </TouchableOpacity>
          {/*取消按钮*/}
          <TouchableOpacity
            onPress={() => {
              setModalVisible(false);
              props.cancelEvent && props.cancelEvent();
            }}>
            <Text style={styles.cancelButtonStyle}>{props.cancelText ? props.cancelText : '取消'}</Text>
          </TouchableOpacity>
        </View>
      </View>
    </Modal>
  );
}

最后我们要在代码中显示弹窗,就不必像第一章节那样,显式的去声明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();