React 19 开发
灵动桌面框架前端基于 React 19(react / react-dom ^19.1.0)+ TypeScript 5.8(~5.8.3,strict 模式)构建,由 Vite 7 打包,使用函数组件和 Hooks 模式。
目录结构
src/
├── main.tsx # 入口:窗口显示兜底 + 渲染 <App />
├── App.tsx # 根组件:StyleProvider + ConfigProvider + 主题 + ErrorBoundary
├── Router.tsx # 路由表(createBrowserRouter)
├── pages/ # 页面(每个页面一个目录,index.tsx 默认导出)
│ ├── home/index.tsx
│ ├── settings/index.tsx
│ └── about/index.tsx
├── components/
│ ├── layout/ # AppLayout / Sidebar / WindowControls
│ ├── ui/ # ErrorBoundary / ThemeToggle / UpdateModal
│ └── GlobalNativeTooltip.tsx
├── hooks/useCommand.ts # useCommand Hook + safeInvoke
├── lib/api/ # invoke 封装(client / system / config / updater)
├── store/ # Zustand Store(app / settings)
├── theme/antdTheme.ts # Ant Design 亮色 / 暗色主题
├── styles/ # global.css / variables.css
├── types/ # 类型定义(config / system)
└── mobile/ # 移动端独立 SPA(单独的 vite 配置)
入口结构
// src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { getCurrentWindow } from "@tauri-apps/api/window";
import App from "./App";
import "./styles/global.css";
// WebView2 初始化可能阻塞事件循环导致窗口不可见,多次调用 show() 兜底
const appWindow = getCurrentWindow();
appWindow.show().catch(() => {});
setTimeout(() => appWindow.show().catch(() => {}), 500);
setTimeout(() => appWindow.show().catch(() => {}), 5000);
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
// src/App.tsx
import { useEffect, useState } from "react";
import { ConfigProvider } from "antd";
import { StyleProvider } from "@ant-design/cssinjs";
import GlobalNativeTooltip from "@/components/GlobalNativeTooltip";
import zhCN from "antd/locale/zh_CN";
import { ErrorBoundary } from "@/components/ui/ErrorBoundary";
import { useAppStore } from "@/store";
import { resolveTheme } from "@/store/app";
import { getAntdTheme } from "@/theme/antdTheme";
import { AppRouter } from "@/Router";
function App() {
const appTheme = useAppStore((s) => s.theme);
const [resolved, setResolved] = useState<"light" | "dark">(
resolveTheme(appTheme)
);
// 解析 light / dark / system,并把结果写入 <html data-theme>(详见「主题切换」)
// ...
return (
<StyleProvider layer>
<ConfigProvider locale={zhCN} theme={getAntdTheme(resolved)}>
<ErrorBoundary>
<AppRouter />
</ErrorBoundary>
<GlobalNativeTooltip />
</ConfigProvider>
</StyleProvider>
);
}
export default App;
页面组件示例
// src/pages/home/index.tsx(节选)
import { useState } from "react";
import { Card, Input, Button, Typography, Space, message } from "antd";
import { systemApi } from "@/lib/api";
const { Title, Text } = Typography;
export default function HomePage() {
const [name, setName] = useState("");
const [greeting, setGreeting] = useState("");
async function handleGreet() {
if (!name.trim()) {
message.warning("请输入名称");
return;
}
try {
const result = await systemApi.greet(name);
setGreeting(result);
} catch (e) {
message.error(String(e));
}
}
return (
<div className="max-w-2xl mx-auto">
<Title level={3}>首页</Title>
<Card title="Greet 示例" className="mt-6">
<Space.Compact style={{ width: "100%" }}>
<Input
placeholder="输入你的名字..."
value={name}
onChange={(e) => setName(e.target.value)}
onPressEnter={handleGreet}
/>
<Button type="primary" onClick={handleGreet}>
问候
</Button>
</Space.Compact>
{greeting && (
<div className="mt-4 p-3 rounded-lg">
<Text>{greeting}</Text>
</div>
)}
</Card>
</div>
);
}
调用后端
所有 invoke() 调用按业务模块封装在 src/lib/api/,页面只引用封装后的对象:
// src/lib/api/system.ts
import { invoke } from "./client";
import type { SystemInfo } from "@/types";
export const systemApi = {
greet: (name: string) => invoke<string>("greet", { name }),
getSystemInfo: () => invoke<SystemInfo>("get_system_info"),
};
src/lib/api/index.ts 统一导出 systemApi、configApi、updaterApi,以及错误解析工具 parseCommandError / getErrorMessage / getErrorCode(把 Rust 端返回的 { code, message } 结构化错误解析出来)。
需要 loading / error 状态时,可以用 src/hooks/useCommand.ts 里的 useCommand。参数在创建 Hook 时传入,execute() 本身不接收参数:
import { useCommand } from "@/hooks/useCommand";
const { data, error, loading, execute } = useCommand<string>("greet", { name });
// 点击时调用 execute(),失败时 error 为错误文本、返回 null
同文件还导出 safeInvoke<T>(command, args):失败时打印 console.error 并抛出 Error。
路径别名
vite.config.ts 配置了 @/ 路径别名指向 src/ 目录,tsconfig.json 里同步配置了 "paths": { "@/*": ["src/*"] }:
import path from "path";
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
},
}
// 使用别名导入
import { useAppStore } from "@/store";
import { systemApi, configApi } from "@/lib/api";
import type { AppConfig } from "@/types";
import { ErrorBoundary } from "@/components/ui/ErrorBoundary";
TIP
组件统一使用具名导出(export function AppLayout、export class ErrorBoundary),只有页面组件和 App 用默认导出。
开发规范
禁止
- 使用
any类型 — 必须严格 TypeScript - 直接调用
invoke()— 封装到src/lib/api/ - 使用
fetch()请求外部 API — 通过 Rust Command 代理 - 导入 Node.js 模块 — WebView 环境不支持
- 硬编码文件路径 — 使用 Tauri Path API
推荐
- 使用函数组件 + Hooks(
ErrorBoundary是唯一的 class 组件,React 错误边界只能用 class 实现) - 使用 Ant Design 组件,保持 UI 一致性
- 使用 TailwindCSS 原子类处理间距和布局
App.tsx已用ErrorBoundary包裹整个路由,页面渲染出错时显示「页面出错了」和重试按钮