收口原生导航桥接边界
将 Expo 外链打开和受控 WebView 导航收口到 navigation 模块 将 Tauri 外链打开和主窗口导航刷新收口到 navigation 模块 同步原生壳结构门禁、架构文档和共享记忆
This commit is contained in:
@@ -86,6 +86,14 @@ const desktopHostBridgeDispatchSource = fs.readFileSync(
|
||||
desktopHostBridgeDispatchPath,
|
||||
'utf8',
|
||||
);
|
||||
const desktopHostBridgeNavigationPath = new URL(
|
||||
'../src-tauri/src/host_bridge/navigation.rs',
|
||||
import.meta.url,
|
||||
);
|
||||
const desktopHostBridgeNavigationSource = fs.readFileSync(
|
||||
desktopHostBridgeNavigationPath,
|
||||
'utf8',
|
||||
);
|
||||
const desktopHostBridgeNotificationsPath = new URL(
|
||||
'../src-tauri/src/host_bridge/notifications.rs',
|
||||
import.meta.url,
|
||||
@@ -216,6 +224,7 @@ const expectedDesktopHostBridgeRustFiles = [
|
||||
'dispatch.rs',
|
||||
'files.rs',
|
||||
'mod.rs',
|
||||
'navigation.rs',
|
||||
'notifications.rs',
|
||||
'protocol.rs',
|
||||
'share.rs',
|
||||
@@ -1650,13 +1659,38 @@ if (
|
||||
}
|
||||
|
||||
if (
|
||||
!desktopHostBridgeDispatchSource.includes('open_normalized_desktop_external_url(&app, url)') ||
|
||||
!desktopHostBridgeDispatchSource.includes(
|
||||
'open_desktop_host_bridge_external_url(&app, &request)',
|
||||
) ||
|
||||
!desktopHostBridgeDispatchSource.includes(
|
||||
'open_desktop_host_bridge_native_page(&app, &request)',
|
||||
) ||
|
||||
!desktopHostBridgeDispatchSource.includes(
|
||||
'reload_desktop_host_bridge_webview(&app, &request)',
|
||||
) ||
|
||||
desktopHostBridgeDispatchSource.includes('open_normalized_desktop_external_url(&app, url)') ||
|
||||
desktopHostBridgeDispatchSource.includes('normalize_external_url') ||
|
||||
desktopHostBridgeDispatchSource.includes('normalize_native_page_url') ||
|
||||
desktopHostBridgeDispatchSource.includes('app.opener().open_url(')
|
||||
) {
|
||||
throw new Error(
|
||||
'desktop shell app.openExternalUrl must use the shared external opener helper',
|
||||
'desktop shell navigation HostBridge methods must delegate to navigation module',
|
||||
);
|
||||
}
|
||||
for (const snippet of [
|
||||
'open_desktop_host_bridge_external_url',
|
||||
'normalize_external_url',
|
||||
'open_normalized_desktop_external_url(app, url)',
|
||||
'open_desktop_host_bridge_native_page',
|
||||
'normalize_native_page_url',
|
||||
'window.navigate(url)',
|
||||
'reload_desktop_host_bridge_webview',
|
||||
'window.reload()',
|
||||
]) {
|
||||
if (!desktopHostBridgeNavigationSource.includes(snippet)) {
|
||||
throw new Error(`desktop shell navigation module is missing ${snippet}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (config.build?.frontendDist !== '../../../dist') {
|
||||
throw new Error('desktop shell must package the root H5 dist');
|
||||
@@ -1828,6 +1862,7 @@ const expectedRustHostBridgeFiles = [
|
||||
'dispatch.rs',
|
||||
'files.rs',
|
||||
'mod.rs',
|
||||
'navigation.rs',
|
||||
'notifications.rs',
|
||||
'protocol.rs',
|
||||
'share.rs',
|
||||
|
||||
@@ -7,16 +7,17 @@ use crate::host_bridge::files::{
|
||||
import_document_file_payload, import_image_file_payload, import_text_file_payload,
|
||||
write_export_bytes_file, write_export_text_file,
|
||||
};
|
||||
use crate::host_bridge::navigation::{
|
||||
open_desktop_host_bridge_external_url, open_desktop_host_bridge_native_page,
|
||||
reload_desktop_host_bridge_webview,
|
||||
};
|
||||
use crate::host_bridge::notifications::show_desktop_local_notification;
|
||||
use crate::host_bridge::protocol::{
|
||||
failed, ok, required_string_payload, validate_request, HostBridgeRequest, HostBridgeResponse,
|
||||
HostBridgeRuntime, HOST_BRIDGE_VERSION,
|
||||
};
|
||||
use crate::host_bridge::share::{share_text_from_request, DesktopShareState};
|
||||
use crate::shell::webview::{
|
||||
desktop_platform, normalize_external_url, normalize_native_page_url,
|
||||
open_normalized_desktop_external_url, resolve_desktop_network_status,
|
||||
};
|
||||
use crate::shell::webview::{desktop_platform, resolve_desktop_network_status};
|
||||
use serde_json::json;
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
@@ -67,56 +68,11 @@ pub(super) async fn execute_host_bridge_request(
|
||||
|
||||
match request.method.as_str() {
|
||||
"app.openExternalUrl" => {
|
||||
let url = match required_string_payload(&request, "url")
|
||||
.ok()
|
||||
.and_then(normalize_external_url)
|
||||
{
|
||||
Some(url) => url,
|
||||
None => {
|
||||
return failed(
|
||||
request.id,
|
||||
"invalid_request",
|
||||
"url must use an allowed external protocol",
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
match open_normalized_desktop_external_url(&app, url) {
|
||||
Ok(()) => ok(request.id, json!(true)),
|
||||
Err(error) => failed(request.id, "host_error", error.to_string()),
|
||||
}
|
||||
open_desktop_host_bridge_external_url(&app, &request)
|
||||
}
|
||||
"appearance.getColorScheme" => desktop_appearance_color_scheme(&app, &request),
|
||||
"navigation.openNativePage" => {
|
||||
let url = match required_string_payload(&request, "url")
|
||||
.ok()
|
||||
.and_then(normalize_native_page_url)
|
||||
{
|
||||
Some(url) => url,
|
||||
None => {
|
||||
return failed(
|
||||
request.id,
|
||||
"invalid_request",
|
||||
"url must use an allowed same-origin H5 route",
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
match app.get_webview_window("main") {
|
||||
Some(window) => match window.navigate(url) {
|
||||
Ok(()) => ok(request.id, json!(true)),
|
||||
Err(error) => failed(request.id, "host_error", error.to_string()),
|
||||
},
|
||||
None => failed(request.id, "host_error", "main window not found"),
|
||||
}
|
||||
}
|
||||
"app.reloadWebView" => match app.get_webview_window("main") {
|
||||
Some(window) => match window.reload() {
|
||||
Ok(()) => ok(request.id, json!(true)),
|
||||
Err(error) => failed(request.id, "host_error", error.to_string()),
|
||||
},
|
||||
None => failed(request.id, "host_error", "main window not found"),
|
||||
},
|
||||
"navigation.openNativePage" => open_desktop_host_bridge_native_page(&app, &request),
|
||||
"app.reloadWebView" => reload_desktop_host_bridge_webview(&app, &request),
|
||||
"clipboard.writeText" => {
|
||||
let text = match required_string_payload(&request, "text") {
|
||||
Ok(text) => text,
|
||||
|
||||
@@ -4,6 +4,7 @@ mod badge;
|
||||
mod clipboard;
|
||||
mod dispatch;
|
||||
pub(crate) mod files;
|
||||
mod navigation;
|
||||
mod notifications;
|
||||
pub(crate) mod protocol;
|
||||
mod share;
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
use crate::host_bridge::protocol::{
|
||||
failed, ok, required_string_payload, HostBridgeRequest, HostBridgeResponse,
|
||||
};
|
||||
use crate::shell::webview::{
|
||||
normalize_external_url, normalize_native_page_url, open_normalized_desktop_external_url,
|
||||
};
|
||||
use serde_json::json;
|
||||
use tauri::Manager;
|
||||
|
||||
pub(crate) fn open_desktop_host_bridge_external_url(
|
||||
app: &tauri::AppHandle,
|
||||
request: &HostBridgeRequest,
|
||||
) -> HostBridgeResponse {
|
||||
let url = match required_string_payload(request, "url")
|
||||
.ok()
|
||||
.and_then(normalize_external_url)
|
||||
{
|
||||
Some(url) => url,
|
||||
None => {
|
||||
return failed(
|
||||
request.id.clone(),
|
||||
"invalid_request",
|
||||
"url must use an allowed external protocol",
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
match open_normalized_desktop_external_url(app, url) {
|
||||
Ok(()) => ok(request.id.clone(), json!(true)),
|
||||
Err(error) => failed(request.id.clone(), "host_error", error.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn open_desktop_host_bridge_native_page(
|
||||
app: &tauri::AppHandle,
|
||||
request: &HostBridgeRequest,
|
||||
) -> HostBridgeResponse {
|
||||
let url = match required_string_payload(request, "url")
|
||||
.ok()
|
||||
.and_then(normalize_native_page_url)
|
||||
{
|
||||
Some(url) => url,
|
||||
None => {
|
||||
return failed(
|
||||
request.id.clone(),
|
||||
"invalid_request",
|
||||
"url must use an allowed same-origin H5 route",
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
match app.get_webview_window("main") {
|
||||
Some(window) => match window.navigate(url) {
|
||||
Ok(()) => ok(request.id.clone(), json!(true)),
|
||||
Err(error) => failed(request.id.clone(), "host_error", error.to_string()),
|
||||
},
|
||||
None => failed(request.id.clone(), "host_error", "main window not found"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn reload_desktop_host_bridge_webview(
|
||||
app: &tauri::AppHandle,
|
||||
request: &HostBridgeRequest,
|
||||
) -> HostBridgeResponse {
|
||||
match app.get_webview_window("main") {
|
||||
Some(window) => match window.reload() {
|
||||
Ok(()) => ok(request.id.clone(), json!(true)),
|
||||
Err(error) => failed(request.id.clone(), "host_error", error.to_string()),
|
||||
},
|
||||
None => failed(request.id.clone(), "host_error", "main window not found"),
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,14 @@ const dispatchPath = new URL('../src/host-bridge/dispatch.ts', import.meta.url);
|
||||
const dispatchSource = fs.readFileSync(dispatchPath, 'utf8');
|
||||
const hapticsPath = new URL('../src/host-bridge/haptics.ts', import.meta.url);
|
||||
const hapticsSource = fs.readFileSync(hapticsPath, 'utf8');
|
||||
const hostBridgeNavigationPath = new URL(
|
||||
'../src/host-bridge/navigation.ts',
|
||||
import.meta.url,
|
||||
);
|
||||
const hostBridgeNavigationSource = fs.readFileSync(
|
||||
hostBridgeNavigationPath,
|
||||
'utf8',
|
||||
);
|
||||
const notificationsPath = new URL('../src/host-bridge/notifications.ts', import.meta.url);
|
||||
const notificationsSource = fs.readFileSync(notificationsPath, 'utf8');
|
||||
const protocolPath = new URL('../src/host-bridge/protocol.ts', import.meta.url);
|
||||
@@ -87,6 +95,7 @@ const requiredMobileShellSourceModules = [
|
||||
'host-bridge/dispatch.ts',
|
||||
'host-bridge/files.ts',
|
||||
'host-bridge/haptics.ts',
|
||||
'host-bridge/navigation.ts',
|
||||
'host-bridge/notifications.ts',
|
||||
'host-bridge/protocol.ts',
|
||||
'host-bridge/scanner.ts',
|
||||
@@ -1621,10 +1630,12 @@ assertMobileDocumentPickerBoundary(
|
||||
);
|
||||
|
||||
if (
|
||||
!hostBridgeSource.includes(
|
||||
!hostBridgeNavigationSource.includes(
|
||||
'const externalUrlPayload = normalizeHostBridgeExternalUrlPayload(',
|
||||
) ||
|
||||
!hostBridgeSource.includes('openMobileShellExternalNavigation(Linking, externalUrlPayload.url)')
|
||||
!hostBridgeNavigationSource.includes(
|
||||
'openMobileShellExternalNavigation(navigator, externalUrlPayload.url)',
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
'mobile shell app.openExternalUrl must normalize payloads and use the shared external navigation helper',
|
||||
@@ -1855,7 +1866,32 @@ for (const snippet of [
|
||||
}
|
||||
}
|
||||
|
||||
if (!dispatchSource.includes('openMobileShellExternalNavigation(Linking, externalUrlPayload.url)')) {
|
||||
if (
|
||||
!dispatchSource.includes('openMobileHostBridgeExternalUrl(Linking, request.payload)') ||
|
||||
!dispatchSource.includes('openMobileHostBridgeNativePage(navigation, request.payload)') ||
|
||||
!dispatchSource.includes('reloadMobileHostBridgeWebView(navigation)') ||
|
||||
dispatchSource.includes('openMobileShellExternalNavigation') ||
|
||||
dispatchSource.includes('resolveMobileShellWebViewUrl') ||
|
||||
dispatchSource.includes('normalizeHostBridgeExternalUrlPayload') ||
|
||||
dispatchSource.includes('buildMobileShellUrl')
|
||||
) {
|
||||
throw new Error('mobile shell navigation HostBridge methods must delegate to navigation module');
|
||||
}
|
||||
for (const snippet of [
|
||||
'openMobileHostBridgeExternalUrl',
|
||||
'openMobileShellExternalNavigation(navigator, externalUrlPayload.url)',
|
||||
'normalizeHostBridgeExternalUrlPayload',
|
||||
'openMobileHostBridgeNativePage',
|
||||
'resolveMobileShellWebViewUrl',
|
||||
'buildMobileShellUrl(webViewUrl, navigation.urlOptions)',
|
||||
'reloadMobileHostBridgeWebView',
|
||||
]) {
|
||||
if (!hostBridgeNavigationSource.includes(snippet)) {
|
||||
throw new Error(`mobile shell navigation module is missing ${snippet}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hostBridgeNavigationSource.includes('openMobileShellExternalNavigation(navigator, externalUrlPayload.url)')) {
|
||||
throw new Error(
|
||||
'mobile shell HostBridge external URL flow must use the shared external navigation helper',
|
||||
);
|
||||
|
||||
@@ -5,17 +5,9 @@ import {
|
||||
type ClipboardWriteTextPayload,
|
||||
type HapticsImpactPayload,
|
||||
HOST_BRIDGE_VERSION,
|
||||
type HostBridgeError,
|
||||
type HostBridgeRequest,
|
||||
type NavigateNativePagePayload,
|
||||
normalizeHostBridgeExternalUrlPayload,
|
||||
normalizeHostBridgeLocalNotification,
|
||||
type OpenExternalUrlPayload,
|
||||
} from '../../../../packages/shared/src/contracts/hostBridge';
|
||||
import {
|
||||
openMobileShellExternalNavigation,
|
||||
resolveMobileShellWebViewUrl,
|
||||
} from '../shell/navigation';
|
||||
import { getMobileNetworkStatus } from '../shell/network';
|
||||
import { MOBILE_SHELL_HOST_VERSION } from '../shell/runtime';
|
||||
import { resolveMobileHostCapabilities } from './capabilities';
|
||||
@@ -38,7 +30,6 @@ import {
|
||||
} from './protocol';
|
||||
import { resetQrScannerForTest, scanQrCode } from './scanner';
|
||||
import { openShare } from './share';
|
||||
import { buildMobileShellUrl } from '../shell/url';
|
||||
import { showMobileLocalNotification } from './notifications';
|
||||
import {
|
||||
readMobileClipboardText,
|
||||
@@ -47,6 +38,11 @@ import {
|
||||
import { runMobileHapticsImpact } from './haptics';
|
||||
import { setMobileAppBadgeCount } from './badge';
|
||||
import { getMobileAppearanceColorScheme } from './appearance';
|
||||
import {
|
||||
openMobileHostBridgeExternalUrl,
|
||||
openMobileHostBridgeNativePage,
|
||||
reloadMobileHostBridgeWebView,
|
||||
} from './navigation';
|
||||
|
||||
let currentShareTarget: unknown = null;
|
||||
let navigation: MobileHostBridgeNavigation | null = null;
|
||||
@@ -57,24 +53,6 @@ export function configureMobileHostBridgeNavigation(
|
||||
navigation = nextNavigation;
|
||||
}
|
||||
|
||||
async function openExternalUrl(payload: unknown) {
|
||||
const externalUrlPayload = normalizeHostBridgeExternalUrlPayload(
|
||||
(payload as OpenExternalUrlPayload | undefined)?.url,
|
||||
);
|
||||
if (!externalUrlPayload) {
|
||||
throw invalidRequest('url must use an allowed external protocol');
|
||||
}
|
||||
|
||||
if (!(await openMobileShellExternalNavigation(Linking, externalUrlPayload.url))) {
|
||||
throw {
|
||||
code: 'host_error',
|
||||
message: 'external URL cannot be opened',
|
||||
} satisfies HostBridgeError;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function writeClipboard(payload: unknown) {
|
||||
if (
|
||||
!(await writeMobileClipboardText(
|
||||
@@ -123,39 +101,6 @@ function getRuntime() {
|
||||
};
|
||||
}
|
||||
|
||||
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(
|
||||
buildMobileShellUrl(webViewUrl, navigation.urlOptions),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
function reloadWebView() {
|
||||
if (!navigation) {
|
||||
throw unsupported('app.reloadWebView');
|
||||
}
|
||||
|
||||
navigation.reloadWebView();
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function dispatchMobileHostBridgeRequest(
|
||||
request: HostBridgeRequest,
|
||||
) {
|
||||
@@ -165,9 +110,12 @@ export async function dispatchMobileHostBridgeRequest(
|
||||
case 'appearance.getColorScheme':
|
||||
return ok(request, getMobileAppearanceColorScheme());
|
||||
case 'app.openExternalUrl':
|
||||
return ok(request, await openExternalUrl(request.payload));
|
||||
return ok(
|
||||
request,
|
||||
await openMobileHostBridgeExternalUrl(Linking, request.payload),
|
||||
);
|
||||
case 'app.reloadWebView':
|
||||
return ok(request, reloadWebView());
|
||||
return ok(request, reloadMobileHostBridgeWebView(navigation));
|
||||
case 'network.status':
|
||||
return ok(request, await getMobileNetworkStatus());
|
||||
case 'clipboard.writeText':
|
||||
@@ -207,7 +155,10 @@ export async function dispatchMobileHostBridgeRequest(
|
||||
: null;
|
||||
return ok(request, true);
|
||||
case 'navigation.openNativePage':
|
||||
return ok(request, openNativePage(request.payload));
|
||||
return ok(
|
||||
request,
|
||||
openMobileHostBridgeNativePage(navigation, request.payload),
|
||||
);
|
||||
case 'auth.requestLogin':
|
||||
case 'payment.request':
|
||||
return failure(request, unsupported(request.method));
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
type HostBridgeError,
|
||||
type NavigateNativePagePayload,
|
||||
normalizeHostBridgeExternalUrlPayload,
|
||||
type OpenExternalUrlPayload,
|
||||
} from '../../../../packages/shared/src/contracts/hostBridge';
|
||||
import {
|
||||
type MobileShellExternalNavigator,
|
||||
openMobileShellExternalNavigation,
|
||||
resolveMobileShellWebViewUrl,
|
||||
} from '../shell/navigation';
|
||||
import { buildMobileShellUrl } from '../shell/url';
|
||||
import {
|
||||
type MobileHostBridgeNavigation,
|
||||
invalidRequest,
|
||||
unsupported,
|
||||
} from './protocol';
|
||||
|
||||
export async function openMobileHostBridgeExternalUrl(
|
||||
navigator: MobileShellExternalNavigator,
|
||||
payload: unknown,
|
||||
) {
|
||||
const externalUrlPayload = normalizeHostBridgeExternalUrlPayload(
|
||||
(payload as OpenExternalUrlPayload | undefined)?.url,
|
||||
);
|
||||
if (!externalUrlPayload) {
|
||||
throw invalidRequest('url must use an allowed external protocol');
|
||||
}
|
||||
|
||||
if (
|
||||
!(await openMobileShellExternalNavigation(navigator, externalUrlPayload.url))
|
||||
) {
|
||||
throw {
|
||||
code: 'host_error',
|
||||
message: 'external URL cannot be opened',
|
||||
} satisfies HostBridgeError;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function openMobileHostBridgeNativePage(
|
||||
navigation: MobileHostBridgeNavigation | null,
|
||||
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(
|
||||
buildMobileShellUrl(webViewUrl, navigation.urlOptions),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function reloadMobileHostBridgeWebView(
|
||||
navigation: MobileHostBridgeNavigation | null,
|
||||
) {
|
||||
if (!navigation) {
|
||||
throw unsupported('app.reloadWebView');
|
||||
}
|
||||
|
||||
navigation.reloadWebView();
|
||||
return true;
|
||||
}
|
||||
@@ -2785,3 +2785,10 @@
|
||||
- 决策:`packages/shared/src/contracts/hostBridge.ts` 导出 `HOST_BRIDGE_DESKTOP_NETWORK_CHECK_TIMEOUT_MS`,作为桌面壳主站可达性探测超时的声明来源;Tauri Rust 侧保留同名职责镜像 `DESKTOP_NETWORK_CHECK_TIMEOUT_MS`,由 `apps/desktop-shell/scripts/check-config.mjs` 反查共享值并拒绝漂移。该边界只服务桌面宿主内部网络状态,不新增 H5 任意网络探测能力。
|
||||
- 影响范围:`packages/shared/src/contracts/hostBridge.ts`、`apps/desktop-shell/src-tauri/src/shell/network.rs`、`apps/desktop-shell/scripts/check-config.mjs`、宿主壳能力统一协议文档、Expo / Tauri HostBridge 方案文档。
|
||||
- 验证方式:`npm run test -- packages/shared/src/contracts/hostBridge.test.ts`、`npm run desktop-shell:typecheck`、`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
## 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 细节。
|
||||
- 影响范围:`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`。
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -258,6 +258,7 @@ const expectedMobileHostBridgeFiles = [
|
||||
'dispatch.ts',
|
||||
'files.ts',
|
||||
'haptics.ts',
|
||||
'navigation.ts',
|
||||
'notifications.ts',
|
||||
'protocol.ts',
|
||||
'scanner.ts',
|
||||
@@ -301,6 +302,7 @@ const expectedDesktopHostBridgeRustFiles = [
|
||||
'dispatch.rs',
|
||||
'files.rs',
|
||||
'mod.rs',
|
||||
'navigation.rs',
|
||||
'notifications.rs',
|
||||
'protocol.rs',
|
||||
'share.rs',
|
||||
|
||||
Reference in New Issue
Block a user