接入原生壳生命周期事件

新增 app.lifecycle HostBridge 能力与 H5 订阅入口

Expo 壳通过 React Native AppState 注入真实前后台状态

Tauri 壳通过主窗口 focus 和 blur 注入真实激活状态

更新壳能力漂移检查、测试和架构文档
This commit is contained in:
2026-06-18 02:16:47 +08:00
parent 45eec17007
commit 346368f0e7
16 changed files with 299 additions and 6 deletions
@@ -29,6 +29,7 @@ import {
setHostAppBadgeCount,
setHostAppTitle,
setHostShareTarget,
subscribeHostAppLifecycle,
subscribeHostRuntimeChange,
writeHostClipboardText,
} from './hostBridge';
@@ -140,6 +141,82 @@ describe('hostBridge', () => {
).toBe(false);
});
test('订阅原生 App 生命周期事件并归一化 payload', () => {
const listener = vi.fn();
window.history.replaceState(
null,
'',
nativeAppPath(['app.lifecycle']),
);
window.ReactNativeWebView = {
postMessage: vi.fn(),
};
const unsubscribe = subscribeHostAppLifecycle(listener);
window.dispatchEvent(
new MessageEvent('message', {
data: JSON.stringify({
bridge: 'GenarrativeHostBridge',
version: 1,
event: 'app.lifecycle',
payload: {
state: 'extension',
focused: true,
nativeState: 'extension',
},
}),
}),
);
unsubscribe();
window.dispatchEvent(
new MessageEvent('message', {
data: JSON.stringify({
bridge: 'GenarrativeHostBridge',
version: 1,
event: 'app.lifecycle',
payload: {
state: 'active',
focused: true,
nativeState: 'active',
},
}),
}),
);
expect(listener).toHaveBeenCalledTimes(1);
expect(listener).toHaveBeenCalledWith({
state: 'inactive',
focused: true,
nativeState: 'extension',
});
});
test('未声明生命周期能力时不订阅原生事件', () => {
const listener = vi.fn();
window.history.replaceState(null, '', nativeAppPath());
window.ReactNativeWebView = {
postMessage: vi.fn(),
};
const unsubscribe = subscribeHostAppLifecycle(listener);
window.dispatchEvent(
new MessageEvent('message', {
data: JSON.stringify({
bridge: 'GenarrativeHostBridge',
version: 1,
event: 'app.lifecycle',
payload: {
state: 'active',
focused: true,
},
}),
}),
);
unsubscribe();
expect(listener).not.toHaveBeenCalled();
});
test('从真实宿主 runtime 回读能力并通知订阅者', async () => {
const listener = vi.fn();
const unsubscribe = subscribeHostRuntimeChange(listener);
+29
View File
@@ -1,5 +1,6 @@
import type {
AppearanceColorSchemeResult,
AppLifecycleEventPayload,
FileExportImagePayload,
FileExportImageResult,
FileExportTextPayload,
@@ -17,6 +18,7 @@ import {
normalizeHostBridgeBadgeCount,
normalizeHostBridgeColorScheme,
normalizeHostBridgeExternalUrl,
normalizeHostBridgeLifecycleState,
} from '../../../packages/shared/src/contracts/hostBridge';
import type {
WechatMiniProgramPayParams,
@@ -25,6 +27,7 @@ import type {
import {
canUseNativeAppHostBridge,
requestNativeAppHostBridge,
subscribeNativeAppHostBridgeEvent,
} from './nativeAppHostBridge';
const WECHAT_JS_SDK_URL = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js';
@@ -99,6 +102,8 @@ export type HostExternalUrlRequest = OpenExternalUrlPayload;
export type HostAppearanceColorSchemeSnapshot = AppearanceColorSchemeResult;
export type HostAppLifecycleSnapshot = AppLifecycleEventPayload;
const HOST_RUNTIME_REFRESH_TIMEOUT_MS = 3000;
let cachedNativeHostRuntime: HostBridgeRuntimeResult | null = null;
@@ -786,3 +791,27 @@ export async function getHostAppearanceColorScheme() {
return false;
}
}
export function subscribeHostAppLifecycle(
listener: (payload: HostAppLifecycleSnapshot) => void,
) {
if (!canUseNativeHostCapability('app.lifecycle')) {
return () => undefined;
}
return subscribeNativeAppHostBridgeEvent<AppLifecycleEventPayload>(
'app.lifecycle',
(payload) => {
const state = normalizeHostBridgeLifecycleState(payload?.state);
listener({
state,
focused: typeof payload?.focused === 'boolean'
? payload.focused
: state === 'active',
...(typeof payload?.nativeState === 'string'
? { nativeState: payload.nativeState }
: {}),
});
},
);
}