mcpField
A
adding-tauri-splashscreen

adding-tauri-splashscreen

github.com/dchuk开发36152 次安装40 周前更新
代码仓库

关于 adding-tauri-splashscreen

面向 Tauri v2 应用的启动画面实现指南,涵盖窗口配置以及前后端初始化协调,待准备完成后再显示主窗口。

主要功能

  1. 01配置启动画面和主窗口
  2. 02协调前后端启动流程
  3. 03关闭启动画面并显示主窗口
  4. 04设计简约、进度条和深色画面
  5. 05处理初始化错误和显示时机

适用场景

  1. 01在 Tauri 应用初始化时显示加载画面
  2. 02初始化完成前隐藏主窗口
  3. 03排查启动画面的显示和位置问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dchuk/claude-code-tauri-skills --skill adding-tauri-splashscreen

安装后在 Claude Code 中输入/adding-tauri-splashscreen确认已加载。

详细介绍

Tauri 启动画面实现

本技能介绍如何在 Tauri v2 应用中实现启动画面。应用启动时,主窗口加载和初始化期间会显示启动画面。

概述

启动画面的实现模式包括:

  1. 启动后立即显示启动窗口
  2. 主窗口就绪前将其隐藏
  3. 执行初始化任务(前端和后端)
  4. 初始化完成后关闭启动画面并显示主窗口

配置

窗口配置

在 tauri.conf.json 中配置这两个窗口:

{
  "app": {
    "windows": [
      {
        "label": "main",
        "title": "My Application",
        "width": 1200,
        "height": 800,
        "visible": false
      },
      {
        "label": "splashscreen",
        "title": "Loading",
        "url": "splashscreen.html",
        "width": 400,
        "height": 300,
        "center": true,
        "resizable": false,
        "decorations": false,
        "transparent": true,
        "alwaysOnTop": true
      }
    ]
  }
}

关键设置:

  • 主窗口设置 "visible": false,让它在就绪前保持隐藏
  • "url": "splashscreen.html",指向启动画面 HTML
  • "decorations": false,移除窗口边框,使界面更简洁
  • "transparent": true,启用透明背景
  • "alwaysOnTop": true,确保加载期间启动画面始终可见

启动画面 HTML

在前端源代码目录(例如 src/ 或 public/)中创建 splashscreen.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Loading</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    html, body {
      height: 100%;
      overflow: hidden;
      background: transparent;
    }

    .splash-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      height: 100vh;
      background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
      border-radius: 12px;
      color: white;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    }

    .logo {
      width: 80px;
      height: 80px;
      margin-bottom: 24px;
    }

    .app-name {
      font-size: 24px;
      font-weight: 600;
      margin-bottom: 16px;
    }

    .loading-spinner {
      width: 40px;
      height: 40px;
      border: 3px solid rgba(255, 255, 255, 0.3);
      border-top-color: #4f46e5;
      border-radius: 50%;
      animation: spin 1s linear infinite;
    }

    @keyframes spin {
      to { transform: rotate(360deg); }
    }

    .loading-text {
      margin-top: 16px;
      font-size: 14px;
      color: rgba(255, 255, 255, 0.7);
    }
  </style>
</head>
<body>
  <div class="splash-container">
    
    <svg class="logo" viewBox="0 0 100 100" fill="none">
      <circle cx="50" cy="50" r="45" stroke="#4f46e5" stroke-width="4"/>
      <path d="M30 50 L45 65 L70 35" stroke="#4f46e5" stroke-width="4" fill="none"/>
    </svg>
    <div class="app-name">My Application</div>
    <div class="loading-spinner"></div>
    <div class="loading-text">Loading...</div>
  </div>
</body>
</html>

前端设置

TypeScript/JavaScript 实现

在主入口文件(例如 src/main.ts)中:

import { invoke } from '@tauri-apps/api/core';

// Helper function for delays
function sleep(seconds: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, seconds * 1000));
}

// Frontend initialization
async function initializeFrontend(): Promise<void> {
  // Perform frontend setup tasks here:
  // - Load configuration
  // - Initialize state management
  // - Set up routing
  // - Preload critical assets

  // Example: simulate initialization time
  await sleep(1);

  // Notify backend that frontend is ready
  await invoke('set_complete', { task: 'frontend' });
}

// Start initialization when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
  initializeFrontend().catch(console.error);
});
替代方案:使用窗口事件
import { invoke } from '@tauri-apps/api/core';
import { getCurrentWindow } from '@tauri-apps/api/window';

async function initializeFrontend(): Promise<void> {
  // Your initialization logic
  const config = await loadConfig();
  await setupRouter();
  await preloadAssets();

  // Signal completion
  await invoke('set_complete', { task: 'frontend' });
}

// Wait for window to be fully ready
getCurrentWindow().once('tauri://created', () => {
  initializeFrontend();
});

后端设置

添加 Tokio 依赖
cargo add tokio --features time
Rust 实现

在 src-tauri/src/lib.rs 中:

use std::sync::Mutex;
use tauri::{AppHandle, Manager, State};

// Track initialization state
struct SetupState {
    frontend_task: bool,
    backend_task: bool,
}

impl Default for SetupState {
    fn default() -> Self {
        Self {
            frontend_task: false,
            backend_task: false,
        }
    }
}

