Files
jrshikoku/lib/useWebViewRemount.ts

129 lines
5.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useRef, useState } from "react";
import { AppState, AppStateStatus } from "react-native";
import WebView from "react-native-webview";
/**
* WebView のメモリ解放・プロセス終了による白画面を自動復帰させるフック。
*
* 使い方:
* const { remountKey, remount, processHandlers, pingHandlers, webViewRef } = useWebViewRemount();
* <WebView key={remountKey} ref={webViewRef} {...processHandlers} {...pingHandlers} ... />
*
* 既存の ref がある場合は webViewRef を使わず processHandlers / pingHandlers だけ使ってもよい。
* pingHandlers を使う場合は onMessage を上書きせず spread すること。
* ping による白画面検知を有効にするには pingEnabled: true を渡す。
*/
export function useWebViewRemount(options?: { pingEnabled?: boolean; backgroundThresholdMs?: number }) {
const pingEnabled = options?.pingEnabled ?? false;
const backgroundThresholdMs = options?.backgroundThresholdMs ?? 300_000; // デフォルト5分
const [remountKey, setRemountKey] = useState(0);
const backgroundedAt = useRef<number | null>(null);
const webViewRef = useRef<WebView>(null);
// ping watchdog 用
const lastPongAt = useRef(Date.now());
const isLoadingRef = useRef(true);
const remount = useCallback(() => {
lastPongAt.current = Date.now();
isLoadingRef.current = true;
setRemountKey((k) => k + 1);
}, []);
// バックグラウンドから復帰したら再マウントデフォルト5分超
useEffect(() => {
const onAppStateChange = (nextState: AppStateStatus) => {
if (nextState.match(/inactive|background/)) {
backgroundedAt.current = Date.now();
} else if (nextState === "active" && backgroundedAt.current !== null) {
const elapsed = Date.now() - backgroundedAt.current;
backgroundedAt.current = null;
if (elapsed > backgroundThresholdMs) {
remount();
}
}
};
const subscription = AppState.addEventListener("change", onAppStateChange);
return () => subscription.remove();
}, [remount, backgroundThresholdMs]);
// ping watchdog: 5秒ごとに生存確認と白画面検知を行う
// - ローディング中isLoadingRef=trueでも45秒超なら remountレンダラー死亡でonLoadEndが来ないケース
// - ロード完了後は30秒 pong 無応答で remount
const maxTextLenRef = useRef(0);
const blankCountRef = useRef(0);
useEffect(() => {
if (!pingEnabled) return;
const id = setInterval(() => {
const elapsed = Date.now() - lastPongAt.current;
if (isLoadingRef.current) {
// ローディング中でも45秒超はレンダラー死亡と判定
if (elapsed > 45_000) remount();
return;
}
// ロード完了後30秒 pong 無応答 → レンダラー死亡
if (elapsed > 30_000) {
remount();
return;
}
webViewRef.current?.injectJavaScript(
`(function(){var t=document.body?(document.body.innerText||'').replace(/\\s+/g,'').length:0;window.ReactNativeWebView&&window.ReactNativeWebView.postMessage(JSON.stringify({type:'__ping',len:t}));})();true;`
);
}, 5_000);
return () => clearInterval(id);
}, [pingEnabled, remount]);
const processHandlers = {
onRenderProcessGone: () => remount(),
onContentProcessDidTerminate: () => remount(),
} as const;
// pingHandlers は onLoadEnd と onMessage を提供する
// 既存の onMessage がある場合は手動でマージすること
const pingHandlers = pingEnabled ? {
onLoadEnd: () => {
isLoadingRef.current = false;
lastPongAt.current = Date.now();
maxTextLenRef.current = 0;
blankCountRef.current = 0;
// ロード完了3秒後に初回コンテンツチェック
setTimeout(() => {
if (!isLoadingRef.current) {
webViewRef.current?.injectJavaScript(
`(function(){var t=document.body?(document.body.innerText||'').replace(/\\s+/g,'').length:0;window.ReactNativeWebView&&window.ReactNativeWebView.postMessage(JSON.stringify({type:'__ping',len:t}));})();true;`
);
}
}, 3000);
},
onLoadStart: () => {
isLoadingRef.current = true;
lastPongAt.current = Date.now(); // ナビゲーション開始時にタイムアウトリセット
maxTextLenRef.current = 0;
blankCountRef.current = 0;
},
onMessage: (event: any) => {
try {
const parsed = JSON.parse(event.nativeEvent.data);
if (parsed.type === "__ping") {
lastPongAt.current = Date.now(); // 応答ごとにタイムアウトリセット
const len: number = parsed.len ?? 0;
if (len > maxTextLenRef.current) maxTextLenRef.current = len;
// 一度でも20文字超になったページが5文字未満になったら白画面と判定
if (maxTextLenRef.current > 20 && len < 5) {
blankCountRef.current += 1;
if (blankCountRef.current >= 3) {
blankCountRef.current = 0;
maxTextLenRef.current = 0;
remount();
}
} else {
blankCountRef.current = 0;
}
}
} catch {}
},
} : {};
return { remountKey, remount, processHandlers, pingHandlers, webViewRef };
}