React集成 #

一、项目创建 #

1.1 使用脚手架创建 #

bash
# 创建 React + Tauri 项目
npm create tauri-app@latest my-react-app

# 选择配置
✔ Project name · my-react-app
✔ Choose which language to use for your frontend · TypeScript / JavaScript
✔ Choose your package manager · pnpm
✔ Choose your UI template · React
✔ Choose your UI flavor · TypeScript

1.2 项目结构 #

text
my-react-app/
├── src/
│   ├── components/
│   │   ├── Header.tsx
│   │   └── Sidebar.tsx
│   ├── hooks/
│   │   └── useTauri.ts
│   ├── App.tsx
│   ├── main.tsx
│   └── App.css
├── src-tauri/
│   ├── src/
│   │   └── lib.rs
│   ├── Cargo.toml
│   └── tauri.conf.json
├── index.html
├── package.json
├── vite.config.ts
└── tsconfig.json

1.3 安装依赖 #

bash
# 安装 Tauri API
pnpm add @tauri-apps/api

# 安装常用插件
pnpm add @tauri-apps/plugin-shell
pnpm add @tauri-apps/plugin-dialog
pnpm add @tauri-apps/plugin-fs

二、基础配置 #

2.1 Vite 配置 #

typescript
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
    plugins: [react()],
    clearScreen: false,
    server: {
        port: 1420,
        strictPort: true,
    },
    envPrefix: ['VITE_', 'TAURI_'],
    build: {
        target: ['es2021', 'chrome100', 'safari13'],
        minify: !process.env.TAURI_DEBUG ? 'esbuild' : false,
        sourcemap: !!process.env.TAURI_DEBUG,
    },
});

2.2 TypeScript 配置 #

json
// tsconfig.json
{
    "compilerOptions": {
        "target": "ES2020",
        "useDefineForClassFields": true,
        "lib": ["ES2020", "DOM", "DOM.Iterable"],
        "module": "ESNext",
        "skipLibCheck": true,
        "moduleResolution": "bundler",
        "allowImportingTsExtensions": true,
        "resolveJsonModule": true,
        "isolatedModules": true,
        "noEmit": true,
        "jsx": "react-jsx",
        "strict": true,
        "noUnusedLocals": true,
        "noUnusedParameters": true,
        "noFallthroughCasesInSwitch": true
    },
    "include": ["src"],
    "references": [{ "path": "./tsconfig.node.json" }]
}

三、Tauri Hooks #

3.1 创建自定义 Hooks #

typescript
// hooks/useTauri.ts
import { useState, useEffect, useCallback } from 'react';
import { invoke } from '@tauri-apps/api/core';

export function useCommand<T, P = void>(
    command: string,
    initialParams?: P
) {
    const [data, setData] = useState<T | null>(null);
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState<string | null>(null);

    const execute = useCallback(async (params?: P) => {
        setLoading(true);
        setError(null);
        
        try {
            const result = await invoke<T>(command, params ?? initialParams);
            setData(result);
            return result;
        } catch (err) {
            setError(String(err));
            throw err;
        } finally {
            setLoading(false);
        }
    }, [command, initialParams]);

    return { data, loading, error, execute };
}

3.2 使用命令 Hook #

tsx
import { useCommand } from './hooks/useTauri';

interface User {
    id: number;
    name: string;
}

function UserProfile() {
    const { data: user, loading, error, execute } = useCommand<User>('get_user');

    useEffect(() => {
        execute({ id: 1 });
    }, []);

    if (loading) return <div>Loading...</div>;
    if (error) return <div>Error: {error}</div>;
    if (!user) return null;

    return (
        <div>
            <h2>{user.name}</h2>
        </div>
    );
}

3.3 事件监听 Hook #

typescript
// hooks/useEvent.ts
import { useEffect } from 'react';
import { listen, UnlistenFn } from '@tauri-apps/api/event';

export function useEvent<T>(
    event: string,
    callback: (payload: T) => void
) {
    useEffect(() => {
        let unlisten: UnlistenFn;

        listen<T>(event, (e) => {
            callback(e.payload);
        }).then((fn) => {
            unlisten = fn;
        });

        return () => {
            if (unlisten) {
                unlisten();
            }
        };
    }, [event, callback]);
}

3.4 窗口状态 Hook #

