From 81b51ed405610b22c82d4c26877eb37912244c4f Mon Sep 17 00:00:00 2001 From: kdletters Date: Fri, 19 Jun 2026 04:07:08 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=86=E8=96=84=E6=A1=8C=E9=9D=A2=E5=A3=B3?= =?UTF-8?q?=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增桌面壳 app.rs 承接 Tauri 应用装配 收敛 main.rs 为薄入口并调用 app::run 扩展原生壳门禁锁定桌面入口职责边界 更新宿主壳方案和共享决策记录 --- apps/desktop-shell/scripts/check-config.mjs | 40 +++++++-- apps/desktop-shell/src-tauri/src/app.rs | 88 +++++++++++++++++++ apps/desktop-shell/src-tauri/src/main.rs | 84 +----------------- .../shared-memory/decision-log.md | 2 +- ...€‘ExpoReactNative与Tauri宿主壳方案-2026-06-17.md | 4 +- ...�前端架构】宿主壳能力统一协议-2026-06-17.md | 2 +- scripts/check-native-shells.mjs | 26 +++++- 7 files changed, 154 insertions(+), 92 deletions(-) create mode 100644 apps/desktop-shell/src-tauri/src/app.rs diff --git a/apps/desktop-shell/scripts/check-config.mjs b/apps/desktop-shell/scripts/check-config.mjs index c3c16b69c..cc5a338bc 100644 --- a/apps/desktop-shell/scripts/check-config.mjs +++ b/apps/desktop-shell/scripts/check-config.mjs @@ -34,6 +34,8 @@ const nativeAppHostBridgePath = new URL( import.meta.url, ); const nativeAppHostBridgeSource = fs.readFileSync(nativeAppHostBridgePath, 'utf8'); +const appPath = new URL('../src-tauri/src/app.rs', import.meta.url); +const app = fs.readFileSync(appPath, 'utf8'); const mainPath = new URL('../src-tauri/src/main.rs', import.meta.url); const main = fs.readFileSync(mainPath, 'utf8'); const rustSourceDir = new URL('../src-tauri/src/', import.meta.url); @@ -852,6 +854,7 @@ function extractTauriInvokeCommands(source) { return match[1] .split(',') .map((command) => command.trim()) + .map((command) => command.split('::').pop()) .filter(Boolean); } @@ -1395,6 +1398,7 @@ const requiredRustHostModules = [ 'host_bridge/mod.rs', 'host_bridge/protocol.rs', 'host_bridge/share.rs', + 'app.rs', 'main.rs', 'shell/deep_link.rs', 'shell/events.rs', @@ -1640,7 +1644,7 @@ assertSameList( 'desktop shell build manifest commands', ); assertSameList( - extractTauriInvokeCommands(main), + extractTauriInvokeCommands(app), allowedTauriCommands, 'desktop shell invoke handler commands', ); @@ -1700,24 +1704,50 @@ const orderedDesktopPluginSnippets = [ 'tauri_plugin_opener::init()', ]; for (const pluginSnippet of orderedDesktopPluginSnippets) { - if (!main.includes(pluginSnippet)) { - throw new Error(`desktop shell main.rs missing plugin ${pluginSnippet}`); + if (!app.includes(pluginSnippet)) { + throw new Error(`desktop shell app.rs missing plugin ${pluginSnippet}`); } } for (let index = 1; index < orderedDesktopPluginSnippets.length; index += 1) { const previousPlugin = orderedDesktopPluginSnippets[index - 1]; const currentPlugin = orderedDesktopPluginSnippets[index]; - if (main.indexOf(previousPlugin) > main.indexOf(currentPlugin)) { + if (app.indexOf(previousPlugin) > app.indexOf(currentPlugin)) { throw new Error( `desktop shell plugin order drifted: ${previousPlugin} must be registered before ${currentPlugin}`, ); } } -if (main.includes('single-instance",') || main.includes('"single-instance"')) { +if (app.includes('single-instance",') || app.includes('"single-instance"')) { throw new Error('desktop shell must not emit secondary-instance argv to H5'); } +if (!main.includes('mod app;') || !main.includes('app::run();')) { + throw new Error('desktop shell main.rs must stay a thin app entrypoint'); +} + +for (const entrypointOnlySnippet of [ + 'tauri::Builder::default()', + 'tauri::generate_handler!', + 'WebviewWindowBuilder::from_config', +]) { + if (main.includes(entrypointOnlySnippet)) { + throw new Error( + `desktop shell main.rs must not own Tauri app setup: ${entrypointOnlySnippet}`, + ); + } +} + +for (const appSetupSnippet of [ + 'tauri::Builder::default()', + 'crate::host_bridge::host_bridge_request', + 'WebviewWindowBuilder::from_config', +]) { + if (!app.includes(appSetupSnippet)) { + throw new Error(`desktop shell app.rs missing setup snippet ${appSetupSnippet}`); + } +} + for (const snippet of requiredRustHostSnippets) { if (!rustHostSource.includes(snippet)) { throw new Error(`desktop shell Rust host bridge missing ${snippet}`); diff --git a/apps/desktop-shell/src-tauri/src/app.rs b/apps/desktop-shell/src-tauri/src/app.rs new file mode 100644 index 000000000..3271ccccd --- /dev/null +++ b/apps/desktop-shell/src-tauri/src/app.rs @@ -0,0 +1,88 @@ +use crate::host_bridge::{DesktopShareState, HostBridgeReplayState}; +use crate::shell::deep_link::{ + register_desktop_deep_link_events, register_desktop_deep_link_schemes, +}; +use crate::shell::menu::register_desktop_app_menu; +use crate::shell::tray::{ + register_desktop_tray, register_desktop_window_close_events, + resolve_desktop_single_instance_action, show_main_window, DesktopSingleInstanceAction, +}; +use crate::shell::webview::{ + desktop_window_config_with_runtime_platform, emit_desktop_lifecycle_event, + open_desktop_external_navigation, register_desktop_file_drop_events, + register_desktop_lifecycle_events, register_desktop_navigation_events, + register_desktop_network_events, replay_desktop_webview_state, + should_allow_desktop_webview_download, should_allow_desktop_webview_navigation, + should_replay_desktop_webview_state_on_page_load, +}; +use crate::shell::window_state::desktop_window_state_plugin; +use tauri::webview::NewWindowResponse; + +pub(crate) fn run() { + tauri::Builder::default() + .manage(DesktopShareState::default()) + .manage(HostBridgeReplayState::default()) + .plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| { + match resolve_desktop_single_instance_action() { + DesktopSingleInstanceAction::ShowMainWindow => { + let _ = show_main_window(app); + } + } + })) + .plugin(desktop_window_state_plugin()) + .plugin(tauri_plugin_deep_link::init()) + .plugin(tauri_plugin_clipboard_manager::init()) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_notification::init()) + .plugin(tauri_plugin_opener::init()) + .setup(|app| { + register_desktop_app_menu(app)?; + let tray_registered = match register_desktop_tray(app) { + Ok(()) => true, + Err(error) => { + eprintln!("desktop tray registration failed: {error}"); + false + } + }; + let window_config = app.config().app.windows.get(0).cloned(); + if let Some(config) = window_config { + let config = desktop_window_config_with_runtime_platform(config); + let app_handle = app.handle().clone(); + let new_window_app_handle = app.handle().clone(); + let window = tauri::WebviewWindowBuilder::from_config(app.handle(), &config)? + .on_navigation(move |url| { + if should_allow_desktop_webview_navigation(url) { + true + } else { + open_desktop_external_navigation(&app_handle, url); + false + } + }) + .on_new_window(move |url, _features| { + open_desktop_external_navigation(&new_window_app_handle, &url); + NewWindowResponse::Deny + }) + .on_page_load(|window, payload| { + if should_replay_desktop_webview_state_on_page_load(payload.event()) { + replay_desktop_webview_state(&window); + } + }) + .on_download(|_webview, event| should_allow_desktop_webview_download(&event)) + .build()?; + register_desktop_window_close_events(&window, tray_registered); + register_desktop_lifecycle_events(&window); + let _ = emit_desktop_lifecycle_event(&window, "active", true, "created"); + let _ = register_desktop_network_events(&window); + let _ = register_desktop_navigation_events(&window); + register_desktop_file_drop_events(&window); + register_desktop_deep_link_events(app)?; + register_desktop_deep_link_schemes(app); + } + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + crate::host_bridge::host_bridge_request + ]) + .run(tauri::generate_context!()) + .expect("failed to run Genarrative desktop shell"); +} diff --git a/apps/desktop-shell/src-tauri/src/main.rs b/apps/desktop-shell/src-tauri/src/main.rs index 16c7865fc..cab5f08f7 100644 --- a/apps/desktop-shell/src-tauri/src/main.rs +++ b/apps/desktop-shell/src-tauri/src/main.rs @@ -1,87 +1,7 @@ +mod app; mod host_bridge; mod shell; -use host_bridge::{host_bridge_request, DesktopShareState, HostBridgeReplayState}; -use shell::deep_link::{register_desktop_deep_link_events, register_desktop_deep_link_schemes}; -use shell::menu::register_desktop_app_menu; -use shell::tray::{ - register_desktop_tray, register_desktop_window_close_events, - resolve_desktop_single_instance_action, show_main_window, DesktopSingleInstanceAction, -}; -use shell::webview::{ - desktop_window_config_with_runtime_platform, emit_desktop_lifecycle_event, - open_desktop_external_navigation, register_desktop_file_drop_events, - register_desktop_lifecycle_events, register_desktop_navigation_events, - register_desktop_network_events, replay_desktop_webview_state, - should_allow_desktop_webview_download, should_allow_desktop_webview_navigation, - should_replay_desktop_webview_state_on_page_load, -}; -use shell::window_state::desktop_window_state_plugin; -use tauri::webview::NewWindowResponse; - fn main() { - tauri::Builder::default() - .manage(DesktopShareState::default()) - .manage(HostBridgeReplayState::default()) - .plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| { - match resolve_desktop_single_instance_action() { - DesktopSingleInstanceAction::ShowMainWindow => { - let _ = show_main_window(app); - } - } - })) - .plugin(desktop_window_state_plugin()) - .plugin(tauri_plugin_deep_link::init()) - .plugin(tauri_plugin_clipboard_manager::init()) - .plugin(tauri_plugin_dialog::init()) - .plugin(tauri_plugin_notification::init()) - .plugin(tauri_plugin_opener::init()) - .setup(|app| { - register_desktop_app_menu(app)?; - let tray_registered = match register_desktop_tray(app) { - Ok(()) => true, - Err(error) => { - eprintln!("desktop tray registration failed: {error}"); - false - } - }; - let window_config = app.config().app.windows.get(0).cloned(); - if let Some(config) = window_config { - let config = desktop_window_config_with_runtime_platform(config); - let app_handle = app.handle().clone(); - let new_window_app_handle = app.handle().clone(); - let window = tauri::WebviewWindowBuilder::from_config(app.handle(), &config)? - .on_navigation(move |url| { - if should_allow_desktop_webview_navigation(url) { - true - } else { - open_desktop_external_navigation(&app_handle, url); - false - } - }) - .on_new_window(move |url, _features| { - open_desktop_external_navigation(&new_window_app_handle, &url); - NewWindowResponse::Deny - }) - .on_page_load(|window, payload| { - if should_replay_desktop_webview_state_on_page_load(payload.event()) { - replay_desktop_webview_state(&window); - } - }) - .on_download(|_webview, event| should_allow_desktop_webview_download(&event)) - .build()?; - register_desktop_window_close_events(&window, tray_registered); - register_desktop_lifecycle_events(&window); - let _ = emit_desktop_lifecycle_event(&window, "active", true, "created"); - let _ = register_desktop_network_events(&window); - let _ = register_desktop_navigation_events(&window); - register_desktop_file_drop_events(&window); - register_desktop_deep_link_events(app)?; - register_desktop_deep_link_schemes(app); - } - Ok(()) - }) - .invoke_handler(tauri::generate_handler![host_bridge_request]) - .run(tauri::generate_context!()) - .expect("failed to run Genarrative desktop shell"); + app::run(); } diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index f66b408fb..c2bc94ee5 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -2514,7 +2514,7 @@ ## 2026-06-18 三端宿主桥接层文件结构对齐 - 背景:微信小程序壳、Expo 移动壳和 Tauri 桌面壳都在承接宿主能力;如果微信页面继续散落 `index.shared.js`,桌面端继续把桥接分发堆在 `main.rs`,后续新增登录、支付、文件、通知或 sandbox 转发能力时会很难跨端对照 owner。 -- 决策:三端桥接层按职责对齐,但保留各宿主真实边界。微信小程序页面路由不改,`miniprogram/host-bridge/protocol.js` 只沉淀微信壳能力、页面 URL、结果 hash / storage key 和分享消息类型等常量,`dispatch.js` 只作为 `protocol`、`webView`、`payment`、`shareGrid`、`subscribeMessage` 的薄索引,真实协议归一、支付 / 订阅 / 分享结果编解码仍分别放在 `webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js`,页面目录只保留生命周期和装配,不把微信小程序硬改成 Expo / Tauri 的 request 总线;Expo 移动壳拆成 `apps/mobile-shell/src/host-bridge/protocol.ts`、`capabilities.ts`、`dispatch.ts`、`files.ts`、`share.ts` 和 facade `bridge.ts`,分别负责 envelope / request 校验 / ok-failure 响应 / replay 基础类型、能力清单与 iOS 差异、method 分发、文件能力、分享能力和 WebView message 入口 / request id replay 编排;根 `App.tsx` 只装配 `apps/mobile-shell/src/shell/ShellApp.tsx`,`apps/mobile-shell/src/shell/*.ts(x)` 负责 WebView 容器、URL、导航、网络、生命周期、安全区和 WebView policy;Tauri 桌面壳拆成 `apps/desktop-shell/src-tauri/src/host_bridge/protocol.rs`、`capabilities.rs`、`dispatch.rs`、`files.rs`、`share.rs` 和 command facade `mod.rs`,分别负责 envelope / method 白名单 / request 校验 / replay 状态、能力清单、method 分发、文件能力、分享能力和 `host_bridge_request` command / replay 编排;`apps/desktop-shell/src-tauri/src/shell/*.rs` 承接运行态、入口 URL、导航 / 下载、网络、生命周期、拖拽图片、HostBridge 事件注入、深链、托盘和 WebView 门面,`main.rs` 只做 builder、plugin、窗口和状态装配。`scripts/check-native-shells.mjs` 锁定三端桥接层目录清单,并拒绝移动根入口直接引用 HostBridge。 +- 决策:三端桥接层按职责对齐,但保留各宿主真实边界。微信小程序页面路由不改,`miniprogram/host-bridge/protocol.js` 只沉淀微信壳能力、页面 URL、结果 hash / storage key 和分享消息类型等常量,`dispatch.js` 只作为 `protocol`、`webView`、`payment`、`shareGrid`、`subscribeMessage` 的薄索引,真实协议归一、支付 / 订阅 / 分享结果编解码仍分别放在 `webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js`,页面目录只保留生命周期和装配,不把微信小程序硬改成 Expo / Tauri 的 request 总线;Expo 移动壳拆成 `apps/mobile-shell/src/host-bridge/protocol.ts`、`capabilities.ts`、`dispatch.ts`、`files.ts`、`share.ts` 和 facade `bridge.ts`,分别负责 envelope / request 校验 / ok-failure 响应 / replay 基础类型、能力清单与 iOS 差异、method 分发、文件能力、分享能力和 WebView message 入口 / request id replay 编排;根 `App.tsx` 只装配 `apps/mobile-shell/src/shell/ShellApp.tsx`,`apps/mobile-shell/src/shell/*.ts(x)` 负责 WebView 容器、URL、导航、网络、生命周期、安全区和 WebView policy;Tauri 桌面壳拆成 `apps/desktop-shell/src-tauri/src/app.rs`、`host_bridge/protocol.rs`、`capabilities.rs`、`dispatch.rs`、`files.rs`、`share.rs` 和 command facade `mod.rs`,分别负责 Tauri builder / plugin / window 装配、envelope / method 白名单 / request 校验 / replay 状态、能力清单、method 分发、文件能力、分享能力和 `host_bridge_request` command / replay 编排;`apps/desktop-shell/src-tauri/src/shell/*.rs` 承接运行态、入口 URL、导航 / 下载、网络、生命周期、拖拽图片、HostBridge 事件注入、深链、托盘和 WebView 门面,`main.rs` 只做薄入口并调用 `app::run()`。`scripts/check-native-shells.mjs` 锁定三端桥接层目录清单,并拒绝移动根入口和桌面根入口重新承接宿主能力装配。 - 影响范围:`miniprogram/host-bridge/`、`miniprogram/pages/*/index.js`、`apps/mobile-shell/src/`、`apps/desktop-shell/src-tauri/src/`、`scripts/check-native-shells.mjs`、宿主壳方案文档。 - 验证方式:`npm run test -- miniprogram/host-bridge/webView.test.js miniprogram/host-bridge/payment.test.js miniprogram/host-bridge/shareGrid.test.js miniprogram/host-bridge/subscribeMessage.test.js miniprogram/pages/web-view/index.style.test.js`、`npm run check:native-shells`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。 diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index b0b84543d..346d782d9 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -64,7 +64,7 @@ src/ 已落地:`packages/shared/src/contracts/hostBridge.ts` 保存消息 envelope、method、payload 和错误码,H5、Expo 壳与 Tauri 壳共享同一份协议类型。 -三端宿主桥接层按职责对齐命名:微信小程序页面路由仍保留在 `miniprogram/pages/*`,`miniprogram/host-bridge/protocol.js` 只沉淀微信壳能力、页面 URL、结果 hash / storage key 和分享消息类型等常量,`dispatch.js` 只作为 `protocol`、`webView`、`payment`、`shareGrid`、`subscribeMessage` 的薄索引,真实协议归一、支付 / 订阅 / 分享结果编解码仍分别在 `webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js`,不把微信小程序硬改成 Expo / Tauri 的 request 总线;Page 生命周期、`wx.*` 容器调用、WebView 容器行为和页面工厂统一放在 `miniprogram/shell/webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js`,页面入口只做 `Page(createWechat...Page())` 装配。Expo 移动壳使用 `apps/mobile-shell/src/host-bridge/protocol.ts` 承接 envelope、request 校验、ok / failure 响应和 replay 基础类型,`capabilities.ts` 只引用共享 HostBridge capability profile 并选择 iOS 差异能力,`dispatch.ts` 承接 method 分发和宿主能力调用,`files.ts` / `share.ts` 分别承接文件和分享能力,`bridge.ts` 只作为 WebView message 入口、request id replay 编排和对外 facade;`apps/mobile-shell/App.tsx` 只装配 `apps/mobile-shell/src/shell/ShellApp.tsx`,由 `apps/mobile-shell/src/shell/*.ts(x)` 承接 WebView 容器、URL、导航、网络、生命周期、安全区和 WebView policy。Tauri 桌面壳使用 `apps/desktop-shell/src-tauri/src/host_bridge/protocol.rs` 承接 envelope、method 白名单、request 校验和 replay 状态,`capabilities.rs` 承接共享桌面 capability profile 的 Rust 运行时镜像,`dispatch.rs` 承接 method 分发和宿主能力调用,`files.rs` / `share.rs` 分别承接文件和分享能力,`mod.rs` 只保留模块声明、必要 re-export、`host_bridge_request` command facade 和 replay 编排;`apps/desktop-shell/src-tauri/src/shell/runtime.rs`、`url.rs`、`navigation.rs`、`network.rs`、`lifecycle.rs`、`file_drop.rs`、`events.rs`、`deep_link.rs`、`tray.rs`、`menu.rs`、`window_state.rs` 和 `webview.rs` 分别承接运行态、入口 URL、导航 / 下载、网络、生命周期、拖拽图片、HostBridge 事件注入、深链、托盘、应用菜单、窗口状态持久化和 WebView 门面,`main.rs` 只保留 Tauri builder / plugin / window 装配。 +三端宿主桥接层按职责对齐命名:微信小程序页面路由仍保留在 `miniprogram/pages/*`,`miniprogram/host-bridge/protocol.js` 只沉淀微信壳能力、页面 URL、结果 hash / storage key 和分享消息类型等常量,`dispatch.js` 只作为 `protocol`、`webView`、`payment`、`shareGrid`、`subscribeMessage` 的薄索引,真实协议归一、支付 / 订阅 / 分享结果编解码仍分别在 `webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js`,不把微信小程序硬改成 Expo / Tauri 的 request 总线;Page 生命周期、`wx.*` 容器调用、WebView 容器行为和页面工厂统一放在 `miniprogram/shell/webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js`,页面入口只做 `Page(createWechat...Page())` 装配。Expo 移动壳使用 `apps/mobile-shell/src/host-bridge/protocol.ts` 承接 envelope、request 校验、ok / failure 响应和 replay 基础类型,`capabilities.ts` 只引用共享 HostBridge capability profile 并选择 iOS 差异能力,`dispatch.ts` 承接 method 分发和宿主能力调用,`files.ts` / `share.ts` 分别承接文件和分享能力,`bridge.ts` 只作为 WebView message 入口、request id replay 编排和对外 facade;`apps/mobile-shell/App.tsx` 只装配 `apps/mobile-shell/src/shell/ShellApp.tsx`,由 `apps/mobile-shell/src/shell/*.ts(x)` 承接 WebView 容器、URL、导航、网络、生命周期、安全区和 WebView policy。Tauri 桌面壳使用 `apps/desktop-shell/src-tauri/src/host_bridge/protocol.rs` 承接 envelope、method 白名单、request 校验和 replay 状态,`capabilities.rs` 承接共享桌面 capability profile 的 Rust 运行时镜像,`dispatch.rs` 承接 method 分发和宿主能力调用,`files.rs` / `share.rs` 分别承接文件和分享能力,`mod.rs` 只保留模块声明、必要 re-export、`host_bridge_request` command facade 和 replay 编排;`apps/desktop-shell/src-tauri/src/shell/runtime.rs`、`url.rs`、`navigation.rs`、`network.rs`、`lifecycle.rs`、`file_drop.rs`、`events.rs`、`deep_link.rs`、`tray.rs`、`menu.rs`、`window_state.rs` 和 `webview.rs` 分别承接运行态、入口 URL、导航 / 下载、网络、生命周期、拖拽图片、HostBridge 事件注入、深链、托盘、应用菜单、窗口状态持久化和 WebView 门面,`apps/desktop-shell/src-tauri/src/app.rs` 承接 Tauri builder / plugin / window 装配,`main.rs` 只保留薄入口并调用 `app::run()`。 ## HostBridge 消息协议 @@ -467,7 +467,7 @@ GameBridge 禁止: 2026-06-18 追加:`app.openExternalUrl` 的协议白名单以共享 HostBridge 契约 `HOST_BRIDGE_EXTERNAL_URL_PROTOCOLS` 为唯一来源,当前只允许 `http:`、`https:`、`mailto:`、`tel:`。Expo 壳直接复用共享归一化逻辑,Tauri 壳 Rust 侧用 URL parser 镜像同一清单;`npm run check:native-shells` 会反查共享契约与桌面壳协议清单,防止某一端单独放宽外链协议。 -2026-06-18 追加:微信、移动端和桌面端桥接层文件结构按职责对齐。微信小程序的 `web-view`、支付、九宫切图和订阅消息桥接逻辑统一迁入 `miniprogram/host-bridge/webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js`,页面目录只保留页面生命周期、WXML/WXSS 和装配;移动壳拆成 `apps/mobile-shell/src/host-bridge/protocol.ts`、`files.ts`、`share.ts` 和 facade `bridge.ts`,与桌面端 `host_bridge/protocol.rs`、`files.rs`、`share.rs`、`mod.rs` 对齐;移动壳根 `App.tsx` 也保持薄入口,只装配 `src/shell/ShellApp.tsx`,WebView 容器、深链、网络、生命周期和安全策略全部留在 `src/shell/`;桌面壳 Rust 源码拆成 `apps/desktop-shell/src-tauri/src/host_bridge/*.rs` 与 `apps/desktop-shell/src-tauri/src/shell/*.rs`,其中 `runtime.rs`、`url.rs`、`navigation.rs`、`network.rs`、`lifecycle.rs`、`file_drop.rs`、`events.rs`、`deep_link.rs`、`tray.rs`、`menu.rs`、`window_state.rs` 和 `webview.rs` 分别承接运行态、入口 URL、导航 / 下载、网络、生命周期、拖拽图片、HostBridge 事件注入、深链、托盘、应用菜单、窗口状态持久化和 WebView 门面,薄 `main.rs` 只声明两个模块并装配 Tauri builder / plugin / window。根级 `npm run check:native-shells` 会锁定三端桥接层目录清单,避免后续把能力逻辑重新散落到页面、移动入口或桌面入口。 +2026-06-18 追加:微信、移动端和桌面端桥接层文件结构按职责对齐。微信小程序的 `web-view`、支付、九宫切图和订阅消息桥接逻辑统一迁入 `miniprogram/host-bridge/webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js`,页面目录只保留页面生命周期、WXML/WXSS 和装配;移动壳拆成 `apps/mobile-shell/src/host-bridge/protocol.ts`、`files.ts`、`share.ts` 和 facade `bridge.ts`,与桌面端 `host_bridge/protocol.rs`、`files.rs`、`share.rs`、`mod.rs` 对齐;移动壳根 `App.tsx` 也保持薄入口,只装配 `src/shell/ShellApp.tsx`,WebView 容器、深链、网络、生命周期和安全策略全部留在 `src/shell/`;桌面壳 Rust 源码拆成 `apps/desktop-shell/src-tauri/src/app.rs`、`host_bridge/*.rs` 与 `shell/*.rs`,其中 `app.rs` 承接 Tauri builder / plugin / window 装配,`runtime.rs`、`url.rs`、`navigation.rs`、`network.rs`、`lifecycle.rs`、`file_drop.rs`、`events.rs`、`deep_link.rs`、`tray.rs`、`menu.rs`、`window_state.rs` 和 `webview.rs` 分别承接运行态、入口 URL、导航 / 下载、网络、生命周期、拖拽图片、HostBridge 事件注入、深链、托盘、应用菜单、窗口状态持久化和 WebView 门面,薄 `main.rs` 只声明模块并调用 `app::run()`。根级 `npm run check:native-shells` 会锁定三端桥接层目录清单,避免后续把能力逻辑重新散落到页面、移动入口或桌面入口。 2026-06-19 追加:HostBridge 载荷边界以共享契约为单一声明来源。`packages/shared/src/contracts/hostBridge.ts` 导出文本 / 图片 / 音频 MIME 清单、导入 / 导出字节上限、导出文件名 fallback 与长度上限,以及 request id、角标、剪贴板和本地通知文本长度边界;Expo 移动壳必须直接导入这些共享常量,不再本地重声明文件大小或 MIME 清单,并且文本 / 音频导入必须在读取内容前通过 picker `size` 或 Expo `File.size` 完成大小门禁,无法拿到可信 byte count 时直接拒绝导入;Tauri 桌面壳的配置检查会反查 Rust 镜像实现,拒绝文件大小、MIME 清单、文件名、通知、剪贴板或 request id 边界与共享契约漂移。新增文件类型或调整体积上限必须先更新共享契约、壳实现和门禁,再进入玩法或 H5 facade。 diff --git a/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md b/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md index e2811b587..7fbb041a3 100644 --- a/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md +++ b/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md @@ -37,7 +37,7 @@ AI H5 sandbox -> parent HostBridge adapter ``` -桥接层文件结构按宿主统一为“协议 / 能力清单 / 分发 / 宿主容器行为”四类职责。微信小程序不硬套 Expo / Tauri 的 request 总线:`miniprogram/host-bridge/protocol.js` 只沉淀微信壳能力、页面 URL、结果 hash / storage key 和分享消息类型等常量,`dispatch.js` 只作为 `protocol`、`webView`、`payment`、`shareGrid`、`subscribeMessage` 的薄索引,真实协议归一、支付 / 订阅 / 分享结果编解码仍分别放在 `webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js`;`miniprogram/shell/webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js` 承接 Page 生命周期、`wx.*` 容器调用、WebView 容器行为、支付页和订阅页装配,页面目录只保留 `Page(createWechat...Page())` 装配。Expo 移动壳使用 `apps/mobile-shell/src/host-bridge/protocol.ts` 承接 envelope、request 校验、ok / failure 响应和 replay 基础类型,`capabilities.ts` 只引用共享 HostBridge capability profile 并选择 iOS 差异能力,`dispatch.ts` 承接 method 分发和宿主能力调用,`files.ts` / `share.ts` 分别承接文件和分享能力,`bridge.ts` 只作为 WebView message 入口、request id replay 编排和对外 facade;`apps/mobile-shell/App.tsx` 只装配 `apps/mobile-shell/src/shell/ShellApp.tsx`,由 `apps/mobile-shell/src/shell/*.ts(x)` 承接 WebView 容器、URL、导航、网络、生命周期、安全区和 WebView policy。Tauri 桌面壳使用 `apps/desktop-shell/src-tauri/src/host_bridge/protocol.rs` 承接 envelope、method 白名单、request 校验和 replay 状态,`capabilities.rs` 承接共享桌面 capability profile 的 Rust 运行时镜像,`dispatch.rs` 承接 method 分发和宿主能力调用,`files.rs` / `share.rs` 分别承接文件和分享能力,`mod.rs` 只保留模块声明、必要 re-export、`host_bridge_request` command facade 和 replay 编排;`apps/desktop-shell/src-tauri/src/shell/runtime.rs`、`url.rs`、`navigation.rs`、`network.rs`、`lifecycle.rs`、`file_drop.rs`、`events.rs`、`deep_link.rs`、`tray.rs` 和 `webview.rs` 分别承接运行态、入口 URL、导航 / 下载、网络、生命周期、拖拽图片、HostBridge 事件注入、深链、托盘和 WebView 门面,`main.rs` 只保留 Tauri builder / plugin / window 装配。`npm run check:native-shells` 会检查这些目录清单。 +桥接层文件结构按宿主统一为“协议 / 能力清单 / 分发 / 宿主容器行为”四类职责。微信小程序不硬套 Expo / Tauri 的 request 总线:`miniprogram/host-bridge/protocol.js` 只沉淀微信壳能力、页面 URL、结果 hash / storage key 和分享消息类型等常量,`dispatch.js` 只作为 `protocol`、`webView`、`payment`、`shareGrid`、`subscribeMessage` 的薄索引,真实协议归一、支付 / 订阅 / 分享结果编解码仍分别放在 `webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js`;`miniprogram/shell/webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js` 承接 Page 生命周期、`wx.*` 容器调用、WebView 容器行为、支付页和订阅页装配,页面目录只保留 `Page(createWechat...Page())` 装配。Expo 移动壳使用 `apps/mobile-shell/src/host-bridge/protocol.ts` 承接 envelope、request 校验、ok / failure 响应和 replay 基础类型,`capabilities.ts` 只引用共享 HostBridge capability profile 并选择 iOS 差异能力,`dispatch.ts` 承接 method 分发和宿主能力调用,`files.ts` / `share.ts` 分别承接文件和分享能力,`bridge.ts` 只作为 WebView message 入口、request id replay 编排和对外 facade;`apps/mobile-shell/App.tsx` 只装配 `apps/mobile-shell/src/shell/ShellApp.tsx`,由 `apps/mobile-shell/src/shell/*.ts(x)` 承接 WebView 容器、URL、导航、网络、生命周期、安全区和 WebView policy。Tauri 桌面壳使用 `apps/desktop-shell/src-tauri/src/host_bridge/protocol.rs` 承接 envelope、method 白名单、request 校验和 replay 状态,`capabilities.rs` 承接共享桌面 capability profile 的 Rust 运行时镜像,`dispatch.rs` 承接 method 分发和宿主能力调用,`files.rs` / `share.rs` 分别承接文件和分享能力,`mod.rs` 只保留模块声明、必要 re-export、`host_bridge_request` command facade 和 replay 编排;`apps/desktop-shell/src-tauri/src/shell/runtime.rs`、`url.rs`、`navigation.rs`、`network.rs`、`lifecycle.rs`、`file_drop.rs`、`events.rs`、`deep_link.rs`、`tray.rs` 和 `webview.rs` 分别承接运行态、入口 URL、导航 / 下载、网络、生命周期、拖拽图片、HostBridge 事件注入、深链、托盘和 WebView 门面,`apps/desktop-shell/src-tauri/src/app.rs` 承接 Tauri builder / plugin / window 装配,`main.rs` 只保留薄入口并调用 `app::run()`。`npm run check:native-shells` 会检查这些目录清单。 ## 首批能力 diff --git a/scripts/check-native-shells.mjs b/scripts/check-native-shells.mjs index fcaff0a82..1b4ab91c1 100644 --- a/scripts/check-native-shells.mjs +++ b/scripts/check-native-shells.mjs @@ -836,10 +836,34 @@ function assertHostBridgeLayerLayout() { .sort(); assertSameList( desktopEntrypointRustFiles, - ['main.rs'], + ['app.rs', 'main.rs'], 'desktop shell entrypoint Rust files', ); + const desktopMainSource = fs.readFileSync( + 'apps/desktop-shell/src-tauri/src/main.rs', + 'utf8', + ); + const desktopAppSource = fs.readFileSync( + 'apps/desktop-shell/src-tauri/src/app.rs', + 'utf8', + ); + if (!desktopMainSource.includes('mod app;') || !desktopMainSource.includes('app::run();')) { + throw new Error('desktop shell main.rs must stay a thin app entrypoint'); + } + if ( + desktopMainSource.includes('tauri::Builder::default()') || + desktopMainSource.includes('tauri::generate_handler!') + ) { + throw new Error('desktop shell main.rs must not own Tauri app setup'); + } + if ( + !desktopAppSource.includes('tauri::Builder::default()') || + !desktopAppSource.includes('crate::host_bridge::host_bridge_request') + ) { + throw new Error('desktop shell app.rs must own Tauri app setup'); + } + const desktopHostBridgeRustFiles = fs .readdirSync('apps/desktop-shell/src-tauri/src/host_bridge', { withFileTypes: true }) .filter((entry) => entry.isFile() && entry.name.endsWith('.rs'))