Files
Genarrative/apps/mobile-shell/App.tsx
kdletters 9b7da18879 新增 Expo 与 Tauri 原生宿主壳
新增 HostBridge 原生宿主契约和 H5 native_app transport

新增 Expo React Native 移动壳并收紧 WebView 外链边界

新增 Tauri 桌面壳并用 capability 收口受控命令

更新宿主壳方案、文档索引和共享记忆
2026-06-17 21:39:34 +08:00

92 lines
2.6 KiB
TypeScript

import { StatusBar } from 'expo-status-bar';
import { useEffect, useMemo, useRef, useState } from 'react';
import { BackHandler, Linking, Platform, StyleSheet, View } from 'react-native';
import type { WebViewMessageEvent } from 'react-native-webview';
import { WebView } from 'react-native-webview';
import {
handleMobileHostBridgeMessage,
MOBILE_HOST_CAPABILITIES,
} from './src/mobileHostBridge';
import { shouldOpenInMobileShellWebView } from './src/mobileShellNavigation';
import { buildMobileShellUrl } from './src/mobileShellUrl';
const defaultWebUrl = 'http://127.0.0.1:3000/';
export default function App() {
const webViewRef = useRef<WebView>(null);
const [canGoBack, setCanGoBack] = useState(false);
const webUrl = useMemo(
() =>
buildMobileShellUrl(
process.env.EXPO_PUBLIC_GENARRATIVE_WEB_URL || defaultWebUrl,
{
platform: Platform.OS === 'ios' ? 'ios' : 'android',
hostVersion: '0.1.0',
capabilities: MOBILE_HOST_CAPABILITIES,
},
),
[],
);
const allowedWebOrigin = useMemo(() => new URL(webUrl).origin, [webUrl]);
useEffect(() => {
const subscription = BackHandler.addEventListener(
'hardwareBackPress',
() => {
if (!canGoBack) {
return false;
}
webViewRef.current?.goBack();
return true;
},
);
return () => subscription.remove();
}, [canGoBack]);
const handleMessage = (event: WebViewMessageEvent) => {
void handleMobileHostBridgeMessage(event.nativeEvent.data, (response) => {
webViewRef.current?.injectJavaScript(
`window.dispatchEvent(new MessageEvent('message', { data: ${JSON.stringify(
JSON.stringify(response),
)} })); true;`,
);
});
};
const handleShouldStartLoad = (request: { url: string }) => {
if (shouldOpenInMobileShellWebView(request.url, allowedWebOrigin)) {
return true;
}
void Linking.openURL(request.url).catch(() => undefined);
return false;
};
return (
<View style={styles.root}>
<StatusBar style="auto" />
<WebView
ref={webViewRef}
source={{ uri: webUrl }}
javaScriptEnabled
domStorageEnabled
originWhitelist={[allowedWebOrigin]}
onMessage={handleMessage}
onShouldStartLoadWithRequest={handleShouldStartLoad}
onNavigationStateChange={(event) => setCanGoBack(event.canGoBack)}
setSupportMultipleWindows={false}
/>
</View>
);
}
const styles = StyleSheet.create({
root: {
flex: 1,
backgroundColor: '#fffdf9',
},
});