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