收口移动壳WebView状态重放
移动壳同源页面加载后补发生命周期和网络状态 移动壳配置检查锁定WebView状态重放路径 宿主壳方案文档补充状态重放边界 共享决策日志记录移动壳状态重放约束
This commit is contained in:
+48
-26
@@ -24,7 +24,10 @@ import {
|
||||
shouldAcceptMobileShellHostBridgeMessage,
|
||||
shouldOpenInMobileShellWebView,
|
||||
} from './src/mobileShellNavigation';
|
||||
import { subscribeMobileNetworkStatus } from './src/mobileShellNetwork';
|
||||
import {
|
||||
getMobileNetworkStatus,
|
||||
subscribeMobileNetworkStatus,
|
||||
} from './src/mobileShellNetwork';
|
||||
import { MOBILE_SHELL_HOST_VERSION } from './src/mobileShellRuntime';
|
||||
import { MOBILE_SHELL_SAFE_AREA_EDGES } from './src/mobileShellSafeArea';
|
||||
import {
|
||||
@@ -60,6 +63,28 @@ export default function App() {
|
||||
const reloadCurrentWebView = useCallback(() => {
|
||||
webViewRef.current?.reload();
|
||||
}, []);
|
||||
const injectHostBridgeEvent = useCallback((event: string, payload: unknown) => {
|
||||
webViewRef.current?.injectJavaScript(
|
||||
buildHostBridgeMessageScript({
|
||||
bridge: 'GenarrativeHostBridge',
|
||||
version: 1,
|
||||
event,
|
||||
payload,
|
||||
}),
|
||||
);
|
||||
}, []);
|
||||
const injectLifecycleEvent = useCallback(
|
||||
(state: AppStateStatus) => {
|
||||
injectHostBridgeEvent('app.lifecycle', lifecyclePayloadFromAppState(state));
|
||||
},
|
||||
[injectHostBridgeEvent],
|
||||
);
|
||||
const injectNetworkStatusEvent = useCallback(
|
||||
(payload: Awaited<ReturnType<typeof getMobileNetworkStatus>>) => {
|
||||
injectHostBridgeEvent('network.statusChanged', payload);
|
||||
},
|
||||
[injectHostBridgeEvent],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
configureMobileHostBridgeNavigation({
|
||||
@@ -118,38 +143,18 @@ export default function App() {
|
||||
}, [canGoBack]);
|
||||
|
||||
useEffect(() => {
|
||||
const sendLifecycleEvent = (state: AppStateStatus) => {
|
||||
webViewRef.current?.injectJavaScript(
|
||||
buildHostBridgeMessageScript({
|
||||
bridge: 'GenarrativeHostBridge',
|
||||
version: 1,
|
||||
event: 'app.lifecycle',
|
||||
payload: lifecyclePayloadFromAppState(state),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const subscription = AppState.addEventListener(
|
||||
'change',
|
||||
sendLifecycleEvent,
|
||||
injectLifecycleEvent,
|
||||
);
|
||||
sendLifecycleEvent(AppState.currentState);
|
||||
injectLifecycleEvent(AppState.currentState);
|
||||
|
||||
return () => subscription.remove();
|
||||
}, []);
|
||||
}, [injectLifecycleEvent]);
|
||||
|
||||
useEffect(() => {
|
||||
return subscribeMobileNetworkStatus((payload) => {
|
||||
webViewRef.current?.injectJavaScript(
|
||||
buildHostBridgeMessageScript({
|
||||
bridge: 'GenarrativeHostBridge',
|
||||
version: 1,
|
||||
event: 'network.statusChanged',
|
||||
payload,
|
||||
}),
|
||||
);
|
||||
});
|
||||
}, []);
|
||||
return subscribeMobileNetworkStatus(injectNetworkStatusEvent);
|
||||
}, [injectNetworkStatusEvent]);
|
||||
|
||||
const handleMessage = (event: WebViewMessageEvent) => {
|
||||
if (
|
||||
@@ -180,6 +185,22 @@ export default function App() {
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleWebViewLoad = (event: { nativeEvent: { url: string } }) => {
|
||||
if (
|
||||
!shouldAcceptMobileShellHostBridgeMessage(
|
||||
event.nativeEvent.url,
|
||||
allowedWebOrigin,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
injectLifecycleEvent(AppState.currentState);
|
||||
void getMobileNetworkStatus()
|
||||
.then(injectNetworkStatusEvent)
|
||||
.catch(() => undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaProvider>
|
||||
<SafeAreaView style={styles.root} edges={MOBILE_SHELL_SAFE_AREA_EDGES}>
|
||||
@@ -206,6 +227,7 @@ export default function App() {
|
||||
onMessage={handleMessage}
|
||||
onContentProcessDidTerminate={reloadCurrentWebView}
|
||||
onRenderProcessGone={reloadCurrentWebView}
|
||||
onLoad={handleWebViewLoad}
|
||||
onShouldStartLoadWithRequest={handleShouldStartLoad}
|
||||
onNavigationStateChange={(event) => {
|
||||
setCanGoBack(event.canGoBack);
|
||||
|
||||
@@ -702,7 +702,13 @@ for (const snippet of [
|
||||
'AppState.addEventListener',
|
||||
'app.lifecycle',
|
||||
'network.statusChanged',
|
||||
'getMobileNetworkStatus',
|
||||
'subscribeMobileNetworkStatus',
|
||||
'injectHostBridgeEvent',
|
||||
'injectLifecycleEvent',
|
||||
'injectNetworkStatusEvent',
|
||||
'handleWebViewLoad',
|
||||
'onLoad={handleWebViewLoad}',
|
||||
'navigation.canGoBack',
|
||||
'buildHostBridgeMessageScript',
|
||||
'origin: window.location.origin',
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
- 2026-06-18 宿主外观只读查询:新增 `appearance.getColorScheme` HostBridge capability,Expo 壳通过 React Native `Appearance.getColorScheme()` 读取系统配色,Tauri 壳通过主窗口 `theme()` 读取窗口主题;该能力只返回 `light` / `dark` / `unknown`,不设置 H5 主题、不覆盖系统主题,也不作为强制 UI 样式入口。
|
||||
- 2026-06-18 原生壳生命周期事件:新增 `app.lifecycle` HostBridge capability,Expo 壳通过 React Native `AppState` 派发 `active` / `inactive` / `background`,Tauri 壳通过主窗口 focus / blur 派发 `active` / `inactive`;H5 只通过 `subscribeHostAppLifecycle()` 订阅统一状态,后续游戏循环、音频和轮询暂停 / 恢复不得直接依赖 Expo / Tauri 平台细节。
|
||||
- 2026-06-18 原生壳网络状态:新增 `network.status` 与 `network.statusChanged` HostBridge capability,Expo 壳通过 `expo-network` 查询和订阅真实系统网络状态,Tauri 壳通过短超时连接 `app.genarrative.world:443` 查询主站可达性,并通过 WebView `online` / `offline` 注入变化事件;H5 统一使用 `getHostNetworkStatus()` / `subscribeHostNetworkStatusChange()`,不得直接读取 Expo / Tauri 私有网络 API。
|
||||
- 2026-06-18 移动壳 WebView 状态重放:Expo WebView 每次同源主站页面成功加载后都会补发当前 `app.lifecycle` 与 `network.statusChanged` 状态,覆盖首载、受控刷新、H5 刷新和系统回收 WebView 进程后的新 JS 上下文;补发不新增 HostBridge capability,也不向 `about:blank`、外域或错误页注入宿主状态。
|
||||
- 2026-06-18 外部生成队列轮询接入宿主网络状态:H5 新增 `useHostNetworkOnline()`,宿主未声明网络能力时按在线处理以保持浏览器和旧壳行为;宿主明确 `isConnected=false` 或 `isInternetReachable=false` 时,平台外部生成队列概览暂停 HTTP 轮询,恢复在线后重新刷新。该能力只减少离线请求,不改变外部生成队列、作品架、弹窗或后端任务状态事实。
|
||||
- 2026-06-18 桌面图片导入:新增 `file.importImage` 与 `file.imageDropped` HostBridge capability,Tauri 壳通过系统文件选择框和主窗口拖拽事件读取用户选择 / 拖入的真实图片,只允许 `image/png`、`image/jpeg`、`image/webp` 且单次不超过 10 MiB;H5 统一使用 `importHostImageFile()` / `subscribeHostImageDrop()`,宿主只回传文件名、MIME、base64 内容、字节数和可选坐标,不暴露本地绝对路径,也不开放通用文件系统。
|
||||
- 2026-06-18 移动图片导入:Expo 壳开始声明并实现 `file.importImage`,通过 `expo-image-picker` 请求相册权限并打开系统相册选择器,只允许 `image/png`、`image/jpeg`、`image/webp` 且单次不超过 10 MiB;成功只回传清洗后的文件名、MIME、base64 内容和字节数,不暴露设备本地 URI,用户取消返回 `cancelled` 并由 H5 facade 归为 `false`。
|
||||
|
||||
@@ -286,6 +286,8 @@ GameBridge 禁止:
|
||||
|
||||
2026-06-18 追加:移动壳 WebView 内容 / 渲染进程终止时复用同一受控刷新路径。iOS `onContentProcessDidTerminate` 和 Android `onRenderProcessGone` 只调用当前 `react-native-webview` 的 `reload()`,不改写 H5 URL、不注入额外脚本、不新增宿主恢复页面,避免系统回收 WebView 进程后留下空白容器。
|
||||
|
||||
2026-06-18 追加:移动壳 WebView 每次同源主站页面成功加载后都会补发当前 `app.lifecycle` 与 `network.statusChanged` 状态,覆盖首载、`app.reloadWebView`、进程恢复 reload 和 H5 自身刷新后的新 JS 上下文。补发仍只使用 HostBridge event,不新增 capability,不向 `about:blank`、外域或错误页注入宿主状态。
|
||||
|
||||
2026-06-18 追加:移动壳根布局接入 `react-native-safe-area-context`,用 `SafeAreaProvider` 和四边 `SafeAreaView` 承接 iOS 刘海、底部 Home Indicator、Android 状态栏和横屏边缘安全区;WebView 仍加载同一 H5 主站,不改变 H5 路由、玩法 runtime 或 HostBridge capability。该能力属于宿主壳布局保护,不在 H5 内补额外占位 UI。
|
||||
|
||||
2026-06-18 追加:移动壳不锁定竖屏,Expo `orientation` 固定为 `default`,由 iOS / Android 设备方向和 H5 响应式布局共同承接竖屏、横屏与后续横屏玩法;壳层只负责安全区、WebView 容器和 HostBridge,不为某一类玩法硬编码方向。`npm run mobile-shell:typecheck` 与 `npm run mobile-shell:config` 会拒绝重新锁回 portrait / landscape。
|
||||
|
||||
Reference in New Issue
Block a user