PAYATHON 2026

React Native WebView 如何处理新标签页链接?

支付阿杰

结论

可以处理。对于通过 window.open() 或 <a target="_blank"> 打开的支付确认页,可以使用 react-native-webview 的 onOpenWindow 获取目标 URL,再根据情况选择处理方式:

  1. 在当前 WebView 中打开;
  2. 在 Modal 中创建第二个 WebView,模拟新标签页;
  3. 按支付网关的要求,交给系统浏览器或原生支付 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 模拟新标签页。

备注:内容仅供参考。