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