typescript
// hooks/useWindowState.ts
import { useState, useEffect } from 'react';
import { getCurrentWindow } from '@tauri-apps/api/window';

export function useWindowState() {
    const [isMaximized, setIsMaximized] = useState(false);
    const window = getCurrentWindow();

    useEffect(() => {
        window.isMaximized().then(setIsMaximized);

        const unlisten = window.onMaximized(({ payload }) => {
            setIsMaximized(payload);
        });

        return () => {
            unlisten.then((fn) => fn());
        };
    }, []);

    const toggleMaximize = async () => {
        await window.toggleMaximize();
    };

    const minimize = async () => {
        await window.minimize();
    };

    const close = async () => {
        await window.close();
    };

    return { isMaximized, toggleMaximize, minimize, close };
}

四、组件开发 #

4.1 标题栏组件 #

tsx
// components/TitleBar.tsx
import { useWindowState } from '../hooks/useWindowState';
import './TitleBar.css';

export function TitleBar() {
    const { isMaximized, toggleMaximize, minimize, close } = useWindowState();

    return (
        <div className="titlebar" data-tauri-drag-region>
            <div className="titlebar-title">My App</div>
            <div className="titlebar-controls">
                <button onClick={minimize} className="titlebar-button">
                    <svg width="12" height="12" viewBox="0 0 12 12">
                        <rect y="5" width="12" height="2" />
                    </svg>
                </button>
                <button onClick={toggleMaximize} className="titlebar-button">
                    {isMaximized ? (
                        <svg width="12" height="12" viewBox="0 0 12 12">
                            <rect x="2" y="4" width="6" height="6" fill="none" stroke="currentColor" />
                            <path d="M4 4V2h6v6h-2" fill="none" stroke="currentColor" />
                        </svg>
                    ) : (
                        <svg width="12" height="12" viewBox="0 0 12 12">
                            <rect x="2" y="2" width="8" height="8" fill="none" stroke="currentColor" />
                        </svg>
                    )}
                </button>
                <button onClick={close} className="titlebar-button close">
                    <svg width="12" height="12" viewBox="0 0 12 12">
                        <path d="M1 1l10 10M11 1L1 11" stroke="currentColor" strokeWidth="2" />
                    </svg>
                </button>
            </div>
        </div>
    );
}
css
/* TitleBar.css */
.titlebar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    height: 32px;
    background: #1e1e1e;
    padding: 0 8px;
    user-select: none;
}

.titlebar-title {
    color: #fff;
    font-size: 13px;
}

.titlebar-controls {
    display: flex;
    gap: 4px;
}

.titlebar-button {
    width: 32px;
    height: 24px;
    border: none;
    background: transparent;
    color: #fff;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 4px;
}

.titlebar-button:hover {
    background: rgba(255, 255, 255, 0.1);
}

.titlebar-button.close:hover {
    background: #e81123;
}

4.2 文件选择组件 #

tsx
// components/FilePicker.tsx
import { useState } from 'react';
import { open } from '@tauri-apps/plugin-dialog';

interface FilePickerProps {
    onFileSelected: (path: string) => void;
    filters?: { name: string; extensions: string[] }[];
}

export function FilePicker({ onFileSelected, filters }: FilePickerProps) {
    const [selectedPath, setSelectedPath] = useState<string | null>(null);

    const handleOpen = async () => {
        const selected = await open({
            multiple: false,
            filters: filters || [
                { name: 'All Files', extensions: ['*'] }
            ],
        });

        if (selected) {
            setSelectedPath(selected as string);
            onFileSelected(selected as string);
        }
    };

    return (
        <div className="file-picker">
            <button onClick={handleOpen}>选择文件</button>
            {selectedPath && (
                <span className="selected-path">{selectedPath}</span>
            )}
        </div>
    );
}

4.3 通知组件 #

tsx
// components/Notification.tsx
import { useEffect, useState } from 'react';
import { listen } from '@tauri-apps/api/event';

interface NotificationData {
    title: string;
    message: string;
    type: 'info' | 'success' | 'error';
}

export function Notification() {
    const [notification, setNotification] = useState<NotificationData | null>(null);

    useEffect(() => {
        const unlisten = listen<NotificationData>('notification', (event) => {
            setNotification(event.payload);
            
            setTimeout(() => {
                setNotification(null);
            }, 3000);
        });

        return () => {
            unlisten.then((fn) => fn());
        };
    }, []);

    if (!notification) return null;

    return (
        <div className={`notification notification-${notification.type}`}>
            <strong>{notification.title}</strong>
            <p>{notification.message}</p>
        </div>
    );
}

