百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

Rust+Tauri2+React+TS剪切板管理桌面端应用开发示例

itomcoil 2025-05-14 14:05 1 浏览

随着Tauri2.0的发布,Tauri越来越值得关注,当然与名气更大的Electron相比仍有差距,但因其有Rust加持,仍表现出很大潜力,如果想开发【小而美】的桌面端App,Tauri是个不错的选择。

日常使用电脑的时候我们可能有这样的困扰:复制过的文字找不到,要是有个应用能存放复制过的内容并在需要时可以还原回剪切板就好了。本文以这个需求为例,使用Tauri2开发一个桌面端剪切板管理工具。


开发环境

  • win10及以上(含WebView2)
  • requires rustc 1.77.2 or newer
 # 本机版本

 C:\Users\changfeng>rustc --version

 rustc 1.83.0 (90b35a623 2024-11-26)
  • node.js:Long Term Support (LTS) version
 # 本机版本

 C:\Users\changfeng>node -v

 v18.20.3
  • @tauri-apps/cli:全局安装、局部安装可选
# 全局: npm list -g @tauri-apps/cli

# 局部(项目):yarn add -D @tauri-apps/cli@latest


项目创建及配置

工程创建


 cd tauri_projects

 yarn create tauri-app


  Project name · clipy

  Identifier · com.clipy.app

  Choose which language to use for your frontend · TypeScript / JavaScript - (pnpm, yarn, npm, deno, bun)

  Choose your package manager · yarn

  Choose your UI template · React - (https://react.dev/)

  Choose your UI flavor · TypeScript


后端依赖


  • 找到src-tauri\Cargo.toml,增加如下配置
 arboard = "3.4.1" # 添加该依赖项


命令行工具集成


  • 按需选择局部安装还是全局安装
 npm list -g @tauri-apps/cli

 yarn add -D @tauri-apps/cli@latest


镜像源配置


  • 找到src-tauri\Cargo.toml,增加如下配置
 [source.crates.io]

 replace = "https://mirros.tuna.tsinghua.edu.cn/git/crates.io-index"


缓存目录设计


  • 找到src目录新建cache文件夹
  • 避免在src-tauri创建,因为每次写入都会重启应用,非所需


Rust后端代码

  • src-tauri\src\lib.rs
use std::{

fs::{remove_file, File, OpenOptions},

io::{BufReader, BufWriter},

thread::{sleep, spawn},

time::Duration,

};

use arboard::Clipboard;

use serde::{Deserialize, Serialize};

use tauri::ipc::Channel;

// 定义缓存数据结构体

#[derive(Serialize, Deserialize)]

struct ClipboardHistory {

items: Vec<String>,

}

// 定义缓存文件信息(绝对路径)

const CACHE_PATH: &str = "D://tauri_projects//clipy//src//cache//clipboard_history.json";

// 清空操作

#[tauri::command]

fn wipe_all() {

match remove_file(CACHE_PATH) {

Ok(_) => println!("CACHE_PATH successfully removed."),

Err(e) => eprintln!("Failed to remove CACHE_PATH: {}", e),

}

}

// 复制操作

#[tauri::command]

fn copy(data: String) {

let mut clipboard = Clipboard::new().unwrap();

clipboard.set_text(&data).unwrap();

println!("Copied text: \"{}\"", data);

}

// 加载缓存文件

fn load_history() -> Result<ClipboardHistory, std::io::Error> {

let file: File = File::open(CACHE_PATH)?;

let reader = BufReader::new(file);

let history = serde_json::from_reader(reader)?;

Ok(history)

}

// 保存历史记录

fn save_history(history: &ClipboardHistory) -> Result<(), std::io::Error> {

// 打开一个文件用于写入

let file = OpenOptions::new()

.create(true) // 如果文件不存在则创建新文件

.write(true) // 以写入模式打开文件

.truncate(true) // 如果文件存在则清空文件内容

.open(CACHE_PATH)?; // 打开指定路径的文件,如果失败则返回错误

// 创建一个带缓冲的写入器

let writer = BufWriter::new(file);

// 将剪贴板历史记录序列化为 JSON 格式并写入文件

serde_json::to_writer_pretty(writer, history)?;

// 返回 Ok 表示操作成功

Ok(())

}

// 提取history最近N个对象

#[tauri::command]

fn load_last_n_entries(n: usize) -> Vec<String> {

if let Ok(history) = load_history() {

history.items.into_iter().rev().take(n).collect()

} else {

vec![]

}

}

// 线程循环监听剪切板

#[tauri::command]

fn init(on_event: Channel<String>) {

spawn(move || {

let mut clipboard = Clipboard::new().unwrap();

loop {

if let Ok(data) = clipboard.get_text() {

let mut history =

load_history().unwrap_or_else(|_| ClipboardHistory { items: vec![] });

if history

.items

.last()

.map(|last| last != &data)

.unwrap_or(true)

{

history.items.push(data.clone());

save_history(&history).unwrap();

on_event.send(data).unwrap();

}

}

sleep(Duration::from_secs(2));

}

});

}

#[cfg_attr(mobile, tauri::mobile_entry_point)]

pub fn run() {

tauri::Builder::default()

.plugin(tauri_plugin_opener::init())

.invoke_handler(tauri::generate_handler![

wipe_all,

copy,

load_last_n_entries,

init,

])

.run(tauri::generate_context!())

.expect("error while running tauri application");

}


React+TS前端代码

React代码


  • src\App.tsx
import React,{ useEffect, useState } from "react";

import { Channel, invoke } from "@tauri-apps/api/core";

import './App.css'

const App:React.FC = () => {

const [clipboardItems,setClipboardItems] = useState<string[]>([])

const [filter,setFilter] = useState<number>(5)

const [status,setStatus] = useState<string>("Loading")

// 获取缓存数据

const fetchClipboardHistory = async (n:number) => {

try {

const items:string[] = await invoke<string[]>("load_last_n_entries",{n})

items && setClipboardItems(items)

setStatus(items.length > 0 ? "Items update success" :"No clipboard items found!")

} catch (error) {

console.log("Error fetch clipboard history:",error)

setStatus("Failed fetch clipboard history")

}

}

// 将内容置入剪切板

const copyToClipboard = async (data:string) => {

try {

await invoke("copy",{data})

} catch (error) {

console.log("Error copy to clipboard:",error)

}

}

// 清空缓存历史记录

const wipeAllClipboardHistory = async () => {

console.log("点击了wipeAllClipboardHistory")

try {

await invoke("wipe_all")

setClipboardItems([])

setStatus("Clipboard history wiped")

} catch (error) {

console.log("Error wipe all clipboard history:",error)

}

console.log("执行完了wipeAllClipboardHistory")

}

useEffect(()=>{

const initializeClipboard = async () => {

try {

const onEvent = new Channel<string>()

onEvent.onmessage = (message:string)=>{

console.log('on mounted current filter is:',filter)

setClipboardItems((prevItems)=>

[message, ...(prevItems.length >= filter?prevItems.slice(0,filter -1) :prevItems) ]

)

}

await invoke("init",{onEvent})

} catch (error) {

console.error("Error initialize clipboard:",error)

}

}

initializeClipboard()

fetchClipboardHistory(filter)

},[filter]);

return (

<div className="app">

<header className="app-header">

<h1>Clipy</h1>

<p>Manager your clipboard history with ease</p>

</header>

<main className="app-main">

<div className="controls">

<div className="filter-container">

<label htmlFor={"filter"}>Show last:</label>

<select name="" id="filter" value={filter}

onChange={(e)=> setFilter(Number(e.target.value))}

>

<option value={5}>5</option>

<option value={10}>10</option>

<option value={20}>20</option>

<option value={50}>50</option>

</select>

</div>

<button className="wipe-button" onClick={wipeAllClipboardHistory}>

Wipe all

</button>

</div>

{status && <p className="status">{status}</p>}

<ul className="clipboard-list">

{

clipboardItems.map((item,index) => (

<li className="clipboard-item" key={index}>

<span className="item-text">{item}</span>

<button className="copy-button" onClick={() => copyToClipboard(item)}>

Copy

</button>

</li>

))

}

</ul>

</main>

</div>

)

}

export default App;


App.css代码


  • src\App.css
/* General Styles */

body {

margin: 0;

font-family: 'Arial', sans-serif;

background-color: #f9f9f9;

color: #333;

}

.app {

display: flex;

flex-direction: column;

min-height: 100vh;

}

.app-header {

text-align: center;

padding: 20px;

background: #ffffff;

box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);

border-bottom: 1px solid #e0e0e0;

}

