Chapter 03

前端框架集成

在 Tauri 中使用 React、Vue、Svelte,掌握 @tauri-apps/api 核心用法

@tauri-apps/api 包

这是 Tauri 官方提供的前端 JavaScript/TypeScript 库,封装了所有与 Rust 后端通信的 API:

# 安装(项目创建时通常已自动安装)
npm install @tauri-apps/api

# 各功能插件(按需安装)
npm install @tauri-apps/plugin-fs
npm install @tauri-apps/plugin-dialog
npm install @tauri-apps/plugin-notification
npm install @tauri-apps/plugin-store
npm install @tauri-apps/plugin-http

React + Vite 集成

vite.config.ts 配置

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  // Tauri 要求:防止 Vite 清除 Rust 错误
  clearScreen: false,
  server: {
    port: 1420,         // 与 tauri.conf.json devUrl 保持一致
    strictPort: true,   // 端口被占用时报错而非自动换端口
    host: false,         // 不对外暴露,仅本机访问
    hmr: {
      protocol: 'ws',
      host: 'localhost',
      port: 1420,
    },
    watch: {
      // 监听 Rust 文件变化(触发 Tauri 重新编译)
      ignored: ['**/src-tauri/**'],
    },
  },
  envPrefix: ['VITE_', 'TAURI_'], // 允许 TAURI_ 开头的环境变量
  build: {
    // Windows 上 Tauri 使用 Chromium,支持现代特性
    target: process.env.TAURI_ENV_PLATFORM === 'windows'
      ? 'chrome105'
      : 'safari13',
    minify: !process.env.TAURI_ENV_DEBUG ? 'esbuild' : false,
    sourcemap: !!process.env.TAURI_ENV_DEBUG,
  },
});

React 组件中调用 Tauri

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

function App() {
  const [message, setMessage] = useState('');
  const [name, setName] = useState('World');

  async function handleGreet() {
    // invoke 调用 Rust 中的 #[tauri::command] 函数
    const result = await invoke<string>('greet', { name });
    setMessage(result);
  }

  return (
    <div>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
        placeholder="Enter your name"
      />
      <button onClick={handleGreet}>Greet</button>
      <p>{message}</p>
    </div>
  );
}

Vue 3 + Vite 集成

<!-- src/App.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import { invoke } from '@tauri-apps/api/core';

const name = ref('World');
const message = ref('');

async function greet() {
  message.value = await invoke<string>('greet', { name: name.value });
}
</script>

<template>
  <div>
    <input v-model="name" placeholder="Enter your name" />
    <button @click="greet">Greet</button>
    <p>{{ message }}</p>
  </div>
</template>

SvelteKit 集成

SvelteKit 是 SSR 框架,在 Tauri 中需要禁用 SSR,使用纯客户端模式:

// svelte.config.js
import adapter from '@sveltejs/adapter-static';

export default {
  kit: {
    adapter: adapter(),
    prerender: {
      handleMissingID: 'ignore',
    },
  },
};
<!-- src/routes/+layout.ts -->
// 禁用 SSR,使用纯 SPA 模式
export const ssr = false;
export const prerender = true;

<!-- src/routes/+page.svelte -->
<script lang="ts">
  import { invoke } from '@tauri-apps/api/core';
  let message = '';
  let name = 'World';

  async function greet() {
    message = await invoke<string>('greet', { name });
  }
</script>

<input bind:value={name} />
<button on:click={greet}>Greet</button>
<p>{message}</p>

检测 Tauri 运行环境

同一份前端代码可能在浏览器和 Tauri 两种环境中运行。区分环境的常见方式:

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

// 方式1:官方 API(推荐)
if (isTauri()) {
  console.log('Running in Tauri');
} else {
  console.log('Running in browser');
}

// 方式2:检查 window.__TAURI__ 全局对象
const isTauriEnv = ('__TAURI__' in window);

// 方式3:Vite 环境变量(构建时替换)
// tauri dev/build 会设置 TAURI_ENV_PLATFORM
const platform = import.meta.env.TAURI_ENV_PLATFORM; // "macos" | "windows" | "linux"
const isDesktop = !!import.meta.env.TAURI_ENV_PLATFORM;

WebView 的限制与注意事项

无 Node.js API
WebView 中运行的是纯浏览器 JavaScript,没有 require()fspath 等 Node.js 模块。需要系统功能必须通过 Tauri Commands 调用 Rust 实现。
跨域限制
生产模式下前端通过 tauri://localhost 协议加载,向外部 API 发起请求受 CORS 限制。需要在 tauri.conf.json 配置 CSP,或使用 plugin-http 绕过限制。
WebView 版本差异
macOS WKWebKit 与 Windows WebView2 对某些 CSS/JS 特性支持程度不同。建议开发时同时在多平台测试,避免使用太新的实验性特性。
本地存储
localStoragesessionStorage 可正常使用,数据存储在 Tauri 应用的 Web 数据目录中。跨版本持久化建议使用 plugin-store

TypeScript 类型支持

// tsconfig.json 推荐配置
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "skipLibCheck": true,
    "types": ["@tauri-apps/api"]   // 引入 Tauri 类型声明
  }
}
// 为 invoke 返回值定义接口提供类型安全
interface SystemInfo {
  os: string;
  arch: string;
  version: string;
}

// 使用泛型参数指定返回类型
const info = await invoke<SystemInfo>('get_system_info');
console.log(info.os); // TypeScript 知道这是 string

// 传参类型安全
interface ReadFileArgs {
  path: string;
  encoding?: string;
}
const content = await invoke<string>('read_file', {
  path: '/tmp/test.txt',
  encoding: 'utf-8',
} satisfies ReadFileArgs);
使用 specta 自动生成 TypeScript 类型

