收紧移动壳消息注入失败边界
移动壳 HostBridge 响应和事件注入统一捕获 WebView 同步异常 移动壳 ShellApp 测试覆盖响应注入失败不崩溃 移动壳配置检查反查统一消息注入边界 共享决策记录补充移动消息注入失败约定
This commit is contained in:
@@ -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', () => {
|
||||
|
||||
@@ -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`。
|
||||
|
||||
Reference in New Issue
Block a user