跳到正文
灵动桌面框架
Esc
本栏目菜单

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 包裹整个路由,页面渲染出错时显示「页面出错了」和重试按钮