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