收紧移动壳消息注入失败边界

移动壳 HostBridge 响应和事件注入统一捕获 WebView 同步异常

移动壳 ShellApp 测试覆盖响应注入失败不崩溃

移动壳配置检查反查统一消息注入边界

共享决策记录补充移动消息注入失败约定
This commit is contained in:
2026-06-20 15:54:14 +08:00
parent 67954c54de
commit 4f95e29895
4 changed files with 88 additions and 30 deletions
+17 -6
View File
@@ -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',
@@ -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(<ShellApp />);
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', () => {
+26 -21
View File
@@ -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);
});
};
@@ -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`