Tauri桌面应用开发实战:从Electron到轻量化AI聊天客户端的架构演进

📅 2026/8/15 5:03:53
Tauri桌面应用开发实战:从Electron到轻量化AI聊天客户端的架构演进
1. 项目概述为什么是Tauri最近在折腾一个AI对话的桌面应用核心需求很简单一个能调用大模型API的聊天窗口加上历史记录和基本的UI交互。最开始像很多人一样我下意识地想到了Electron。毕竟生态成熟用JavaScript/TypeScript就能搞定前后端开发效率没得说。但当我看到打包出来的安装包动辄100MB起步运行时内存占用轻松突破200MB时心里就有点不是滋味了。我的应用逻辑并不复杂大部分“体重”都被Chromium内核和Node.js运行时给占了这感觉就像为了喝杯牛奶得养一头牛。就在这时Tauri进入了我的视野。它的核心理念非常吸引我用系统自带的WebView来渲染界面后端逻辑则用Rust编写最后通过一个极小的Rust二进制文件将一切打包起来。官方宣称其应用体积可以小到令人发指。对于一个AI Chat应用前端无非是HTML/CSS/JS后端主要是API调用和数据处理Tauri的方案看起来再合适不过。我决定动手试一试目标很明确用Tauri打造一个功能完整的AI桌面聊天应用并把它压缩到极致看看能否真的用几MB的体积实现Electron上百MB应用的功能。最终的结果让我非常满意。经过一系列优化产出的应用安装包在Windows上不到3MB在macOS上约为5MB启动速度飞快内存占用也只有Electron应用的零头。这个项目不仅验证了Tauri在生产环境下的可行性更让我深刻体会到在桌面开发领域“轻量化”和“高性能”不再是遥不可及的命题。下面我就把这个从技术选型、开发实战到极致优化的完整过程拆解给你看无论你是前端开发者想涉足桌面还是Rust爱好者寻找练手项目相信都能获得直接的参考。2. 核心架构与工具选型解析2.1 为什么放弃Electron不仅仅是体积问题选择Tauri而非Electron体积是最直观的驱动力但绝非唯一原因。我们需要深入技术底层去看差异。Electron的架构决定了它的“重”。每个Electron应用都捆绑了一个完整的Chromium浏览器实例和一个Node.js运行时。这带来了巨大的优势你拥有一个与Chrome几乎一致的、功能强大的渲染环境以及一个成熟的服务器端JavaScript运行时。但代价也显而易见体积膨胀即使是一个“Hello World”基础框架的体积也相当可观。内存占用高每个应用都是一个独立的浏览器进程内存开销与打开一个Chrome标签页类似。启动速度慢需要初始化整个浏览器引擎。安全更新滞后应用的Chromium版本依赖于Electron的更新周期可能无法及时获取最新的安全补丁。反观Tauri它采用了完全不同的思路前端与后端分离并最大化利用系统资源。前端使用各操作系统Windows、macOS、Linux系统原生的WebView组件如Windows上的WebView2 macOS上的WKWebView来渲染HTML/CSS/JS。这意味着你的UI渲染引擎是操作系统提供并维护的无需打包。后端应用的核心逻辑、系统交互、API调用等全部由Rust编写的二进制文件来执行。Rust以其卓越的性能、内存安全和零成本抽象著称。胶水层Tauri框架本身提供了一套安全的、基于消息传递的IPC进程间通信机制让前端JavaScript可以安全地调用后端Rust函数。这种架构带来的好处是颠覆性的体积极小最终分发给用户的主要就是你Rust代码编译后的二进制文件通常只有几MB加上前端静态资源HTML, JS, CSS。系统WebView是预装或按需下载的不包含在应用包内。性能优异Rust后端的性能远超Node.js对于数据处理、加密解密、文件IO等操作有巨大优势。系统WebView的启动和渲染速度也很快。内存占用低共享系统的WebView运行时如果多个Tauri应用同时运行且Rust后端本身非常节省内存。更安全Tauri的IPC默认是隔离的前端无法直接访问系统API必须通过显式定义并暴露的Rust命令Command来调用这极大地减少了攻击面。启动更快无需初始化庞大的Chromium启动几乎是瞬时的。对于我们的AI Chat应用后端需要处理网络请求调用OpenAI、Claude等API、管理对话历史可能涉及本地数据库操作、以及处理可能的文件上传。这些任务用Rust来实现不仅在性能上更有保障其强大的类型系统和错误处理也能让应用更加健壮。2.2 技术栈确定现代前端 强类型后端基于以上分析我确定了本项目的技术栈前端框架Vite React TypeScript。选择Vite是因为其极快的启动和热更新速度与Tauri的开发体验完美契合。React用于构建复杂的交互界面TypeScript则提供静态类型检查与后端的Rust形成“前后端双类型安全”的闭环极大减少运行时错误。UI组件库Tailwind CSS。采用实用优先的原子化CSS框架可以快速构建美观、响应式的界面且最终的CSS经过Purge优化后体积非常小符合我们“极致轻量”的目标。后端语言Rust。这是Tauri的基石。我们将用Rust编写所有业务逻辑并通过Tauri提供的#[tauri::command]宏将函数暴露给前端调用。状态与数据流前端状态使用React Context或Zustand这类轻量级状态管理库来管理UI状态如当前对话、输入框内容。后端数据持久化对于聊天记录、API密钥需加密存储等数据使用sqlx库配合SQLite数据库。SQLite是一个单文件数据库无需额外服务非常适合桌面应用。网络请求后端使用reqwest库Rust中最流行的HTTP客户端来处理所有与大模型API的通信。前端只负责UI渲染和用户输入收集所有网络请求都通过IPC发送到Rust后端执行。打包与分发使用Tauri CLI工具进行构建和打包它支持生成Windows.msi,.exe、macOS.app,.dmg和Linux.AppImage,.deb等多种平台的安装包。注意这里没有选择像Electron那样在前端直接使用axios或fetch调用API是出于安全性和架构清晰的考虑。将密钥和网络请求放在后端可以避免前端代码可能存在的密钥泄露风险也使得逻辑更加集中便于实现请求重试、速率限制、日志记录等高级功能。2.3 开发环境搭建要点搭建环境是第一步这里有几个关键点需要注意Rust环境通过rustup安装Rust是最佳实践。对于国内用户下载速度可能较慢可以配置国内镜像源加速。# 设置中科大镜像源在终端中执行 export RUSTUP_DIST_SERVERhttps://mirrors.ustc.edu.cn/rust-static export RUSTUP_UPDATE_ROOThttps://mirrors.ustc.edu.cn/rust-static/rustup # 然后安装rustup安装完成后使用rustc --version和cargo --version验证。系统WebView依赖Windows需要WebView2。Windows 10及以后版本大多已预装。如果未安装Tauri在首次运行或构建时会提示并引导安装或者应用包可以将其作为运行时依赖捆绑。macOSWKWebView是系统组件通常无需额外处理。Linux需要安装webkit2gtk等开发包。具体依赖请参考Tauri官方文档不同发行版命令不同。Tauri CLI使用Cargo安装Tauri命令行工具。cargo install tauri-cli也可以在前端项目中通过tauri-apps/cli的npm包来调用但直接使用Cargo安装的CLI功能更完整。创建项目官方推荐使用create-tauri-app脚手架它能一键生成整合了Vite和前端框架的项目结构。npm create tauri-applatest根据提示选择Vite作为构建工具然后选择ReactTypeScript的模板。完成上述步骤后一个标准的Tauri项目就初始化好了。项目根目录会包含一个src-tauri文件夹存放Rust后端代码和一个前端项目文件夹通常是基于Vite的。运行npm run tauri dev即可启动开发服务器并打开一个嵌入了前端页面的原生窗口。3. 应用核心功能实现拆解3.1 前端界面构建极简聊天室AI Chat的前端界面相对经典一个消息列表、一个输入区、一个发送按钮侧边栏可能还有会话历史。使用React和Tailwind CSS可以快速实现。核心组件设计ChatWindow主聊天窗口包含MessageList和MessageInput。MessageList渲染对话历史区分用户消息和AI消息并应用不同的样式。MessageInput一个支持多行输入的文本框附带发送按钮。需要处理Enter键发送CtrlEnter换行的逻辑。Sidebar会话历史列表可以创建新会话、切换会话、删除会话。状态管理我选择了Zustand因为它足够轻量且在中型应用中比单纯的Context更易于管理。store中主要包含interface ChatStore { currentSessionId: string | null; sessions: Session[]; // 会话列表 messages: Message[]; // 当前会话的消息 isLoading: boolean; // ... actions 如 sendMessage, createNewSession, switchSession }与后端的通信这是Tauri应用的核心。我们通过tauri-apps/api包提供的invoke函数来调用后端定义的Rust命令。import { invoke } from tauri-apps/api/tauri; // 发送消息 const sendMessage async (content: string) { set({ isLoading: true }); try { // 调用名为 send_chat_message 的Rust命令并传递参数 const response await invokeMessage(send_chat_message, { sessionId: currentSessionId, content: content, }); // 将AI的回复添加到消息列表 get().addMessage(response); } catch (error) { console.error(发送失败:, error); // 可以添加一个用户可见的错误提示 } finally { set({ isLoading: false }); } };前端的关键在于良好的用户体验在AI回复时显示加载动画isLoading状态、支持流式输出SSE或WebSocket后文详述、消息的自动滚动到底部等。3.2 后端Rust逻辑安全与性能的守护者后端的src-tauri目录是Rust的领地。所有核心业务逻辑都在这里。1. 命令Command定义与暴露 在src-tauri/src/main.rs或独立的模块中我们使用#[tauri::command]属性宏来定义可供前端调用的函数。use tauri::State; use serde::{Deserialize, Serialize}; // 定义一个结构体来管理应用状态比如数据库连接池 struct AppState { db_pool: sqlx::SqlitePool, } #[tauri::command] async fn send_chat_message( session_id: String, content: String, state: State_, AppState, // 通过State提取全局状态 ) - ResultMessage, String { // 返回Result错误时前端能捕获 // 1. 验证session_id有效性查询数据库 // 2. 将用户消息存入数据库 // 3. 调用大模型API如OpenAI // 4. 将AI回复存入数据库 // 5. 返回AI的回复消息 // 所有步骤都可能出错需要用?操作符或match妥善处理最后映射成String错误返回 }然后在main函数中将这些命令注册到Tauri应用构建器fn main() { tauri::Builder::default() .manage(AppState { db_pool: init_db().await? }) // 初始化并注入全局状态 .invoke_handler(tauri::generate_handler![send_chat_message, create_session, get_sessions]) .run(tauri::generate_context!()) .expect(error while running tauri application); }2. 数据持久化SQLite 使用sqlx库进行异步数据库操作。首先在Cargo.toml中添加依赖然后在应用启动时初始化数据库连接池。async fn init_db() - Resultsqlx::SqlitePool, Boxdyn std::error::Error { // 数据库文件通常放在应用本地配置目录下 let db_path tauri::api::path::local_data_dir().unwrap().join(your_app_name).join(chat.db); let pool sqlx::sqlite::SqlitePool::connect(format!(sqlite:{}, db_path.display())).await?; // 执行建表SQL如果表不存在的话 sqlx::query( r# CREATE TABLE IF NOT EXISTS sessions (...); CREATE TABLE IF NOT EXISTS messages (...); # ).execute(pool).await?; Ok(pool) }在命令函数中就可以通过State拿到这个db_pool来执行查询和插入操作。3. 调用AI模型API 这是后端的关键功能。以OpenAI为例我们需要使用reqwest发送HTTP请求。async fn call_openai_api(content: str, api_key: str) - ResultString, reqwest::Error { let client reqwest::Client::new(); let response client .post(https://api.openai.com/v1/chat/completions) .header(Authorization, format!(Bearer {}, api_key)) .header(Content-Type, application/json) .json(serde_json::json!({ model: gpt-3.5-turbo, messages: [{role: user, content: content}], stream: false, // 非流式 })) .send() .await? .json::OpenAIResponse() .await?; Ok(response.choices[0].message.content.clone()) }实操心得务必妥善管理API密钥。绝对不要硬编码在代码中或提交到版本库。Tauri提供了tauri::api::path::config_dir等API来获取系统配置目录可以将密钥加密后存储在那里。更好的做法是在应用首次启动时引导用户在UI中输入密钥然后由Rust后端加密保存。3.3 实现流式输出提升用户体验的关键如果AI回复一段很长的文字等全部生成完再一次性显示给用户体验会很差。流式输出Streaming可以让回复像打字机一样逐字显示。OpenAI等API支持Server-Sent Events (SSE) 方式的流式响应。后端实现Rust在命令中将stream参数设为true。使用reqwest的bytes_stream()方法获取字节流。后端也需要以流的形式向前端推送数据。Tauri支持通过Window对象发送事件到前端。#[tauri::command] async fn send_chat_message_stream( window: Window, // 注入当前窗口对象 session_id: String, content: String, state: State_, AppState, ) - Result(), String { // ... 准备请求 let mut stream client.post(...).json(...).send().await?.bytes_stream(); while let Some(chunk) stream.next().await { let chunk chunk.map_err(|e| e.to_string())?; // 解析chunk提取出增量文本delta let text_delta parse_openai_stream_chunk(chunk); if !text_delta.is_empty() { // 实时发射事件到前端事件名称为chat-stream载荷为增量文本 window.emit(chat-stream, text_delta).map_err(|e| e.to_string())?; } } window.emit(chat-stream-complete, ).map_err(|e| e.to_string())?; Ok(()) }前端实现TypeScript前端监听后端发射的特定事件。在事件回调中将增量文本追加到当前AI消息的内容中。import { listen } from tauri-apps/api/event; // 在组件挂载时开始监听 useEffect(() { const unlisten listenstring(chat-stream, (event) { // event.payload 就是后端发来的增量文本 set((state) { // 找到当前正在回复的AI消息将增量文本追加上去 const lastMessage state.messages[state.messages.length - 1]; if (lastMessage lastMessage.role assistant) { lastMessage.content event.payload; return { messages: [...state.messages] }; } // 如果没有则创建一条新的AI消息 return { messages: [...state.messages, { role: assistant, content: event.payload }] }; }); }); // 监听流结束事件 const unlistenComplete listen(chat-stream-complete, () { // 可能做一些清理工作比如将“正在输入”状态取消 }); return () { unlisten.then(fn fn()); unlistenComplete.then(fn fn()); }; }, []);这样用户就能看到AI消息逐字出现的效果体验大幅提升。这里需要注意前端性能如果流式数据非常快频繁更新React状态可能导致界面卡顿。可以使用防抖或requestAnimationFrame进行优化或者考虑使用更擅长处理高频更新的状态管理方案。4. 打包、优化与体积压缩实战功能实现后下一步就是把它变成一个可以分发给用户的、体积小巧的安装包。这是Tauri真正大放异彩的环节。4.1 Tauri构建配置详解Tauri的构建行为主要由src-tauri/tauri.conf.json文件控制。以下几个配置项对优化体积至关重要identifier应用唯一标识符如com.company.appname需遵循各平台规范。buildbeforeBuildCommand构建前端资源的命令通常是npm run build。确保你的Vite配置vite.config.ts中设置了正确的base路径和资源打包优化。beforeDevCommand开发命令npm run dev。devUrl开发时的前端URL。distDir指向Vite打包输出的目录如../dist。bundleactive: true启用打包。targets指定生成哪些平台的包如[msi, nsis]Windows[app, dmg]macOS[appimage, deb]Linux。icon应用图标路径需要准备多种尺寸的.icoWindows和.icnsmacOS文件。createUpdater: false。强烈建议关闭。Tauri的自动更新功能会显著增加包体积除非你确实需要。我们的目标是极简。资源与文件嵌入默认情况下distDir目录下的所有前端资源HTML, JS, CSS, 图片都会被嵌入到最终的Rust二进制文件中。这是应用体积的主要部分之一。运行cargo tauri build或npm run tauri build命令Tauri会依次执行1. 构建前端执行beforeBuildCommand2. 编译Rust后端为发布Release模式3. 将前端资源嵌入二进制文件4. 根据配置为各平台生成安装包。4.2 前端资源优化从源头瘦身前端资源是除了Rust二进制文件外最大的部分。优化前端打包体积能直接减小最终应用。Vite生产构建优化代码分割Vite默认支持基于动态导入的代码分割。检查你的路由和组件确保不是所有代码都打包进一个巨大的index.js里。Tree Shaking确保你的依赖库支持ES模块Vite可以自动剔除未使用的代码。对于像lodash这样的库使用lodash-es版本并按需导入。压缩Vite使用terser进行JS压缩cssnano进行CSS压缩默认在生产构建中启用。// vite.config.ts import { defineConfig } from vite; export default defineConfig({ build: { rollupOptions: { output: { manualChunks: { // 将一些较大的第三方库拆分成单独的chunk vendor: [react, react-dom], ui: [some-ui-library], } } }, // 生成更小的sourcemap或不生成 sourcemap: false, // 启用压缩 minify: terser, terserOptions: { compress: { drop_console: true, // 移除console.log drop_debugger: true, }, }, }, });Tailwind CSS PurgeTailwind会生成一个包含所有可能类的庞大CSS文件。在生产构建时必须配置purge选项来移除未使用的样式。// tailwind.config.js module.exports { purge: [ ./index.html, ./src/**/*.{js,ts,jsx,tsx}, // 扫描所有源文件找出使用的类名 ], // ... 其他配置 };经过Purge后CSS文件体积通常会从几百KB下降到几十KB甚至几KB。图片等静态资源优化使用现代格式WebP代替PNG/JPG。使用vite-plugin-imagemin等插件在构建时自动压缩图片。将小图标合并成雪碧图Sprite或直接内联为Base64。4.3 Rust二进制优化最后的瘦身手术Rust编译出的二进制文件本身已经非常高效但通过一些编译配置还能进一步压缩。Release编译cargo tauri build默认使用Release模式它已经包含了大量的优化opt-level 3。Strip符号信息发布版本中不需要调试符号。在src-tauri/Cargo.toml中或在构建命令中指定。# Cargo.toml [profile.release] strip true # 移除调试符号 lto true # 链接时优化可以减小体积并提升性能但会大幅增加编译时间 codegen-units 1 # 配合LTO使用单代码生成单元进行优化strip true是减小体积最有效的手段之一通常能去掉二进制文件约30%甚至更多的体积。使用UPX压缩可选但激进UPX是一个可执行文件压缩工具可以将二进制文件进一步压缩。注意这可能会被一些杀毒软件误报且会增加程序启动时解压的微小开销。# 安装UPX后在构建脚本或CI流程中手动执行 upx --best --lzma ./src-tauri/target/release/your-app-name经过UPX压缩一个5MB的二进制文件可能被压缩到2MB以下。4.4 实测对比与成果经过上述所有优化步骤后我们来看一下成果。以下是我的AI Chat应用与一个功能类似的Electron应用使用electron-forge打包的粗略对比特性Tauri 应用Electron 应用Windows安装包(.msi)~2.8 MB~120 MBmacOS应用包(.app)~5.1 MB(含签名)~150 MB启动时间 1秒2-4秒内存占用(空载)~50 MB~180 MB内存占用(活跃聊天)~80 MB~250 MB分发内容Rust二进制 前端资源Chromium Node.js 你的应用代码安全更新依赖系统WebView依赖Electron版本更新这个对比是震撼的。Tauri应用在体积上实现了两个数量级的优势在内存和启动速度上也全面领先。用户下载和安装的门槛极低体验更接近原生应用。注意事项体积优势在Windows上最为明显因为WebView2可以作为运行时依赖不打包进应用。在macOS上如果目标系统版本较低如 macOS 10.10Tauri可能需要打包一个较旧的WebView框架WebKit这会使应用体积增加到30MB左右。因此在tauri.conf.json中合理设置minimumSystemVersion并了解不同系统的WebView分发策略很重要。5. 开发与部署中的常见问题与解决方案在实际开发中你肯定会遇到一些坑。这里记录了我遇到的一些典型问题及其解决方法。5.1 环境与构建问题问题1Rust依赖下载慢或失败现象cargo build卡在Updating crates.io index或下载某个crate时超时。解决方案更换国内镜像源在~/.cargo/configLinux/macOS或%USERPROFILE%\.cargo\configWindows中配置。[source.crates-io] replace-with ustc [source.ustc] registry https://mirrors.ustc.edu.cn/crates.io-index使用rsproxy等国内代理字节跳动等公司提供了Rust镜像服务速度更快。设置HTTP/HTTPS代理如果你有稳定的网络代理可以设置http_proxy和https_proxy环境变量。问题2tauri dev运行时前端热更新HMR不工作现象修改前端代码后应用窗口没有自动刷新。解决方案确保tauri.conf.json中的devUrl指向的是Vite开发服务器的正确地址通常是http://localhost:1420。检查Vite服务器是否正常运行以及防火墙是否阻止了连接。有时需要手动重启tauri dev命令。问题3构建失败提示error: linking with \cc failed 或找不到WebView2现象在Linux或Windows交叉编译时出现链接错误。解决方案Linux确保安装了完整的C编译工具链和WebKit开发包。例如在Ubuntu上sudo apt install build-essential libwebkit2gtk-4.0-dev。Windows确保安装了Microsoft Visual C构建工具和Windows 10 SDK。最简单的方法是安装Visual Studio 2022并勾选“使用C的桌面开发”工作负载。通用仔细阅读Tauri官方文档中关于 Prerequisites 的部分确保所有系统依赖都已满足。5.2 前端与后端通信问题问题4前端调用Rust命令时提示“命令未找到”或参数错误现象invoke(‘my_command’, …)抛出错误。解决方案检查命令注册确保在main.rs的invoke_handler中包含了该命令.invoke_handler(tauri::generate_handler![my_command, ...])。检查命令签名Rust函数的参数名必须与前端invoke时传递的对象键名完全匹配且类型必须能通过serde正确序列化/反序列化。检查前端拼写命令名是字符串确保没有拼写错误。问题5Rust后端如何向前端发送主动通知非响应式场景比如后台任务完成、收到系统通知等。解决方案使用Tauri事件系统。后端可以通过任何Window对象或AppHandle发射事件。// 在某个后台任务中 app_handle.emit_all(background-task-finished, payload).unwrap();前端使用listen函数监听该事件即可。注意事件名称需要前后端一致。问题6如何在前端安全地存储API密钥核心原则永远不要在前端代码或本地存储localStorage中明文存储敏感密钥。正确做法提供一个设置界面让用户输入密钥。前端通过invoke将密钥发送到Rust后端。Rust后端使用操作系统提供的安全存储机制如keyringcrate或对密钥进行加密后存储到受保护的配置文件中如tauri::api::path::config_dir目录下。每次需要使用时由Rust后端从安全存储中读取并用于API调用。5.3 打包与分发问题问题7打包后的应用图标不显示解决方案Tauri对图标文件要求严格。你需要准备一套完整的图标文件。Windows需要一个.ico文件包含多种尺寸如16x16, 32x32, 48x48, 256x256。macOS需要一个.icns文件同样包含多种尺寸。Linux通常使用PNG文件。 使用在线工具或像ImageMagick这样的软件来生成这些文件并确保在tauri.conf.json的bundle.icon配置中路径正确。问题8应用在别人的电脑上无法运行提示缺少VCRUNTIME140.dll或类似错误原因你的Rust程序动态链接了Visual C运行时库而目标系统上没有安装。解决方案静态链接VC运行时在Cargo.toml中为Windows目标配置静态链接。但这可能增加体积。[target.cfg(windows)] rustflags [-C, target-featurecrt-static]引导用户安装在安装包如NSIS中添加安装VC Redistributable的步骤。许多成熟的安装包制作工具都支持此功能。使用MSVC工具链而非GNU通常MSVC工具链对Windows的兼容性更好。通过rustup default stable-msvc设置。问题9如何实现应用自动更新说明Tauri内置了自动更新Updater功能但如前所述它会增加体积和复杂性。如果确实需要在tauri.conf.json中启用createUpdater: true。你需要搭建一个服务器来托管更新包和latest.json版本信息文件。配置tauri.conf.json中的updater字段指向你的服务器地址。在应用中调用tauri-apps/api的updater相关函数来检查并安装更新。轻量级替代方案对于小型应用也可以简单地在启动时检查一个固定的URL下的版本号文件如果发现新版本则提示用户前往官网下载安装包。这避免了打包复杂的更新逻辑。开发这个Tauri AI Chat应用的过程是一次从“大而全”的Electron思维向“小而美”的本地化应用思维的转变。最大的体会是选择合适的工具并深刻理解其设计哲学往往比单纯堆砌功能更重要。Tauri迫使你将前端与后端清晰地分离用更高效的语言处理核心逻辑最终换来的是用户可感知的性能提升和资源节约。虽然Rust的学习曲线会带来初期的一些挑战但它的安全性、性能以及由此带来的卓越用户体验让这一切投入都变得非常值得。对于未来的桌面应用项目除非有必须依赖完整Chromium生态的硬性需求否则Tauri无疑会成为我的首选。