接入移动壳受控路由导航

移动 HostBridge 声明 navigation.openNativePage 并限制为同源 H5 route

Expo WebView 将受控导航请求切换为新的 WebView URL

补充移动壳 HostBridge 与导航解析测试和配置守卫

更新宿主壳方案、统一协议和团队共享决策记录
This commit is contained in:
2026-06-17 22:15:31 +08:00
parent 02a475d652
commit 080ebaedfd
9 changed files with 249 additions and 6 deletions
+39 -1
View File
@@ -13,20 +13,35 @@ import {
type HostBridgeMethod,
type HostBridgeRequest,
type HostBridgeResponse,
type NavigateNativePagePayload,
type OpenExternalUrlPayload,
type ShareOpenPayload,
} from '../../../packages/shared/src/contracts/hostBridge';
import { resolveMobileShellWebViewUrl } from './mobileShellNavigation';
export const MOBILE_HOST_CAPABILITIES: HostBridgeCapability[] = [
'host.getRuntime',
'share.open',
'share.setTarget',
'navigation.openNativePage',
'app.openExternalUrl',
'clipboard.writeText',
'haptics.impact',
];
export type MobileHostBridgeNavigation = {
allowedOrigin: string;
openWebViewUrl: (url: string) => void;
};
let currentShareTarget: unknown = null;
let navigation: MobileHostBridgeNavigation | null = null;
export function configureMobileHostBridgeNavigation(
nextNavigation: MobileHostBridgeNavigation | null,
) {
navigation = nextNavigation;
}
function unsupported(method: HostBridgeMethod): HostBridgeError {
return {
@@ -141,6 +156,28 @@ async function openShare(payload: unknown) {
return true;
}
function openNativePage(payload: unknown) {
if (!navigation) {
throw unsupported('navigation.openNativePage');
}
const url = (payload as NavigateNativePagePayload | undefined)?.url;
if (typeof url !== 'string') {
throw invalidRequest('url is required');
}
const webViewUrl = resolveMobileShellWebViewUrl(
url,
navigation.allowedOrigin,
);
if (!webViewUrl) {
throw invalidRequest('url must be an allowed same-origin web path');
}
navigation.openWebViewUrl(webViewUrl);
return true;
}
async function handleRequest(request: HostBridgeRequest) {
switch (request.method) {
case 'host.getRuntime':
@@ -165,9 +202,10 @@ async function handleRequest(request: HostBridgeRequest) {
? (request.payload as { target?: unknown }).target
: null;
return ok(request, true);
case 'navigation.openNativePage':
return ok(request, openNativePage(request.payload));
case 'auth.requestLogin':
case 'payment.request':
case 'navigation.openNativePage':
return failure(request, unsupported(request.method));
default:
return failure(request, unsupported(request.method));