React Native JavaScript WebView

React Native の WebView で既存Webサイトを表示していると、リンクをタップしても画面が遷移しないことがある。原因はサイト側のJavaScriptが window.open('', '_blank') で空のウィンドウを開き、その戻り値の location.href に遷移先を代入するタイプの実装になっている場合で、モバイルWebViewは新規ウィンドウを許可していないため戻り値が使えず、代入先が消える。サイト側のコードは触れない前提で、WebView側の注入JSで window.openProxy に差し替えて代入をトラップし、親windowへ遷移させると解決する。

症状:タップしても何も起きない

ブラウザで開くと動くのに、アプリのWebViewで開くとリンクがまったく反応しない。URLも変わらない、エラーも出ない。

まずサイト側のコードを覗くと、こんな実装になっていることがある。

function openInNewTab(url) {
  var w = window.open('', '_blank');
  w.location.href = url;
}

ブラウザなら「空のタブを開いて→そのタブのlocationを書き換える」で正しく遷移する。しかしReact Native WebViewは _blank を新しいWebViewに割り振るような挙動をせず、window.open の戻り値が null 相当になる。次の行の w.location.href = url はプロパティアクセスで例外になるか、無音で無視される。

対処:window.open をProxyで乗っ取る

サイト側を改修しない前提なので、WebViewが読み込む前に window.open を差し替える。Proxy にすると存在しないウィンドウの location.href = url 代入をトラップして、自分のwindowを飛ばせる。

const INJECTED_JS = `
(function() {
  window.open = function(url, target) {
    // ケース1: window.open(url, '_blank') — URLを直接渡している
    if (url && url !== '' && target === '_blank') {
      window.location.href = url;
      return null;
    }
    // ケース2: window.open('', '_blank') → 後で location.href に代入する
    return new Proxy({}, {
      get(_, prop) {
        if (prop === 'location') {
          return new Proxy({}, {
            set(_, k, v) {
              if (k === 'href' && v) {
                window.location.href = v;
              }
              return true;
            }
          });
        }
        return undefined;
      },
      set() { return true; }
    });
  };
})();
true;
`;

返り値のProxyは、.location にアクセスされたら第二のProxyを返し、その第二のProxyが href への代入を捕まえて window.location.href にリレーする。他のプロパティは無視するので、サイト側が w.focus() のようなメソッドを呼んでも壊れない。

injectedJavaScriptBeforeContentLoaded に入れる

差し替えは、ページのスクリプトが動く前に完了している必要がある。injectedJavaScript(読み込み完了後に実行)だと、サイト側のイベントハンドラが既に元の window.open を参照して束縛済みなことがあり、乗っ取りが効かない。

<WebView
  source={{ uri: 'https://example.com/' }}
  injectedJavaScriptBeforeContentLoaded={INJECTED_JS}
  onShouldStartLoadWithRequest={(req) => {
    // 親windowが飛ぼうとしたURLをここでネイティブ画面に橋渡ししてもよい
    return true;
  }}
/>

末尾の true; は注入JSの評価結果として何か値を返すためのおまじない。オブジェクトが最終値だとネイティブ側でエラーになることがある。

効かないパターン:新規ウィンドウの中で処理が続くケース

今回のProxyは「開いたウィンドウの location.href に代入して終わり」のパターンに効く。window.open の戻り値に対して document.write したり、postMessage を送ったり、focus() の戻り値を確認するようなコードは、本来ウィンドウが存在しないと成立しないので、Proxyで穴埋めするより onShouldStartLoadWithRequest でURLを拾ってネイティブ側でハンドリングした方が早い。

目的は「サイト側の window.open で始まる遷移を、モバイルWebViewでも同じ画面に遷移させる」ことに絞る。汎用のwindow shimを目指すと崩れる。