收口移动壳WebView状态重放

移动壳同源页面加载后补发生命周期和网络状态

移动壳配置检查锁定WebView状态重放路径

宿主壳方案文档补充状态重放边界

共享决策日志记录移动壳状态重放约束
This commit is contained in:
2026-06-18 14:47:27 +08:00
parent 165520a911
commit 475a1e866a
4 changed files with 57 additions and 26 deletions
+48 -26
View File
@@ -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 capabilityExpo 壳通过 React Native `Appearance.getColorScheme()` 读取系统配色,Tauri 壳通过主窗口 `theme()` 读取窗口主题;该能力只返回 `light` / `dark` / `unknown`,不设置 H5 主题、不覆盖系统主题,也不作为强制 UI 样式入口。
- 2026-06-18 原生壳生命周期事件:新增 `app.lifecycle` HostBridge capabilityExpo 壳通过 React Native `AppState` 派发 `active` / `inactive` / `background`Tauri 壳通过主窗口 focus / blur 派发 `active` / `inactive`H5 只通过 `subscribeHostAppLifecycle()` 订阅统一状态,后续游戏循环、音频和轮询暂停 / 恢复不得直接依赖 Expo / Tauri 平台细节。
- 2026-06-18 原生壳网络状态:新增 `network.status``network.statusChanged` HostBridge capabilityExpo 壳通过 `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 capabilityTauri 壳通过系统文件选择框和主窗口拖拽事件读取用户选择 / 拖入的真实图片,只允许 `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。