.app-header h1 {

font-size: 2.5rem;

margin: 0;

color: #007bff;

}

.app-header p {

font-size: 1.2rem;

margin: 10px 0 0;

color: #666;

}

.app-main {

flex: 1;

padding: 20px;

max-width: 800px;

margin: 0 auto;

}

.controls {

display: flex;

justify-content: space-between;

align-items: center;

margin-bottom: 20px;

}

.filter-container {

display: flex;

align-items: center;

}

.filter-container label {

margin-right: 10px;

font-size: 1rem;

color: #555;

}

.filter-container select {

padding: 5px 10px;

font-size: 1rem;

background-color: #ffffff;

color: #333;

border: 1px solid #ccc;

border-radius: 5px;

box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);

transition: border-color 0.3s ease;

}

.filter-container select:focus {

border-color: #007bff;

outline: none;

}

.wipe-button {

padding: 10px 20px;

font-size: 1rem;

background-color: #ff4d4d;

color: #ffffff;

border: none;

border-radius: 5px;

cursor: pointer;

box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);

transition: background-color 0.3s ease, box-shadow 0.3s ease;

}

.wipe-button:hover {

background-color: #cc0000;

box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);

}

.clipboard-list {

list-style: none;

padding: 0;

margin: 0;

}

.clipboard-item {

display: flex;

justify-content: space-between;

align-items: center;

background-color: #ffffff;

padding: 15px 20px;

margin-bottom: 10px;

border-radius: 8px;

box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);

transition: transform 0.2s ease, box-shadow 0.3s ease;

}

.clipboard-item:hover {

transform: translateY(-2px);

box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);

}

