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

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;
@@ -2485,3 +2485,10 @@
- 决策:`SquareHoleResultView` 图片槽位弹窗在 `native_app` 且宿主声明 `file.importImage` 时优先调用 `importHostImageFile()`,把宿主返回的图片内容副本转换为 `data:<mime>;base64,<data>` 并写回当前槽位 `imageSrc`。该动作继续走现有 result edit state、自动保存、试玩和发布链路,不新增后端上传路径,不暴露设备 URI、本机绝对路径或通用文件系统;普通浏览器、小程序和未声明能力的裁剪壳继续使用原浏览器文件输入。
- 影响范围:`src/components/square-hole-result/SquareHoleResultView.tsx``src/services/host-bridge/hostBridge.ts`、方洞玩法链路文档与 Expo / Tauri HostBridge 方案文档。
- 验证方式:`npm run test -- src/components/square-hole-result/SquareHoleResultView.test.tsx``npm run typecheck -- --pretty false``npm run check:encoding``npm run check:native-shells``git diff --check`
## 2026-06-18 移动壳主动导航保留宿主上下文
- 背景:Expo 移动壳启动 URL 和 deep link 已经会给 H5 追加 `native_app``expo_mobile`、平台、版本和 capability query;但 H5 通过 HostBridge 调用 `navigation.openNativePage` 主动跳转同源 route 时,壳层只把裸同源 URL 交给 WebView,目标页首屏可能短暂或持续按普通浏览器运行态识别。
- 决策:`navigation.openNativePage` 仍只接受同源 H5 route,不新增真实原生页面、不放宽外域导航;通过校验后的目标 URL 在进入 WebView 前统一调用 `buildMobileShellUrl(...)` 重新附加当前 `MobileShellUrlOptions`,确保主动导航后的页面继续带 `clientRuntime=native_app``hostShell=expo_mobile`、真实平台、宿主版本和当前 capability 清单。
- 影响范围:`apps/mobile-shell/src/host-bridge/``apps/mobile-shell/src/shell/ShellApp.tsx`、Expo / Tauri HostBridge 方案文档。
- 验证方式:`npm run mobile-shell:test``npm run mobile-shell:typecheck``npm run check:native-shells``npm run typecheck -- --pretty false``npm run check:encoding``git diff --check`
File diff suppressed because one or more lines are too long