specta 是一个 Rust 库,可以从 Rust 结构体自动生成对应的 TypeScript 类型定义,配合 tauri-specta 插件还能自动生成类型安全的 invoke 封装函数。这样修改 Rust 结构体后,TypeScript 类型会自动同步更新,避免手动维护类型定义。

使用 plugin-store 持久化数据

相比 localStorageplugin-store 提供了更可靠的持久化存储,数据存储在应用数据目录中,支持类型安全访问:

npm install @tauri-apps/plugin-store
import { load } from '@tauri-apps/plugin-store';

// 打开(或创建)一个 store 文件
// 文件存储在 appDataDir() 下的 settings.json
const store = await load('settings.json', { autoSave: true });

// 写入值
await store.set('theme', 'dark');
await store.set('fontSize', 16);
await store.set('recentFiles', ['/path/to/file1.txt', '/path/to/file2.txt']);

// 读取值(带默认值)
const theme = ((await store.get('theme')) ?? 'light') as string;
const fontSize = ((await store.get('fontSize')) ?? 14) as number;

// 手动保存(autoSave: false 时需要)
await store.save();

// 删除某个键
await store.delete('oldKey');

// 检查某个键是否存在
const hasTheme = await store.has('theme');
// capabilities/default.json — 需要添加 store 权限
{
  "permissions": [
    "store:allow-load",
    "store:allow-set",
    "store:allow-get",
    "store:allow-delete",
    "store:allow-save"
  ]
}

plugin-http:绕过 CORS 限制

Tauri 应用在生产模式下前端使用 tauri:// 协议,向外部 HTTP API 发请求时会遇到 CORS 问题。plugin-http 通过 Rust 层发起请求,完全绕过浏览器的跨域限制:

npm install @tauri-apps/plugin-http
import { fetch } from '@tauri-apps/plugin-http';

// 使用方式与 Web 标准 fetch API 完全相同
const response = await fetch('https://api.github.com/repos/tauri-apps/tauri', {
  method: 'GET',
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json',
  },
});

const data = await response.json();
console.log(data.stargazers_count);

// POST 请求
const result = await fetch('https://api.example.com/data', {
  method: 'POST',
  body: JSON.stringify({ key: 'value' }),
  headers: { 'Content-Type': 'application/json' },
});
// capabilities/default.json
{
  "permissions": [
    "http:default"    // 允许发起 HTTP 请求
  ]
}
HTTP 请求安全

plugin-http 默认允许请求任意 URL。出于安全考虑,可以在 capabilities 中配置 http:scope 限制只能访问特定域名:{"identifier": "http:scope", "allow": ["https://api.example.com/**"]}。这可以防止恶意代码通过注入攻击访问内网服务。

tauri-specta:自动生成类型安全绑定

手动维护 TypeScript 接口与 Rust 结构体的同步非常繁琐,tauri-specta 可以从 Rust 代码自动生成 TypeScript 类型定义和类型安全的调用函数:

// Cargo.toml
// specta = { version = "2", features = ["derive"] }
// tauri-specta = { version = "2", features = ["derive", "typescript"] }

use specta::Type;
use tauri_specta::{collect_commands, ts};

// 为结构体派生 Type 和 serde::Serialize/Deserialize
#[derive(Debug, serde::Serialize, serde::Deserialize, Type)]
pub struct SystemInfo {
    pub os: String,
    pub arch: String,
    pub cpu_count: u32,
    pub total_memory: u64,
}

#[tauri::command]
#[specta::specta]  // 标记此 command 用于类型导出
fn get_system_info() -> SystemInfo {
    SystemInfo {
        os: std::env::consts::OS.to_string(),
        arch: std::env::consts::ARCH.to_string(),
        cpu_count: num_cpus::get() as u32,
        total_memory: 0,
    }
}

fn main() {
    // 生成 bindings.ts 文件(开发时运行一次)
    #[cfg(debug_assertions)]
    ts::export(collect_commands![get_system_info], "../src/bindings.ts")
        .unwrap();

    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![get_system_info])
        .run(tauri::generate_context!())
        .unwrap();
}
// 自动生成的 src/bindings.ts(不要手动修改)
export type SystemInfo = {
  os: string;
  arch: string;
  cpuCount: number;   // Rust 的 snake_case 自动转为 camelCase
  totalMemory: number;
};

// 类型安全的封装函数(参数和返回类型都有 TypeScript 类型)
export function getSystemInfo() {
  return invoke<SystemInfo>('get_system_info');
}

// 使用:直接调用封装函数,自动类型推断
const info = await getSystemInfo();
console.log(info.cpuCount);  // TypeScript 知道这是 number

环境变量与构建配置

Tauri 在构建时会注入一系列环境变量,前端代码可以通过 import.meta.env 访问:

TAURI_ENV_PLATFORM
当前编译目标平台:"windows""macos""linux""ios""android"
TAURI_ENV_ARCH
目标架构:"x86""x86_64""aarch64"
TAURI_ENV_FAMILY
平台家族:"unix"(macOS/Linux)或 "windows"
TAURI_ENV_DEBUG
是否为调试模式:"true""false"
// vite.config.ts 中启用 TAURI_ 环境变量前缀
envPrefix: ['VITE_', 'TAURI_'],

// 前端代码中使用
const platform = import.meta.env.TAURI_ENV_PLATFORM;

// 平台特定功能
function getKeyboardShortcut(action: string) {
  const isMac = import.meta.env.TAURI_ENV_PLATFORM === 'macos';
  const modifier = isMac ? '⌘' : 'Ctrl';

  switch (action) {
    case 'save': return `${modifier}+S`;
    case 'open': return `${modifier}+O`;
    default: return action;
  }
}

本章小结

本章核心要点