美文网首页
基于AntDesign改造优化的Modal弹窗

基于AntDesign改造优化的Modal弹窗

作者: RmondJone | 来源:发表于2020-09-30 17:05 被阅读0次

    一、前言

    在日常开发中,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();
    

    相关文章

      网友评论

          本文标题:基于AntDesign改造优化的Modal弹窗

          本文链接:https://www.haomeiwen.com/subject/kjfpuktx.html