// Command to mark tasks complete
#[tauri::command]
async fn set_complete(
    app: AppHandle,
    state: State<'_, Mutex<SetupState>>,
    task: String,
) -> Result<(), String> {
    let mut state = state.lock().map_err(|e| e.to_string())?;

    match task.as_str() {
        "frontend" => state.frontend_task = true,
        "backend" => state.backend_task = true,
        _ => return Err(format!("Unknown task: {}", task)),
    }

    // Check if all tasks are complete
    if state.frontend_task && state.backend_task {
        // Close splash and show main window
        if let Some(splash) = app.get_webview_window("splashscreen") {
            splash.close().map_err(|e| e.to_string())?;
        }

        if let Some(main) = app.get_webview_window("main") {
            main.show().map_err(|e| e.to_string())?;
            main.set_focus().map_err(|e| e.to_string())?;
        }
    }

    Ok(())
}

// Backend initialization
async fn setup_backend(app: AppHandle) {
    // IMPORTANT: Use tokio::time::sleep, NOT std::thread::sleep
    // std::thread::sleep blocks the entire async runtime

    // Perform backend initialization:
    // - Database connections
    // - Load configuration
    // - Initialize services

    // Example: simulate work
    tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;

    // Mark backend as complete
    if let Some(state) = app.try_state::<Mutex<SetupState>>() {
        let _ = set_complete(
            app.clone(),
            state,
            "backend".to_string(),
        ).await;
    }
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .manage(Mutex::new(SetupState::default()))
        .invoke_handler(tauri::generate_handler![set_complete])
        .setup(|app| {
            let handle = app.handle().clone();

            // Spawn backend initialization
            tauri::async_runtime::spawn(async move {
                setup_backend(handle).await;
            });

            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

简单实现

对于只需等待前端就绪的简单情况:

配置
{
  "app": {
    "windows": [
      {
        "label": "main",
        "visible": false
      },
      {
        "label": "splashscreen",
        "url": "splashscreen.html",
        "width": 400,
        "height": 300,
        "decorations": false
      }
    ]
  }
}
前端
import { invoke } from '@tauri-apps/api/core';

async function init() {
  // Initialize your app
  await setupApp();

  // Close splash, show main
  await invoke('close_splashscreen');
}

document.addEventListener('DOMContentLoaded', init);
后端
use tauri::{AppHandle, Manager};

#[tauri::command]
async fn close_splashscreen(app: AppHandle) -> Result<(), String> {
    if let Some(splash) = app.get_webview_window("splashscreen") {
        splash.close().map_err(|e| e.to_string())?;
    }

    if let Some(main) = app.get_webview_window("main") {
        main.show().map_err(|e| e.to_string())?;
        main.set_focus().map_err(|e| e.to_string())?;
    }

    Ok(())
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![close_splashscreen])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

样式变体

极简启动画面
<style>
  .splash-container {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100vh;
    background: #ffffff;
  }

  .logo {
    width: 120px;
    animation: pulse 2s ease-in-out infinite;
  }

  @keyframes pulse {
    0%, 100% { opacity: 1; transform: scale(1); }
    50% { opacity: 0.7; transform: scale(0.95); }
  }
</style>
进度条启动画面
<style>
  .progress-container {
    width: 200px;
    height: 4px;
    background: rgba(255, 255, 255, 0.2);
    border-radius: 2px;
    overflow: hidden;
    margin-top: 20px;
  }

  .progress-bar {
    height: 100%;
    background: #4f46e5;
    animation: progress 2s ease-in-out infinite;
  }

  @keyframes progress {
    0% { width: 0%; }
    50% { width: 70%; }
    100% { width: 100%; }
  }
</style>

<div class="progress-container">
  <div class="progress-bar"></div>
</div>
带光晕的深色主题
<style>
  .splash-container {
    background: #0a0a0a;
    color: #ffffff;
  }

  .logo {
    filter: drop-shadow(0 0 20px rgba(79, 70, 229, 0.5));
  }

  .app-name {
    text-shadow: 0 0 20px rgba(79, 70, 229, 0.5);
  }
</style>

重要说明

  1. 异步休眠:在异步 Rust 代码中始终使用 tokio::time::sleep,不要使用 std::thread::sleep。后者会阻塞整个运行时。

  2. 窗口标签:确保代码中的窗口标签与 tauri.conf.json 中的标签一致。

  3. 错误处理:启动画面应能妥善处理错误。如果初始化失败,仍应显示主窗口,并呈现错误状态。

  4. 显示时长:启动画面应保持足够长的显示时间以展示品牌,但不要久到让用户感到厌烦。建议至少显示 1-3 秒。

  5. 透明窗口:使用 transparent: true 时,确保 HTML 中 html 和 body 元素的背景设为 background: transparent。

  6. 移动端注意事项:启动画面在移动平台上的工作方式有所不同。考虑为 iOS 和 Android 使用平台原生启动画面。

故障排除

启动画面未显示:

  • 确认 tauri.conf.json 中的 URL 路径正确
  • 检查 HTML 文件是否位于正确位置

主窗口过早显示:

  • 确保主窗口设置了 visible: false
  • 确认 set_complete 命令已正确调用

透明背景无效:

  • 在窗口配置中设置 transparent: true
  • 在 CSS 中将 html 和 body 的背景设为 background: transparent
  • 在某些平台上,可能还需要设置 decorations: false

窗口位置问题:

  • 使用 center: true 将启动画面居中
  • 或指定明确的 x 和 y 坐标

评论

支持 Markdown