diff --git a/src-tauri/AGENTS.md b/src-tauri/AGENTS.md index a6dba66..11e5b88 100644 --- a/src-tauri/AGENTS.md +++ b/src-tauri/AGENTS.md @@ -8,6 +8,8 @@ - **后端**: Rust (Edition 2021) - **架构**: Sidecar 模式 - Sidecar Server 承载主要业务逻辑 - **设计理念**: Tauri 仅提供原生桌面能力(文件对话框、系统通知等),Web 逻辑全部由 Sidecar Server 处理 +- **开发模式**: 使用 localhost:3000(需手动启动开发服务器) +- **生产模式**: 自动启动 Sidecar 二进制 - **异步运行时**: Tokio - **Rust 版本**: 1.92.0+ - **工具管理**: 使用 mise 管理 Rust 和 Tauri CLI 版本(见 `mise.toml`) @@ -16,13 +18,22 @@ ### 开发运行 ```bash -# 开发模式运行 (带 hot-reload) +# 开发模式运行 (需要先启动开发服务器) +# 终端 1: 启动前端开发服务器 +bun run dev + +# 终端 2: 启动 Tauri 应用 tauri dev -# 仅运行 Rust 二进制 (不推荐,需要手动启动 Sidecar Server) -cargo run +# 或者使用单命令并行启动(需要配置 package.json) +bun run dev:tauri ``` +**开发模式说明**: +- 开发模式下,Tauri 直接连接到 `localhost:3000`(不启动 sidecar 二进制) +- 需要手动运行 `bun run dev` 来启动开发服务器 +- 支持热重载(HMR),无需重启 Tauri 应用 + ### 构建 ```bash # 开发构建 (debug mode) @@ -323,12 +334,17 @@ tokio = { version = "1", features = ["net"] } ## 最佳实践 -1. **进程生命周期**: 始终在应用退出时清理子进程和资源 -2. **端口管理**: 使用端口扫描避免硬编码端口冲突 -3. **超时处理**: 异步操作设置合理的超时时间 (如 5 秒) -4. **日志**: 使用表情符号 (✓/✗) 和中文消息提供清晰的状态反馈 -5. **错误退出**: 关键错误时调用 `std::process::exit(1)` -6. **窗口配置**: 使用 `WebviewWindowBuilder` 动态创建窗口 +1. **开发环境配置**: + - 开发模式下需先启动前端开发服务器(`bun run dev`),再启动 Tauri(`tauri dev`) + - 生产构建自动打包 sidecar 二进制,无需额外配置 +2. **进程生命周期**: 始终在应用退出时清理子进程和资源 +3. **端口管理**: + - 开发模式固定使用 3000 端口(与开发服务器匹配) + - 生产模式使用端口扫描避免硬编码端口冲突 +4. **超时处理**: 异步操作设置合理的超时时间 (如 5 秒) +5. **日志**: 使用表情符号 (✓/✗/🔧/🚀) 和中文消息提供清晰的状态反馈 +6. **错误退出**: 关键错误时调用 `std::process::exit(1)` +7. **窗口配置**: 使用 `WebviewWindowBuilder` 动态创建窗口 ## 提交代码前检查清单 diff --git a/src-tauri/src/sidecar.rs b/src-tauri/src/sidecar.rs index 25b9d58..6babe15 100644 --- a/src-tauri/src/sidecar.rs +++ b/src-tauri/src/sidecar.rs @@ -13,6 +13,9 @@ const STARTUP_TIMEOUT_SECS: u64 = 5; /// 默认起始端口 const DEFAULT_PORT: u16 = 3000; +/// 开发模式使用的端口 +const DEV_PORT: u16 = 3000; + /// 端口扫描范围(从起始端口开始扫描的端口数量) const PORT_SCAN_RANGE: u16 = 100; @@ -30,13 +33,26 @@ const WINDOW_TITLE: &str = "Tauri App"; /// 全局状态:存储 Sidecar 进程句柄 pub struct SidecarProcess(pub Mutex>); -// 检查端口是否可用 +// 检查端口是否可用(未被占用) async fn is_port_available(port: u16) -> bool { tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port)) .await .is_ok() } +// 检查端口是否被占用(服务器正在监听) +async fn is_port_in_use(port: u16) -> bool { + use tokio::io::AsyncWriteExt; + + match tokio::net::TcpStream::connect(format!("127.0.0.1:{}", port)).await { + Ok(mut stream) => { + let _ = stream.shutdown().await; + true + } + Err(_) => false, + } +} + // 查找可用端口 async fn find_available_port(start: u16) -> u16 { for port in start..start + PORT_SCAN_RANGE { @@ -50,70 +66,117 @@ async fn find_available_port(start: u16) -> u16 { /// 启动 Sidecar 进程并创建主窗口 pub fn spawn_sidecar(app_handle: tauri::AppHandle) { tauri::async_runtime::spawn(async move { - // 查找可用端口 - let port = find_available_port(DEFAULT_PORT).await; - println!("使用端口: {}", port); + // 检测是否为开发模式 + let is_dev = cfg!(debug_assertions); - // 启动 sidecar - let sidecar = app_handle - .shell() - .sidecar("server") - .expect("无法找到 server") - .env("NITRO_PORT", port.to_string()); + if is_dev { + // 开发模式:直接连接到 localhost:3000 + println!("🔧 开发模式:使用本地开发服务器 (localhost:{})", DEV_PORT); - let (mut rx, child) = sidecar.spawn().expect("启动 sidecar 失败"); + // 等待开发服务器就绪(可选:添加重试逻辑) + let max_retries = 10; + let retry_delay = Duration::from_millis(500); + let mut server_ready = false; - // 保存进程句柄到全局状态 - if let Some(state) = app_handle.try_state::() { - *state.0.lock().unwrap() = Some(child); - } - - // 监听 stdout,等待服务器就绪信号 - let start_time = std::time::Instant::now(); - let timeout = Duration::from_secs(STARTUP_TIMEOUT_SECS); - let mut server_ready = false; - - while let Some(event) = rx.recv().await { - if let CommandEvent::Stdout(line) = event { - let output = String::from_utf8_lossy(&line); - println!("Server: {}", output); - - // 检测服务器启动成功的标志 - if output.contains("Listening on:") || output.contains("localhost") { + for attempt in 1..=max_retries { + if is_port_in_use(DEV_PORT).await { server_ready = true; - println!("✓ Server 启动成功!"); + println!("✓ 开发服务器已就绪 (端口 {})", DEV_PORT); + break; + } + println!( + "⏳ 等待开发服务器启动... (尝试 {}/{})", + attempt, max_retries + ); + tokio::time::sleep(retry_delay).await; + } - // 创建主窗口 - let url = format!("http://localhost:{}", port); - tauri::WebviewWindowBuilder::new( - &app_handle, - "main", - tauri::WebviewUrl::External(url.parse().unwrap()), - ) - .title(WINDOW_TITLE) - .inner_size(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT) - .center() - .build() - .expect("创建窗口失败"); + if !server_ready { + eprintln!("✗ 开发服务器未就绪,请确保运行了 `bun run dev`"); + std::process::exit(1); + } + // 创建主窗口 + let url = format!("http://localhost:{}", DEV_PORT); + tauri::WebviewWindowBuilder::new( + &app_handle, + "main", + tauri::WebviewUrl::External(url.parse().unwrap()), + ) + .title(WINDOW_TITLE) + .inner_size(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT) + .center() + .build() + .expect("创建窗口失败"); + } else { + // 生产模式:启动 sidecar 二进制 + println!("🚀 生产模式:启动 Sidecar Server"); + + // 查找可用端口 + let port = find_available_port(DEFAULT_PORT).await; + println!("使用端口: {}", port); + + // 启动 sidecar + let sidecar = app_handle + .shell() + .sidecar("server") + .expect("无法找到 server") + .env("NITRO_PORT", port.to_string()); + + let (mut rx, child) = sidecar.spawn().expect("启动 sidecar 失败"); + + // 保存进程句柄到全局状态 + if let Some(state) = app_handle.try_state::() { + *state.0.lock().unwrap() = Some(child); + } + + // 监听 stdout,等待服务器就绪信号 + let start_time = std::time::Instant::now(); + let timeout = Duration::from_secs(STARTUP_TIMEOUT_SECS); + let mut server_ready = false; + + while let Some(event) = rx.recv().await { + if let CommandEvent::Stdout(line) = event { + let output = String::from_utf8_lossy(&line); + println!("Server: {}", output); + + // 检测服务器启动成功的标志 + if output.contains("Listening on:") || output.contains("localhost") { + server_ready = true; + println!("✓ Server 启动成功!"); + + // 创建主窗口 + let url = format!("http://localhost:{}", port); + tauri::WebviewWindowBuilder::new( + &app_handle, + "main", + tauri::WebviewUrl::External(url.parse().unwrap()), + ) + .title(WINDOW_TITLE) + .inner_size(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT) + .center() + .build() + .expect("创建窗口失败"); + + break; + } + } + + // 超时检查 + if start_time.elapsed() > timeout { + eprintln!( + "✗ 启动超时: Server 未能在 {} 秒内启动", + STARTUP_TIMEOUT_SECS + ); break; } } - // 超时检查 - if start_time.elapsed() > timeout { - eprintln!( - "✗ 启动超时: Server 未能在 {} 秒内启动", - STARTUP_TIMEOUT_SECS - ); - break; + if !server_ready { + eprintln!("✗ Server 启动失败"); + std::process::exit(1); } } - - if !server_ready { - eprintln!("✗ Server 启动失败"); - std::process::exit(1); - } }); } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 3d47828..9cd0be4 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -5,7 +5,6 @@ "identifier": "com.imbytecat.tauri-shell", "build": { "beforeDevCommand": "bun run dev:vite", - "devUrl": "http://localhost:3000", "beforeBuildCommand": "bun run build" }, "app": {