接入原生壳生命周期事件

新增 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

View File

@@ -1,6 +1,14 @@
import { StatusBar } from 'expo-status-bar';
import { useEffect, useMemo, useRef, useState } from 'react';
import { BackHandler, Linking, Platform, StyleSheet, View } from 'react-native';
import {
AppState,
type AppStateStatus,
BackHandler,
Linking,
Platform,
StyleSheet,
View,
} from 'react-native';
import type { WebViewMessageEvent } from 'react-native-webview';
import { WebView } from 'react-native-webview';
@@ -10,6 +18,7 @@ import {
resolveMobileHostCapabilities,
} from './src/mobileHostBridge';
import { buildMobileShellUrlFromDeepLink } from './src/mobileShellDeepLink';
import { lifecyclePayloadFromAppState } from './src/mobileShellLifecycle';
import {
resolveMobileShellExternalUrl,
shouldOpenInMobileShellWebView,
@@ -97,6 +106,27 @@ export default function App() {
return () => subscription.remove();
}, [canGoBack]);
useEffect(() => {
const sendLifecycleEvent = (state: AppStateStatus) => {
webViewRef.current?.injectJavaScript(
buildHostBridgeMessageScript({
bridge: 'GenarrativeHostBridge',
version: 1,
event: 'app.lifecycle',
payload: lifecyclePayloadFromAppState(state),
}),
);
};
const subscription = AppState.addEventListener(
'change',
sendLifecycleEvent,
);
sendLifecycleEvent(AppState.currentState);
return () => subscription.remove();
}, []);
const handleMessage = (event: WebViewMessageEvent) => {
void handleMobileHostBridgeMessage(event.nativeEvent.data, (response) => {
webViewRef.current?.injectJavaScript(

View File

@@ -75,6 +75,7 @@ for (const capability of iosMobileCapabilities) {
const switchCase = `case '${capability}':`;
if (
capability !== 'host.events' &&
capability !== 'app.lifecycle' &&
capability !== 'navigation.canGoBack' &&
!bridgeSource.includes(switchCase)
) {
@@ -116,6 +117,8 @@ for (const snippet of [
"Linking.addEventListener('url'",
'buildMobileShellUrlFromDeepLink',
'configureMobileHostBridgeNavigation',
'AppState.addEventListener',
'app.lifecycle',
'navigation.canGoBack',
'buildHostBridgeMessageScript',
]) {
@@ -156,6 +159,7 @@ for (const capability of [
'appearance.getColorScheme',
'share.open',
'share.setTarget',
'app.lifecycle',
'navigation.openNativePage',
'navigation.canGoBack',
'app.openExternalUrl',

View File

@@ -174,6 +174,7 @@ describe('handleMobileHostBridgeMessage', () => {
expect.arrayContaining([
'appearance.getColorScheme',
'host.events',
'app.lifecycle',
'navigation.canGoBack',
'app.setBadgeCount',
]),

View File

@@ -48,6 +48,7 @@ export const MOBILE_HOST_CAPABILITIES: HostBridgeCapability[] = [
'host.getRuntime',
'appearance.getColorScheme',
'host.events',
'app.lifecycle',
'share.open',
'share.setTarget',
'navigation.openNativePage',

View File

@@ -0,0 +1,28 @@
import { describe, expect, test } from 'vitest';
import { lifecyclePayloadFromAppState } from './mobileShellLifecycle';
describe('mobileShellLifecycle', () => {
test('把 React Native AppState 映射为统一 HostBridge 生命周期状态', () => {
expect(lifecyclePayloadFromAppState('active')).toEqual({
state: 'active',
focused: true,
nativeState: 'active',
});
expect(lifecyclePayloadFromAppState('background')).toEqual({
state: 'background',
focused: false,
nativeState: 'background',
});
expect(lifecyclePayloadFromAppState('inactive')).toEqual({
state: 'inactive',
focused: false,
nativeState: 'inactive',
});
expect(lifecyclePayloadFromAppState('unknown')).toEqual({
state: 'inactive',
focused: false,
nativeState: 'unknown',
});
});
});

View File

@@ -0,0 +1,13 @@
import type { AppStateStatus } from 'react-native';
export function lifecyclePayloadFromAppState(state: AppStateStatus) {
return {
state: state === 'active'
? 'active'
: state === 'background'
? 'background'
: 'inactive',
focused: state === 'active',
nativeState: state,
};
}