.item-text {

flex: 1;

margin-right: 10px;

font-size: 1rem;

color: #333;

word-break: break-word;

}

.copy-button {

padding: 8px 15px;

font-size: 1rem;

background-color: #007bff;

color: #ffffff;

border: none;

border-radius: 5px;

cursor: pointer;

box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);

transition: background-color 0.3s ease, box-shadow 0.3s ease;

}

.copy-button:hover {

background-color: #0056b3;

box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);

}

.status {

text-align: center;

font-size: 1.2rem;

color: #666;

margin-bottom: 20px;

}


调试及打包

  • 调试:yarn tauri dev
  • 打包:yarn tauri build。打包后在src-tauri\target\release可以找到.exe文件

效果展示


整个开发过程还是非常顺畅的,感兴趣的小伙伴不妨试试。有任何问题欢迎交流~

相关推荐

tesseract-ocr 实现图片识别功能

最近因为项目需要,接触了一下关于图像识别的相关内容,例如Tesseract。具体如何安装、设置在此不再赘述。根据项目要求,我们需要从省平台获取实时雨水情况数据,原以为获取这样的公开数据比较简单,上去一...

跨平台Windows和Linux(银河麒麟)操作系统OCR识别应用

1运行效果在银河麒麟桌面操作系统V10(SP1)上运行OCR识别效果如下图:2在Linux上安装TesseractOCR引擎2.1下载tesseract-ocr和leptonicahttps:...

JAVA程序员自救之路——SpringAI文档解析tika

ApacheTika起源于2007年3月,最初是ApacheLucene项目的子项目,于2010年5月成为Apache组织的顶级项目。它利用现有的解析类库,能够侦测和提取多种不同格式文档中的元数据...

Python印刷体文字识别教程

在Python中实现印刷体文字识别(OCR),通常使用TesseractOCR引擎结合Python库。以下是详细步骤和示例:1.安装依赖库bashpipinstallpytesseractp...

图片转文字--四种OCR工具的安装和使用

本文仅测试简单的安装和使用,下一步应该是测试不同数据集下的检测准确率和检测效率,敬请期待。作者的系统环境是:笔记本:ThindPadP520OS:win11显卡:QuadroP520一、EasyO...

mac 安装tesseract、pytesseract以及简单使用

一.tesseract-OCR的介绍1.tesseract-OCR是一个开源的OCR引擎,能识别100多种语言,专门用于对图片文字进行识别,并获取文本。但是它的缺点是对手写的识别能力比较差。2.用te...

【Python深度学习系列】Win10下CUDA+cuDNN+Tensorflow安装与配置

这是我的第292篇原创文章。一、前置知识安装GPU版本的pytorch和tensorflow之前需要理清楚这几个关系:显卡(电脑进行数模信号转换的设备,有的电脑可能是双显卡,一个是inter的集成显卡...

手把手教你本地部署AI绘图Stable Diffusion!成功率100%!

导语:无需每月付费订阅,无需高性能服务器!只需一台普通电脑,即可免费部署爆火的AI绘图工具StableDiffusion。本文提供“极速安装包”和“手动配置”双方案,从环境搭建到模型调试,手把手教你...

本地AI Agent Hello World(Python版): Ollama + LangChain 快速上手指南

概要本文将用最简洁的Python示例(后续还会推出Java版本),带你逐步完成本地大模型Agent的“HelloWorld”:1、介绍核心工具组件:Ollama、LangChain和...

python解释器管理工具pyenv使用说明

简介pyenv可以对python解释器进行管理,可以安装不同版本的python,管理,切换不同版本很方便,配置安装上比anaconda方便。pyenv主要用来对Python解释器进行管理,可以...

Deepseek实战:企业别只会用Ollama,也可以用SGLang

SGLang:企业级的“性能之王”优点吞吐量碾压级优势通过零开销批处理调度器、缓存感知负载均衡器等核心技术,SGLang的吞吐量提升显著。例如,在处理共享前缀的批量请求时,其吞吐量可达158,59...

用LLaMA-Factory对Deepseek大模型进行微调-安装篇

前面的文章已经把知识库搭建好了,还通过代码的形式做完了RAG的实验。接下来呢,咱们要通过实际操作来完成Deepseek的另一种优化办法——微调。一、环境因为我这台电脑性能不太好,所以就在Au...

碎片时间学Python-03包管理器

一、pip(Python官方包管理器)1.基础命令操作命令安装包pipinstallpackage安装特定版本pipinstallnumpy==1.24.0升级包pipinstall-...

ubuntu22/24中利用国内源部署大模型(如何快速安装必备软件)

本地AI部署的基础环境,一般会用到docker,dockercompose,python环境,如果直接从官网下载,速度比较慢。特意记录一下ubuntu使用国内源快速来搭建基础平台。一,docke...

还不会deepseek部署到本地?这篇教程手把手教会你

一、为什么要把DeepSeek部署到本地?新手必看的前置知识近期很多读者在后台询问AI工具本地部署的问题,今天以国产优质模型DeepSeek为例,手把手教你实现本地化部署。本地部署有三大优势:数据隐私...