五、状态管理 #

5.1 使用 Zustand #

bash
pnpm add zustand
typescript
// store/appStore.ts
import { create } from 'zustand';
import { invoke } from '@tauri-apps/api/core';

interface AppState {
    user: User | null;
    theme: 'light' | 'dark';
    setUser: (user: User | null) => void;
    setTheme: (theme: 'light' | 'dark') => void;
    loadUser: () => Promise<void>;
}

export const useAppStore = create<AppState>((set) => ({
    user: null,
    theme: 'light',
    setUser: (user) => set({ user }),
    setTheme: (theme) => set({ theme }),
    loadUser: async () => {
        const user = await invoke<User>('get_current_user');
        set({ user });
    },
}));

5.2 使用 Store #

tsx
import { useAppStore } from './store/appStore';

function App() {
    const { user, theme, loadUser } = useAppStore();

    useEffect(() => {
        loadUser();
    }, []);

    return (
        <div className={`app ${theme}`}>
            {user && <h1>Welcome, {user.name}</h1>}
        </div>
    );
}

六、路由配置 #

6.1 安装 React Router #

bash
pnpm add react-router-dom

6.2 配置路由 #

tsx
// App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { Home } from './pages/Home';
import { Settings } from './pages/Settings';
import { About } from './pages/About';

function App() {
    return (
        <BrowserRouter>
            <Routes>
                <Route path="/" element={<Home />} />
                <Route path="/settings" element={<Settings />} />
                <Route path="/about" element={<About />} />
            </Routes>
        </BrowserRouter>
    );
}

export default App;

七、样式方案 #

7.1 CSS Modules #

tsx
// components/Button/Button.tsx
import styles from './Button.module.css';

interface ButtonProps {
    children: React.ReactNode;
    variant?: 'primary' | 'secondary';
    onClick?: () => void;
}

export function Button({ children, variant = 'primary', onClick }: ButtonProps) {
    return (
        <button
            className={`${styles.button} ${styles[variant]}`}
            onClick={onClick}
        >
            {children}
        </button>
    );
}
css
/* Button.module.css */
.button {
    padding: 8px 16px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
}

.primary {
    background: #007bff;
    color: white;
}

.secondary {
    background: #6c757d;
    color: white;
}

7.2 Tailwind CSS #

bash
pnpm add -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
javascript
// tailwind.config.js
export default {
    content: [
        "./index.html",
        "./src/**/*.{js,ts,jsx,tsx}",
    ],
    theme: {
        extend: {},
    },
    plugins: [],
}
css
/* index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
tsx
// 使用 Tailwind
function Button({ children }: { children: React.ReactNode }) {
    return (
        <button className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
            {children}
        </button>
    );
}

八、最佳实践 #

8.1 错误边界 #

tsx
// components/ErrorBoundary.tsx
import { Component, ReactNode } from 'react';

interface Props {
    children: ReactNode;
}

interface State {
    hasError: boolean;
    error: Error | null;
}

export class ErrorBoundary extends Component<Props, State> {
    constructor(props: Props) {
        super(props);
        this.state = { hasError: false, error: null };
    }

    static getDerivedStateFromError(error: Error) {
        return { hasError: true, error };
    }

    render() {
        if (this.state.hasError) {
            return (
                <div className="error-boundary">
                    <h2>Something went wrong</h2>
                    <p>{this.state.error?.message}</p>
                </div>
            );
        }

        return this.props.children;
    }
}

8.2 懒加载 #

tsx
import { lazy, Suspense } from 'react';

const Settings = lazy(() => import('./pages/Settings'));

function App() {
    return (
        <Suspense fallback={<div>Loading...</div>}>
            <Settings />
        </Suspense>
    );
}

九、总结 #

9.1 核心要点 #

要点 说明
项目创建 使用 create-tauri-app
自定义 Hooks 封装 Tauri API 调用
组件开发 结合 Tauri 功能
状态管理 使用 Zustand 等库
样式方案 CSS Modules 或 Tailwind

9.2 下一步 #

现在你已经掌握了 React 集成,接下来让我们学习 Vue集成,了解如何在 Tauri 中使用 Vue 框架!

最后更新:2026-03-28