React Native WebView 如何处理新标签页链接?
结论
可以处理。对于通过 window.open() 或 <a target="_blank"> 打开的支付确认页,可以使用 react-native-webview 的 onOpenWindow 获取目标 URL,再根据情况选择处理方式:
- 在当前 WebView 中打开;
- 在
Modal中创建第二个 WebView,模拟新标签页; - 按支付网关的要求,交给系统浏览器或原生支付 SDK。
普通 URL 可以直接在 App 内打开。但如果支付页依赖弹窗上下文、跨站 Cookie、表单 POST、window.opener、3DS 验证或外部银行 App,仅把 URL 放进另一个 WebView 并不能解决问题,还要按照支付网关的接入规范进行处理。
方案一:在当前 WebView 中打开
如果所谓的“新标签页”只是普通 HTTPS 地址,不依赖特殊的弹窗环境,最简单的做法是关闭多窗口支持,让链接在当前 WebView 中打开:
import React from 'react';
import { BackHandler } from 'react-native';
import { WebView } from 'react-native-webview';
export default function CheckoutWebView() {
const webViewRef = React.useRef<WebView>(null);
const [canGoBack, setCanGoBack] = React.useState(false);
React.useEffect(() => {
const subscription = BackHandler.addEventListener(
'hardwareBackPress',
() => {
if (!canGoBack) {
return false;
}
webViewRef.current?.goBack();
return true;
},
);
return () => subscription.remove();
}, [canGoBack]);
return (
<WebView
ref={webViewRef}
source={{ uri: 'https://shop.example.com/checkout' }}
javaScriptEnabled
domStorageEnabled
javaScriptCanOpenWindowsAutomatically
setSupportMultipleWindows={false}
onNavigationStateChange={(state) => {
setCanGoBack(state.canGoBack);
}}
/>
);
}
setSupportMultipleWindows={false} 主要影响 Android。这种方式适合简单重定向,但会改变网页原有的多窗口行为。支付完成后,用户可以通过 WebView 的返回操作回到结账页。
方案二:使用第二个 WebView 模拟新标签页
如果希望支付确认页显示在独立窗口中,可以通过 onOpenWindow 获取 targetUrl,再用 Modal 打开第二个 WebView:
import React from 'react';
import {
ActivityIndicator,
Modal,
SafeAreaView,
StyleSheet,
TouchableOpacity,
Text,
View,
} from 'react-native';
import { WebView } from 'react-native-webview';
const CHECKOUT_URL = 'https://shop.example.com/checkout';
const PAYMENT_RETURN_URL = 'https://shop.example.com/payment/return';
export default function CheckoutWebView() {
const mainWebViewRef = React.useRef<WebView>(null);
const [popupUrl, setPopupUrl] = React.useState<string | null>(null);
const closePaymentWindow = () => {
setPopupUrl(null);
// 根据业务接口刷新订单状态,通常比单纯 reload 更可靠。
mainWebViewRef.current?.reload();
};
return (
<SafeAreaView style={styles.container}>
<WebView
ref={mainWebViewRef}
source={{ uri: CHECKOUT_URL }}
javaScriptEnabled
domStorageEnabled
javaScriptCanOpenWindowsAutomatically
setSupportMultipleWindows
sharedCookiesEnabled
thirdPartyCookiesEnabled
onOpenWindow={(event) => {
const targetUrl = event.nativeEvent.targetUrl;
if (targetUrl?.startsWith('https://')) {
setPopupUrl(targetUrl);
}
}}
/>
<Modal
visible={popupUrl !== null}
animationType="slide"
onRequestClose={() => setPopupUrl(null)}
>
<SafeAreaView style={styles.container}>
<View style={styles.header}>
<TouchableOpacity onPress={() => setPopupUrl(null)}>
<Text style={styles.closeButton}>关闭</Text>
</TouchableOpacity>
</View>
{popupUrl && (
<WebView
source={{ uri: popupUrl }}
javaScriptEnabled
domStorageEnabled
javaScriptCanOpenWindowsAutomatically
sharedCookiesEnabled
thirdPartyCookiesEnabled
startInLoadingState
renderLoading={() => (
<ActivityIndicator style={StyleSheet.absoluteFill} />
)}
onShouldStartLoadWithRequest={(request) => {
if (request.url.startsWith(PAYMENT_RETURN_URL)) {
closePaymentWindow();
return false;
}
return true;
}}
/>
)}
</SafeAreaView>
</Modal>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
height: 48,
justifyContent: 'center',
paddingHorizontal: 16,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#ccc',
},
closeButton: {
color: '#1677ff',
fontSize: 16,
},
});
不要直接信任网页提供的任意 targetUrl。生产环境中需要校验协议、域名和路径,例如只允许自己的商城域名,以及支付服务商明确公布的域名。
为什么系统浏览器可能只显示空白页
支付网站的新标签页不一定直接指向最终地址。常见的实现方式如下:
const paymentWindow = window.open('', '_blank');
paymentWindow.document.write(paymentForm);
paymentWindow.document.forms[0].submit();
在这种情况下,window.open() 最初打开的是 about:blank,网页随后通过 JavaScript 写入表单并提交。如果 React Native 只把初始地址交给系统浏览器,浏览器看到的就只有空白页。
还可能有以下原因:
- 支付请求使用 POST 表单,而不是普通的 GET URL;
- 支付会话依赖原 WebView 中的 Cookie;
- 支付页会检查
window.opener; - 网关禁止嵌入式 WebView;
- 3DS、银行验证或钱包 App 使用自定义 URL Scheme;
- Android WebView 没有启用 JavaScript 弹窗或多窗口支持;
- iOS 的导航策略拦截了目标 URL。
因此,应先在 onOpenWindow 中记录 targetUrl。如果得到的是 about:blank,仅创建一个使用 source={{ uri: targetUrl }} 的 WebView,通常无法恢复网页后续写入的内容。
about:blank 场景的处理
如果可以修改网页代码,更稳妥的做法是不要先生成空白窗口再写入内容,而是让后端创建支付会话,并返回一个可以直接导航的 HTTPS 地址:
const response = await fetch('/api/payment/session', {
method: 'POST',
});
const { confirmationUrl } = await response.json();
window.location.assign(confirmationUrl);
如果必须保留浏览器端的表单提交,可以让表单在当前页面提交:
<form
id="payment-form"
method="POST"
action="https://payment.example.com/confirm"
target="_self"
>
<!-- 支付网关要求的字段 -->
</form>
不要为了绕过问题而随意注入脚本、读取支付表单或复制支付参数。支付数据应继续交由支付服务商页面或官方 SDK 处理。
支付完成后的返回方式
建议为支付流程配置明确的回调地址,例如:
https://shop.example.com/payment/return
App 检测到这个地址后,关闭支付窗口,再向自己的服务端查询订单状态。不能只根据页面显示的“支付成功”来判断结果,因为用户可能关闭页面,网络也可能中断,前端地址还可能被伪造。
可靠的流程通常是:
支付网关服务端通知
↓
商城服务端验证签名并更新订单
↓
App 查询商城服务端的订单状态
如果支付网关通过自定义 Scheme 或 Universal Link/App Link 返回,例如:
myshop://payment/result?orderId=123
还需要配置 React Native Linking、iOS Universal Links 或 Android App Links,并在 WebView 中拦截对应的 URL。
注意事项
- 先确认支付服务商是否允许在 WebView 内完成付款。有些网关要求使用系统浏览器、Chrome Custom Tabs、
ASWebAuthenticationSession或官方原生 SDK。 - 不要绕过支付页的证书错误、域名校验或安全限制。
- 限制 WebView 可以访问的支付域名,并拦截未知协议。
sharedCookiesEnabled主要用于 iOS,thirdPartyCookiesEnabled主要用于 Android。是否需要开启,要看支付网关如何维护会话。- 如果支付流程依赖多层弹窗,单个
popupUrl状态可能不够。这时需要维护窗口栈,或者改用系统浏览器。 - 支付结果必须由服务端验证,不能把 WebView 的跳转地址作为唯一依据。
- 如果
onOpenWindow没有触发,应确认页面是否确实使用了window.open()或target="_blank",并检查当前react-native-webview版本是否支持相关属性。不同版本和平台的行为可能不同。
电商支付通常应优先使用官方支付 SDK。其次是支付服务商允许的系统浏览器授权流程,最后才是在 App 内通过第二个 WebView 模拟新标签页。
备注:内容仅供参考。
