在Mobile App开发中,一个很常见的UI设计就是弹出提示框。比如,当用户输入有误时,我们希望从顶部向下划出一个提示,如图所示,当用户没有输入用户名和密码就点击登陆后,会提示出相应的错误。
![132a7e4c0d802f478deb632193968450.png](https://i-blog.csdnimg.cn/blog_migrate/27c9f4e690284cc78bc19ba7fe9b18f1.jpeg)
当我们用React Native去开发这个功能时,首先能够想到的一种方式就是,在组件中设置一个状态,当它为true时,渲染出提示框;当为false时,隐藏提示框,具体怎么实现相信大家都是没问题的。
但是当我测试这个功能的时候,问题出现了。我发现在iOS中,一切正常,但是在Android手机中,就是看不到提示框,这又是问什么呢?后来发现,问题出在Android对于<View>的显示方式和iOS不同,任何超出<View>边界的东西都会被隐藏掉,相当于被默认设置了overflow: hidden一样。
在我的组件结构中,红色边框内是名为Login的组件,其中有个状态变量isErrorShown,当它为true时,就会渲染<ErrorAlert />这样一个组件出来,那么很显然,<ErrorAlert />就是<Login />的子组件,它也同样是属于这个红色边框以内的,当我通过css设置,把它放在窗口顶部时,它就已经超出了红色边界,在Android上就会被隐藏起来。
![d801da17bd47a5a8b7f6374c915b4672.png](https://i-blog.csdnimg.cn/blog_migrate/5c9eb9dd17d98d3cf47bd69843a0b0e8.jpeg)
那么该如何解决这个问题呢?我们可能会想到把<ErrorAlert />放在<Login />的父组件中(当然也可能是爷爷辈的组件),让它脱离红色框框的管辖,然后把isErrorShown放在Redux中管理。这当然也是一种办法,但是当有多种类型的alert时,就都要在Redux中设置相应的变量;此外alert放置的位置也不一定都是固定在窗口顶端,可能有的时候就需要把它放置在某个组件的顶部或者底部,那么承载它的组件就不得不去connect Redux,在我看来,这种方法可能有点麻烦。
那么除了利用Redux还有其他的办法的么?当然有,对React熟悉的同学可能想到了利用Portal这个特性,具体的使用方法大家可以参看文档,简单的理解,Portal就像一个传送门,可以在不同组件之间传送React Node,你只需要指定好接收React Node的domNode就可以了。那么解决方案已经出来了,只需要在isErrorShown时,把<ErrorAlert />传送到<Login />的某个父组件中去渲染就可以了。
正当我沾沾自喜,庆幸找到解决方案的时候,坑来了!React Native中竟然不支持Portal。。。心中一万头草泥马崩腾而过。。。好吧,让我们利用Context API实现一个类似Portal的功能,代码如下:
PortalContext.js
// @flow
import * as React from 'react';
export default React.createContext({
gates: {}, // key: 挂载点名称,value: 需要渲染的React.Node
teleport: (gateName: string[], element: React.Node) => {}, //将需要渲染的element,传送到对应的挂载点上
});
PortalProvider.js
// @flow
import * as React from 'react';
import PortalContext from './PortalContext';
type Props = {
children: React.Node,
}
type State = {
gates: {
[key: string]: React.Node,
}
}
class PortalProvider extends React.Component<Props, State> {
state = {
gates: {}, // 存储着一一对应的挂载点名称和React.Node
}
teleport = (gateName: string[], element: React.Node) => { // 运行teleport时,将传来的ReactNode存储在gates中
const { gates } = this.state;
const newGates = gateName.reduce((acc, name) => {
acc[name] = element;
return acc;
}, {});
this.setState({
gates: { ...gates, ...newGates},
});
return this.teleport;
}
render() {
const { children } = this.props;
const { gates } = this.state;
return (
<PortalContext.Provider value={{ gates, teleport: this.teleport }}>
{children}
</PortalContext.Provider>
);
}
}
export default PortalProvider;
PortalGate.js
// @flow
import * as React from 'react';
import PortalContext from './PortalContext';
type Props = {
gateName: string,
children?: (teleport: (gateName: string[], element: React.Node) => void) => React.Node,
}
function PortalGate(props: Props) { // 根据挂载点名称,从gates中取出对应的ReactNode进行渲染
const { gateName, children } = props;
return (
<PortalContext.Consumer>
{
(value) => {
return (
<React.Fragment>
{value.gates[gateName]}
</React.Fragment>
);
}
}
</PortalContext.Consumer>
);
}
export default PortalGate;
wrapWithTeleport.js
import React from 'react';
import PortalContext from './PortalContext';
const wrapWithTeleport = WrappedComponent => props => ( // HOC,向组件中注入teleport方法,让组件能够有传送能力
<PortalContext.Consumer>
{(value) => {
const { teleport } = value;
return <WrappedComponent teleport={teleport} {...props} />;
}}
</PortalContext.Consumer>
);
export default wrapWithTeleport;
使用方法如下:
当点击Button后,Login中运行teleport方法,将<ErrorAlert />传送到gateName为’errorAlert`的PortalGate组件中去渲染。这样<ErrorAlert />就可以渲染在<Login />外面,Android中就可以正常显示了。
App.js
const App = () => {
return (
<PortalProvider>
<PortalGate gateName="errorAlert" />
<Login />
</PortalProvider>
)
}
export default App;
Login.js
const Login = (props) => {
const { teleport } = props;
const handleOnPress = () => {
teleport(['errorAlert'], <ErrorAlert />);
}
return (
<View>
...
<Button onPress={handleOnPress} />
...
</View>
)
}
export default wrapWithTeleport(Login);
以上就是React Native中实现类似portal效果的方法,大家可以利用它,把组件渲染在任何需要的地方。完结,撒花!