收紧移动壳事件注入失败边界

移动壳 HostBridge 事件注入捕获 WebView 同步异常并记录稳定日志

移动壳 ShellApp 测试覆盖事件注入失败不崩溃

移动壳配置检查反查事件注入失败边界

共享决策记录补充移动事件注入失败约定
This commit is contained in:
2026-06-20 15:46:54 +08:00
parent cf01ba3097
commit 67954c54de
4 changed files with 54 additions and 3 deletions
@@ -1379,6 +1379,8 @@ for (const snippet of [
'injectLifecycleEvent',
'injectNetworkStatusEvent',
'logMobileHostEventFailure',
'try {',
'logMobileHostEventFailure(event, error)',
"logMobileHostEventFailure('network.statusChanged', error)",
'handleWebViewLoad',
'onLoad={handleWebViewLoad}',
@@ -2435,6 +2437,9 @@ for (const snippet of [
"lastHostBridgeEvent('navigation.canGoBack')",
"shellHarness.appStateListeners[0]?.('background')",
'shellHarness.networkListeners[0]?.({',
'injectJavaScriptError',
"test('host event injection failures are logged without crashing the shell'",
"new Error('webview injection failed')",
"type: 'genarrative.mobile.historyState'",
'mobile host event failed for network.statusChanged',
'external WebView navigation native failures stay outside the WebView',
@@ -15,18 +15,21 @@ const shellHarness = vi.hoisted(() => {
const appStateListeners = [] as Array<(state: string) => void>;
const webViewProps = { current: null as Record<string, unknown> | null };
const cameraViewProps = { current: null as Record<string, unknown> | null };
const injectJavaScriptError = { current: null as Error | null };
const injectedScripts = [] as string[];
const networkListeners = [] as Array<(state: Record<string, unknown>) => void>;
return {
appStateListeners,
cameraViewProps,
injectJavaScriptError,
injectedScripts,
networkListeners,
reset() {
appStateListeners.length = 0;
webViewProps.current = null;
cameraViewProps.current = null;
injectJavaScriptError.current = null;
injectedScripts.length = 0;
networkListeners.length = 0;
},
@@ -158,6 +161,9 @@ vi.mock('react-native-webview', () => ({
React.useImperativeHandle(ref, () => ({
goBack: vi.fn(),
injectJavaScript: (script: string) => {
if (shellHarness.injectJavaScriptError.current) {
throw shellHarness.injectJavaScriptError.current;
}
shellHarness.injectedScripts.push(script);
},
reload: vi.fn(),
@@ -372,6 +378,34 @@ describe('ShellApp HostBridge event injection', () => {
});
});
test('host event injection failures are logged without crashing the shell', async () => {
const ShellApp = await importShellApp();
render(<ShellApp />);
await waitFor(() => {
expect(shellHarness.networkListeners.length).toBeGreaterThan(0);
});
const injectionError = new Error('webview injection failed');
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
shellHarness.injectJavaScriptError.current = injectionError;
expect(() => {
shellHarness.networkListeners[0]?.({
isConnected: false,
isInternetReachable: false,
type: 'NONE',
});
}).not.toThrow();
expect(warnSpy).toHaveBeenCalledWith(
'mobile host event failed for network.statusChanged',
injectionError,
);
warnSpy.mockRestore();
});
test('native and H5 navigation state inject combined navigation.canGoBack events', async () => {
const ShellApp = await importShellApp();
render(<ShellApp />);
+7 -3
View File
@@ -124,9 +124,13 @@ export default function ShellApp() {
}, []);
const injectHostBridgeEvent = useCallback(
(event: HostBridgeEventName, payload: unknown) => {
webViewRef.current?.injectJavaScript(
buildHostBridgeEventScript(event, payload),
);
try {
webViewRef.current?.injectJavaScript(
buildHostBridgeEventScript(event, payload),
);
} catch (error) {
logMobileHostEventFailure(event, error);
}
},
[],
);
@@ -16,6 +16,14 @@
---
## 2026-06-20 移动 HostBridge 事件注入失败边界
- 背景:Expo 移动壳通过 WebView `injectJavaScript``app.lifecycle``network.statusChanged``navigation.canGoBack` 等宿主事件回放给 H5;如果 WebView 进程切换、页面卸载或注入同步失败,壳层不能因为宿主事件回放异常而崩溃。
- 决策:移动壳所有 HostBridge 事件注入必须统一经过 `injectHostBridgeEvent`,该函数捕获同步注入异常并用 `logMobileHostEventFailure(event, 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`
## 2026-06-20 桌面图片拖拽坐标非负边界
- 背景:Tauri 桌面壳通过系统拖拽事件向 H5 发送 `file.imageDropped`,拖拽坐标来自窗口事件;窗口边缘或平台差异可能产生负数或小数坐标,H5 只负责校验有限 number,不负责裁剪桌面系统坐标。