保持移动壳主动导航宿主上下文

HostBridge 主动导航同源页面时重新附加 native_app 宿主参数

补充移动壳导航测试覆盖平台版本和能力清单

同步 Expo/Tauri 宿主壳方案和团队决策记录
This commit is contained in:
2026-06-18 19:12:54 +08:00
parent ab27815810
commit b96be76a34
6 changed files with 37 additions and 5 deletions
@@ -21,6 +21,7 @@ import {
type HostBridgeRequest,
type HostBridgeResponse,
} from '../../../../packages/shared/src/contracts/hostBridge';
import type { MobileShellUrlOptions } from '../shell/url';
import {
configureMobileHostBridgeNavigation,
handleMobileHostBridgeMessage,
@@ -46,6 +47,12 @@ const DENIED_NOTIFICATION_PERMISSION = {
let requestSequence = 0;
const TEST_MOBILE_URL_OPTIONS: MobileShellUrlOptions = {
platform: 'ios',
hostVersion: '0.1.0',
capabilities: ['host.getRuntime', 'share.open'],
};
vi.mock('expo-clipboard', () => ({
getStringAsync: vi.fn(),
setStringAsync: vi.fn(),
@@ -388,6 +395,7 @@ describe('handleMobileHostBridgeMessage', () => {
const openWebViewUrl = vi.fn();
configureMobileHostBridgeNavigation({
allowedOrigin: 'https://app.genarrative.world',
urlOptions: TEST_MOBILE_URL_OPTIONS,
openWebViewUrl,
reloadWebView: vi.fn(),
});
@@ -399,14 +407,24 @@ describe('handleMobileHostBridgeMessage', () => {
);
expectOk(response);
expect(openWebViewUrl).toHaveBeenCalledWith(
'https://app.genarrative.world/works/detail?work=PZ-1',
const openedUrl = new URL(openWebViewUrl.mock.calls[0]?.[0] as string);
expect(openedUrl.origin).toBe('https://app.genarrative.world');
expect(openedUrl.pathname).toBe('/works/detail');
expect(openedUrl.searchParams.get('work')).toBe('PZ-1');
expect(openedUrl.searchParams.get('clientRuntime')).toBe('native_app');
expect(openedUrl.searchParams.get('hostShell')).toBe('expo_mobile');
expect(openedUrl.searchParams.get('hostPlatform')).toBe('ios');
expect(openedUrl.searchParams.get('hostVersion')).toBe('0.1.0');
expect(openedUrl.searchParams.get('bridgeVersion')).toBe('1');
expect(openedUrl.searchParams.get('hostCapabilities')).toBe(
'host.getRuntime,share.open',
);
});
test('navigation.openNativePage 拒绝外域目标', async () => {
configureMobileHostBridgeNavigation({
allowedOrigin: 'https://app.genarrative.world',
urlOptions: TEST_MOBILE_URL_OPTIONS,
openWebViewUrl: vi.fn(),
reloadWebView: vi.fn(),
});
@@ -475,6 +493,7 @@ describe('handleMobileHostBridgeMessage', () => {
const reloadWebView = vi.fn();
configureMobileHostBridgeNavigation({
allowedOrigin: 'https://app.genarrative.world',
urlOptions: TEST_MOBILE_URL_OPTIONS,
openWebViewUrl: vi.fn(),
reloadWebView,
});
@@ -46,6 +46,7 @@ import {
unsupported,
} from './protocol';
import { openShare } from './share';
import { buildMobileShellUrl } from '../shell/url';
const LOCAL_NOTIFICATION_CHANNEL_ID = 'genarrative-local';
@@ -222,7 +223,9 @@ function openNativePage(payload: unknown) {
throw invalidRequest('url must be an allowed same-origin web path');
}
navigation.openWebViewUrl(webViewUrl);
navigation.openWebViewUrl(
buildMobileShellUrl(webViewUrl, navigation.urlOptions),
);
return true;
}
@@ -8,11 +8,13 @@ import {
isHostBridgeMethod,
normalizeHostBridgeRequestId,
} from '../../../../packages/shared/src/contracts/hostBridge';
import type { MobileShellUrlOptions } from '../shell/url';
export const HOST_BRIDGE_RESPONSE_CACHE_MAX = 128;
export type MobileHostBridgeNavigation = {
allowedOrigin: string;
urlOptions: MobileShellUrlOptions;
openWebViewUrl: (url: string) => void;
reloadWebView: () => void;
};
+2 -1
View File
@@ -89,6 +89,7 @@ export default function ShellApp() {
useEffect(() => {
configureMobileHostBridgeNavigation({
allowedOrigin: allowedWebOrigin,
urlOptions,
openWebViewUrl(url) {
setWebUrl(url);
},
@@ -96,7 +97,7 @@ export default function ShellApp() {
});
return () => configureMobileHostBridgeNavigation(null);
}, [allowedWebOrigin, reloadCurrentWebView]);
}, [allowedWebOrigin, reloadCurrentWebView, urlOptions]);
useEffect(() => {
let disposed = false;