From 941e7d979af7cacb70ba2145cb2ea9aa62c362fe Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 20 Jun 2026 01:43:35 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E5=8F=A3=E7=A7=BB=E5=8A=A8=E5=AF=BC?= =?UTF-8?q?=E8=88=AA=E5=93=8D=E5=BA=94=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 Expo 外链打开、WebView 刷新和受控导航成功响应包装收口到 navigation 模块 让移动 dispatch 只委托导航请求 同步移动壳配置门禁和共享记忆 --- apps/mobile-shell/scripts/check-config.mjs | 13 ++++++++++--- apps/mobile-shell/src/host-bridge/dispatch.ts | 9 +++------ apps/mobile-shell/src/host-bridge/navigation.ts | 17 ++++++++++------- .../shared-memory/decision-log.md | 2 +- 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index cdccb938f..8a4cf7aa9 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -1952,9 +1952,12 @@ for (const snippet of [ } if ( - !dispatchSource.includes('openMobileHostBridgeExternalUrl(request.payload)') || - !dispatchSource.includes('openMobileHostBridgeNativePage(navigation, request.payload)') || - !dispatchSource.includes('reloadMobileHostBridgeWebView(navigation)') || + !dispatchSource.includes('openMobileHostBridgeExternalUrl(request)') || + !dispatchSource.includes('openMobileHostBridgeNativePage(request, navigation)') || + !dispatchSource.includes('reloadMobileHostBridgeWebView(request, navigation)') || + dispatchSource.includes('ok(request, await openMobileHostBridgeExternalUrl') || + dispatchSource.includes('ok(request, reloadMobileHostBridgeWebView') || + dispatchSource.includes('ok(\\n request,\\n openMobileHostBridgeNativePage') || dispatchSource.includes("from 'expo-linking'") || dispatchSource.includes('Linking.') || dispatchSource.includes('openMobileShellExternalNavigation') || @@ -1966,13 +1969,17 @@ if ( } for (const snippet of [ 'openMobileHostBridgeExternalUrl', + 'request: HostBridgeRequest', "import * as Linking from 'expo-linking'", 'openMobileShellExternalNavigation(Linking, externalUrlPayload.url)', + '(request.payload as OpenExternalUrlPayload | undefined)?.url', 'normalizeHostBridgeExternalUrlPayload', 'openMobileHostBridgeNativePage', + '(request.payload as NavigateNativePagePayload | undefined)?.url', 'resolveMobileShellWebViewUrl', 'buildMobileShellUrl(webViewUrl, navigation.urlOptions)', 'reloadMobileHostBridgeWebView', + 'ok(request, true)', ]) { if (!hostBridgeNavigationSource.includes(snippet)) { throw new Error(`mobile shell navigation module is missing ${snippet}`); diff --git a/apps/mobile-shell/src/host-bridge/dispatch.ts b/apps/mobile-shell/src/host-bridge/dispatch.ts index ab137b053..72cb7fb90 100644 --- a/apps/mobile-shell/src/host-bridge/dispatch.ts +++ b/apps/mobile-shell/src/host-bridge/dispatch.ts @@ -54,9 +54,9 @@ export async function dispatchMobileHostBridgeRequest( case 'appearance.getColorScheme': return getMobileHostBridgeAppearanceColorScheme(request); case 'app.openExternalUrl': - return ok(request, await openMobileHostBridgeExternalUrl(request.payload)); + return openMobileHostBridgeExternalUrl(request); case 'app.reloadWebView': - return ok(request, reloadMobileHostBridgeWebView(navigation)); + return reloadMobileHostBridgeWebView(request, navigation); case 'network.status': return getMobileHostBridgeNetworkStatus(request); case 'clipboard.writeText': @@ -92,10 +92,7 @@ export async function dispatchMobileHostBridgeRequest( case 'share.setTarget': return setMobileHostBridgeShareTarget(request); case 'navigation.openNativePage': - return ok( - request, - openMobileHostBridgeNativePage(navigation, request.payload), - ); + return openMobileHostBridgeNativePage(request, navigation); case 'auth.requestLogin': case 'payment.request': return failure(request, unsupported(request.method)); diff --git a/apps/mobile-shell/src/host-bridge/navigation.ts b/apps/mobile-shell/src/host-bridge/navigation.ts index 597d6434f..cffc109d3 100644 --- a/apps/mobile-shell/src/host-bridge/navigation.ts +++ b/apps/mobile-shell/src/host-bridge/navigation.ts @@ -2,6 +2,7 @@ import * as Linking from 'expo-linking'; import { type HostBridgeError, + type HostBridgeRequest, type NavigateNativePagePayload, normalizeHostBridgeExternalUrlPayload, type OpenExternalUrlPayload, @@ -15,14 +16,15 @@ import { buildMobileShellUrl } from '../shell/url'; import { type MobileHostBridgeNavigation, invalidRequest, + ok, unsupported, } from './protocol'; export async function openMobileHostBridgeExternalUrl( - payload: unknown, + request: HostBridgeRequest, ) { const externalUrlPayload = normalizeHostBridgeExternalUrlPayload( - (payload as OpenExternalUrlPayload | undefined)?.url, + (request.payload as OpenExternalUrlPayload | undefined)?.url, ); if (!externalUrlPayload) { throw invalidRequest('url must use an allowed external protocol'); @@ -37,18 +39,18 @@ export async function openMobileHostBridgeExternalUrl( } satisfies HostBridgeError; } - return true; + return ok(request, true); } export function openMobileHostBridgeNativePage( + request: HostBridgeRequest, navigation: MobileHostBridgeNavigation | null, - payload: unknown, ) { if (!navigation) { throw unsupported('navigation.openNativePage'); } - const url = (payload as NavigateNativePagePayload | undefined)?.url; + const url = (request.payload as NavigateNativePagePayload | undefined)?.url; if (typeof url !== 'string') { throw invalidRequest('url is required'); } @@ -64,10 +66,11 @@ export function openMobileHostBridgeNativePage( navigation.openWebViewUrl( buildMobileShellUrl(webViewUrl, navigation.urlOptions), ); - return true; + return ok(request, true); } export function reloadMobileHostBridgeWebView( + request: HostBridgeRequest, navigation: MobileHostBridgeNavigation | null, ) { if (!navigation) { @@ -75,5 +78,5 @@ export function reloadMobileHostBridgeWebView( } navigation.reloadWebView(); - return true; + return ok(request, true); } diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index cdc32d777..c38325140 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -2794,7 +2794,7 @@ ## 2026-06-19 原生壳导航桥接边界 - 背景:`app.openExternalUrl`、`navigation.openNativePage` 和 `app.reloadWebView` 已是受控 HostBridge 能力,但移动端和桌面端 `dispatch` 仍直接承接外链归一、同源 H5 跳转、宿主上下文补写和 WebView 刷新细节,后续继续补壳能力时容易让分发层重新变厚。 -- 决策:Expo 移动壳新增 `apps/mobile-shell/src/host-bridge/navigation.ts`,统一承接 HostBridge 外链打开、受控 H5 跳转和 WebView 刷新,底层继续复用 `src/shell/navigation.ts` 与 `src/shell/url.ts`;Tauri 桌面壳新增 `apps/desktop-shell/src-tauri/src/host_bridge/navigation.rs`,统一承接外链打开、同源 H5 跳转和主窗口刷新,底层继续复用 `shell::navigation` 的 URL 归一和宿主上下文补写。两端 `dispatch` 只保留 method 委托和响应包装,配置检查会拒绝分发层直接调用外链 opener、URL 归一或 WebView navigate / reload 细节。 +- 决策:Expo 移动壳新增 `apps/mobile-shell/src/host-bridge/navigation.ts`,统一承接 HostBridge 外链打开、受控 H5 跳转、WebView 刷新和成功响应包装,底层继续复用 `src/shell/navigation.ts` 与 `src/shell/url.ts`;Tauri 桌面壳新增 `apps/desktop-shell/src-tauri/src/host_bridge/navigation.rs`,统一承接外链打开、同源 H5 跳转和主窗口刷新,底层继续复用 `shell::navigation` 的 URL 归一和宿主上下文补写。两端 `dispatch` 只保留 method 委托,配置检查会拒绝分发层直接调用外链 opener、URL 归一、WebView navigate / reload 细节或包装导航成功响应。 - 影响范围:`apps/mobile-shell/src/host-bridge/navigation.ts`、`apps/mobile-shell/src/host-bridge/dispatch.ts`、`apps/mobile-shell/scripts/check-config.mjs`、`apps/desktop-shell/src-tauri/src/host_bridge/navigation.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs`、`apps/desktop-shell/scripts/check-config.mjs`、`scripts/check-native-shells.mjs`、宿主壳能力统一协议文档、Expo / Tauri HostBridge 方案文档。 - 验证方式:`npm run mobile-shell:typecheck`、`npm run mobile-shell:test -- src/host-bridge/bridge.test.ts`、`npm run desktop-shell:typecheck`、`cargo test --manifest-path apps/desktop-shell/src-tauri/Cargo.toml`、`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。