From 4f95e29895f6e48a727659e596433a3de4a0a191 Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 20 Jun 2026 15:54:14 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E7=B4=A7=E7=A7=BB=E5=8A=A8=E5=A3=B3?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E6=B3=A8=E5=85=A5=E5=A4=B1=E8=B4=A5=E8=BE=B9?= =?UTF-8?q?=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移动壳 HostBridge 响应和事件注入统一捕获 WebView 同步异常 移动壳 ShellApp 测试覆盖响应注入失败不崩溃 移动壳配置检查反查统一消息注入边界 共享决策记录补充移动消息注入失败约定 --- apps/mobile-shell/scripts/check-config.mjs | 23 ++++++--- apps/mobile-shell/src/shell/ShellApp.test.tsx | 42 +++++++++++++++++ apps/mobile-shell/src/shell/ShellApp.tsx | 47 ++++++++++--------- .../shared-memory/decision-log.md | 6 +-- 4 files changed, 88 insertions(+), 30 deletions(-) diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index 8ddecd15f..53b629cf1 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -1375,12 +1375,16 @@ for (const snippet of [ 'h5CanGoBackRef', 'syncNavigationCanGoBack', 'resetNavigationCanGoBack', + 'injectHostBridgeMessage', 'injectHostBridgeEvent', 'injectLifecycleEvent', 'injectNetworkStatusEvent', 'logMobileHostEventFailure', + 'logMobileHostBridgeMessageFailure', 'try {', 'logMobileHostEventFailure(event, error)', + 'injectHostBridgeMessage(response, logMobileHostBridgeMessageFailure)', + "console.warn('mobile HostBridge message injection failed', error)", "logMobileHostEventFailure('network.statusChanged', error)", 'handleWebViewLoad', 'onLoad={handleWebViewLoad}', @@ -1477,13 +1481,17 @@ for (const snippet of [ for (const snippet of [ 'type HostBridgeEventName', - 'isHostBridgeEventName', - 'function buildHostBridgeEventScript(event: HostBridgeEventName, payload: unknown)', - 'if (!isHostBridgeEventName(event))', - 'throw new Error(`unsupported HostBridge event ${event}`)', - 'return buildHostBridgeMessageScript({', + 'const injectHostBridgeMessage = useCallback(', + 'buildHostBridgeMessageScript(message)', + 'onError(error)', '(event: HostBridgeEventName, payload: unknown)', - 'buildHostBridgeEventScript(event, payload)', + 'injectHostBridgeMessage(', + 'bridge: HOST_BRIDGE_PROTOCOL', + 'version: HOST_BRIDGE_VERSION', + 'event', + 'payload', + '(error) => logMobileHostEventFailure(event, error)', + 'injectHostBridgeMessage(response, logMobileHostBridgeMessageFailure)', ]) { if (!shellAppSource.includes(snippet)) { throw new Error(`mobile shell HostBridge event injection missing ${snippet}`); @@ -2440,6 +2448,9 @@ for (const snippet of [ 'injectJavaScriptError', "test('host event injection failures are logged without crashing the shell'", "new Error('webview injection failed')", + "test('logs HostBridge response injection failures without crashing the shell'", + "new Error('response injection failed')", + 'mobile HostBridge message injection failed', "type: 'genarrative.mobile.historyState'", 'mobile host event failed for network.statusChanged', 'external WebView navigation native failures stay outside the WebView', diff --git a/apps/mobile-shell/src/shell/ShellApp.test.tsx b/apps/mobile-shell/src/shell/ShellApp.test.tsx index 443da57da..47e01ffc3 100644 --- a/apps/mobile-shell/src/shell/ShellApp.test.tsx +++ b/apps/mobile-shell/src/shell/ShellApp.test.tsx @@ -326,6 +326,48 @@ describe('ShellApp QR scanner HostBridge flow', () => { }, }); }); + + test('logs HostBridge response injection failures without crashing the shell', async () => { + const ShellApp = await importShellApp(); + render(); + + const webViewProps = shellHarness.webViewProps.current as { + onMessage?: (event: { + nativeEvent: { + data: string; + url: string; + }; + }) => void; + source?: { uri?: string }; + }; + const webViewUrl = webViewProps.source?.uri ?? 'https://app.genarrative.world/'; + const injectionError = new Error('response injection failed'); + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + shellHarness.injectJavaScriptError.current = injectionError; + + expect(() => { + webViewProps.onMessage?.({ + nativeEvent: { + data: JSON.stringify({ + bridge: HOST_BRIDGE_PROTOCOL, + version: HOST_BRIDGE_VERSION, + id: 'runtime-request-1', + method: 'host.getRuntime', + }), + url: webViewUrl, + }, + }); + }).not.toThrow(); + + await waitFor(() => { + expect(warnSpy).toHaveBeenCalledWith( + 'mobile HostBridge message injection failed', + injectionError, + ); + }); + + warnSpy.mockRestore(); + }); }); describe('ShellApp HostBridge event injection', () => { diff --git a/apps/mobile-shell/src/shell/ShellApp.tsx b/apps/mobile-shell/src/shell/ShellApp.tsx index 7f6e4174c..2acb15c8c 100644 --- a/apps/mobile-shell/src/shell/ShellApp.tsx +++ b/apps/mobile-shell/src/shell/ShellApp.tsx @@ -19,7 +19,6 @@ import { type HostBridgeEventName, HOST_BRIDGE_PROTOCOL, HOST_BRIDGE_VERSION, - isHostBridgeEventName, } from '../../../../packages/shared/src/contracts/hostBridge'; import { configureMobileHostBridgeNavigation, @@ -60,23 +59,14 @@ function buildHostBridgeMessageScript(message: unknown) { )}, origin: window.location.origin, source: window })); true;`; } -function buildHostBridgeEventScript(event: HostBridgeEventName, payload: unknown) { - if (!isHostBridgeEventName(event)) { - throw new Error(`unsupported HostBridge event ${event}`); - } - - return buildHostBridgeMessageScript({ - bridge: HOST_BRIDGE_PROTOCOL, - version: HOST_BRIDGE_VERSION, - event, - payload, - }); -} - function logMobileHostEventFailure(label: HostBridgeEventName, error: unknown) { console.warn(`mobile host event failed for ${label}`, error); } +function logMobileHostBridgeMessageFailure(error: unknown) { + console.warn('mobile HostBridge message injection failed', error); +} + function logMobileShellNavigationFailure(label: string, error: unknown) { console.warn(`mobile shell navigation failed for ${label}`, error); } @@ -122,18 +112,35 @@ export default function ShellApp() { const reloadCurrentWebView = useCallback(() => { webViewRef.current?.reload(); }, []); - const injectHostBridgeEvent = useCallback( - (event: HostBridgeEventName, payload: unknown) => { + const injectHostBridgeMessage = useCallback( + ( + message: unknown, + onError: (error: unknown) => void, + ) => { try { webViewRef.current?.injectJavaScript( - buildHostBridgeEventScript(event, payload), + buildHostBridgeMessageScript(message), ); } catch (error) { - logMobileHostEventFailure(event, error); + onError(error); } }, [], ); + const injectHostBridgeEvent = useCallback( + (event: HostBridgeEventName, payload: unknown) => { + injectHostBridgeMessage( + { + bridge: HOST_BRIDGE_PROTOCOL, + version: HOST_BRIDGE_VERSION, + event, + payload, + }, + (error) => logMobileHostEventFailure(event, error), + ); + }, + [injectHostBridgeMessage], + ); const injectLifecycleEvent = useCallback( (state: AppStateStatus) => { injectHostBridgeEvent('app.lifecycle', lifecyclePayloadFromAppState(state)); @@ -267,9 +274,7 @@ export default function ShellApp() { } void handleMobileHostBridgeMessage(event.nativeEvent.data, (response) => { - webViewRef.current?.injectJavaScript( - buildHostBridgeMessageScript(response), - ); + injectHostBridgeMessage(response, logMobileHostBridgeMessageFailure); }); }; diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 3ef6d337f..66aa973d7 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -16,10 +16,10 @@ --- -## 2026-06-20 移动 HostBridge 事件注入失败边界 +## 2026-06-20 移动 HostBridge 消息注入失败边界 -- 背景:Expo 移动壳通过 WebView `injectJavaScript` 把 `app.lifecycle`、`network.statusChanged`、`navigation.canGoBack` 等宿主事件回放给 H5;如果 WebView 进程切换、页面卸载或注入同步失败,壳层不能因为宿主事件回放异常而崩溃。 -- 决策:移动壳所有 HostBridge 事件注入必须统一经过 `injectHostBridgeEvent`,该函数捕获同步注入异常并用 `logMobileHostEventFailure(event, error)` 记录;配置检查反查运行时 try/catch 和 ShellApp 注入失败测试。 +- 背景:Expo 移动壳通过 WebView `injectJavaScript` 把 HostBridge response 以及 `app.lifecycle`、`network.statusChanged`、`navigation.canGoBack` 等宿主事件回放给 H5;如果 WebView 进程切换、页面卸载或注入同步失败,壳层不能因为响应或事件回灌异常而崩溃。 +- 决策:移动壳所有 HostBridge message 注入必须统一经过 `injectHostBridgeMessage`,该函数捕获同步注入异常;事件注入用 `logMobileHostEventFailure(event, error)` 记录,response 注入用 `logMobileHostBridgeMessageFailure(error)` 记录。配置检查反查运行时 try/catch 和 ShellApp 注入失败测试。 - 影响范围:`apps/mobile-shell/src/shell/ShellApp.tsx`、`apps/mobile-shell/src/shell/ShellApp.test.tsx`、`apps/mobile-shell/scripts/check-config.mjs`。 - 验证方式:`npm run mobile-shell:test -- src/shell/ShellApp.test.tsx`、`npm run mobile-shell:typecheck`、`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。 - 关联文档:`docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md`、`docs/【前端架构】宿主壳能力统一协议-2026-06-17.md`。