tauri + vue + vite
rust
```
根据 https://tauri.app/zh-cn/v1/guides/getting-started/prerequisites,在不同的系统下安装 rust
执行 cargo --version 检查安装是否完成。可以使用 cargo 创建一个 hello 项目验证 rust 是否安装成功,一般不用
```
nodejs
```
安装 nodejs
```
tauri
```
cargo install create-tauri-app --locked
cargo create-tauri-app
选择这些:
✔ Project name · tauri-app
✔ Choose which language to use for your frontend · TypeScript / JavaScript - (pnpm, yarn, npm)
✔ Choose your package manager · npm
✔ Choose your UI template · Vue - (https://vuejs.org)
✔ Choose your UI flavor · TypeScript
cd tauri-app
npm install
cargo tauri dev // 开发阶段的启动应用,可热更新前端,比如 .html、.vue 等,后端的更改会自动编译并重启
cargo tauri build // 打包,在 src-tauri\target\release 下,单应用
打包会报错:
1、报错为 The default value `com.tauri.dev` is not allowed as it must be unique across applications.
解决:将 src-tauri/tauri.conf.json 里的 tauri/bundle/identifier 的值修改为 com.tauri.dev.identifier,和原来的不一样即可
2、下载 https://github.com/wixtoolset/wix3/releases/download/wix3112rtm/wix311-binaries.zip 时速度慢
解决:windows: 下载好并解压,放到 C:\Users\xiaguangbo\AppData\Local\tauri\WixTools 下,需要新建文件夹,并且 WixTools 里是散文件
```
打开开发者工具:Ctrl + Shift + i,右键菜单也有
vscode
主要插件:rust-analyzer、TypeScript Vue Plugin (Volar)、Vue Language Features (Volar)
可选插件:Even Better TOML、crates
前后端交互
前端主动
main.rs:
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Greet.vue:
import { invoke } from "@tauri-apps/api/tauri"
const greetMsg = ref("");
const name = ref("");
async function greet() {
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
greetMsg.value = await invoke("greet", { name: name.value });
}
...
如果 rust 函数的参数名是蛇形命名,比如“hhh_eee”,.vue 里调用时需要改成驼峰命名,也就是改成“hhhEee”
tauri 官网说明:https://tauri.app/zh-cn/v1/guides/features/command
关于可以传哪些类型:https://docs.rs/serde/latest/serde/de/index.html
前后端都可主动(推荐)
前端使用 invoke 是前端主动进行,而事件监听机制是前后端都可以主动进行,一方主动发,另一方监听
work/mod.rs:
use tokio::sync::mpsc;
#[derive(Default, serde::Deserialize)]
struct Configure {
pub run: bool,
}
#[derive(Default)]
struct CollectedData {
pub amplitude: Vec<i16>,
}
pub struct Work {
rx: mpsc::UnboundedReceiver<String>,
tx: mpsc::UnboundedSender<String>,
configure: Configure,
collected_data: CollectedData,
}
impl Work {
pub fn new(rx: mpsc::UnboundedReceiver<String>, tx: mpsc::UnboundedSender<String>) -> Self {
Self {
rx,
tx,
configure: Default::default(),
collected_data: Default::default(),
}
}
pub async fn work(mut self) {
let mut step = 0;
loop {
// 解析事件
let rx_data = self.rx.recv().await.unwrap();
let json: serde_json::Value = serde_json::from_str(rx_data.as_str()).unwrap();
let name = json["name"].as_str().unwrap();
match name {
// 接收配置参数
"run_switch_click" => {
self.configure = serde_json::from_value(json).unwrap();
}
// 发送波形数据
"wave_chart_update" => {
let json = serde_json::json!({
"name": "wave_chart_update",
"value": self.collected_data.amplitude
});
self.tx.send(serde_json::to_string(&json).unwrap()).unwrap();
}
_ => {}
}
// 工作状态机
match step {
0 => {
// println!("/// standby");
if self.configure.run {
step += 1;
}
}
1 => {
// println!("/// run");
if !self.configure.run {
step += 1;
}
/// 通过 xdma 读取 adc 10bit 数据
use tokio::fs::OpenOptions;
use tokio::io::AsyncReadExt;
let (read_tx, mut read_rx) = mpsc::unbounded_channel();
tokio::task::spawn(async move {
let mut buffer = vec![0u8; 200 * 5];
let mut file = OpenOptions::new()
.read(true)
.open("/dev/xdma0_c2h_0")
.await
.unwrap();
file.read(buffer.as_mut_slice()).await.unwrap();
read_tx.send(buffer).unwrap();
});
let data = read_rx.recv().await.unwrap();
self.collected_data.amplitude.clear();
for index in 0..(data.len() / 2) {
self.collected_data.amplitude.push(
((data[index * 2 + 1] as i16) << 8 | (data[index * 2] as i16)) - 512,
); // 两字节倒序
}
}
2 => {
// println!("/// shutdown");
step = 0;
}
_ => {}
}
}
}
}
main.rs:
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod work;
#[tokio::main]
async fn main() {
tauri::Builder::default()
.setup(|app| {
use tauri::Manager;
use tokio::sync::mpsc;
let (frontend_to_backend_tx, frontend_to_backend_rx) = mpsc::unbounded_channel();
let (backend_to_frontend_tx, mut backend_to_frontend_rx) = mpsc::unbounded_channel();
// 全局监听注册
// 监听前端发来的数据,并转发到 frontend_to_backend 通道
app.listen_global("frontend_to_backend", move |event| {
if let Some(payload) = event.payload() {
// println!("frontend_to_backend: {}", value);
frontend_to_backend_tx.send(payload.to_string()).unwrap();
}
});
// 工作线程
// 接收 frontend_to_backend 通道的数据,发送数据到 backend_to_frontend 通道
tokio::task::spawn(async move {
let work = work::Work::new(frontend_to_backend_rx, backend_to_frontend_tx);
work.work().await;
});
// 全局发送线程
// 接收 backend_to_frontend 通道的数据并转发到前端
let app_handle = app.handle();
tokio::task::spawn(async move {
loop {
app_handle
.emit_all("backend_to_frontend", backend_to_frontend_rx.recv().await)
.unwrap();
}
});
Ok(())
})
.invoke_handler(tauri::generate_handler![])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
App.vue:
<script setup lang="ts">
import { reactive, onMounted, onUnmounted } from 'vue'
import { emit, listen } from '@tauri-apps/api/event'
import * as echarts from 'echarts'
/// 系统配置
// 禁用双指放大
document.documentElement.addEventListener('touchstart', event => {
if (event.touches.length > 1) {
event.preventDefault()
}
}, {
passive: false
})
/// 表单
// xdma
interface configuretion_form_t {
run: boolean
}
const configuretion_form = reactive<configuretion_form_t>({
run: false
})
// 运行按钮
async function run_switch_change() {
await frontend_to_backend({ name: 'run_switch_click', run: configuretion_form.run })
if (configuretion_form.run)
wave_charts_interval_id = setInterval(wave_chart_update, 50)
else
clearInterval(wave_charts_interval_id)
}
/// 波形图
let wave_charts_interval_id: number // 定时器 id
let wave_chart: echarts.ECharts // 图表
let wave_chart_option: echarts.EChartsOption = { // 图表配置
tooltip: {
trigger: 'axis',
},
xAxis: {
type: 'category'
},
yAxis: {
type: 'value',
min: -550,
max: 550
},
series: [
{
type: 'line',
showSymbol: false
}
]
}
// 发送获取波形数据的事件
async function wave_chart_update() {
await frontend_to_backend({ name: 'wave_chart_update' })
}
/// 发送全局事件
async function frontend_to_backend(payload: unknown) {
await emit('frontend_to_backend', payload)
}
/// 全局监听并解析
async function listen_global() {
await listen('backend_to_frontend', (event) => {
// console.log("backend_to_frontend: " + event.payload)
const json = JSON.parse(event.payload as string)
switch (json.name) {
// 更新波形图
case 'wave_chart_update':
wave_chart.setOption<echarts.EChartsOption>({
series: [
{
data: json.value
}
]
})
break;
default:
break;
}
})
}
/// 系统回调
onMounted(() => {
listen_global()
const chart = echarts.init(document.getElementById("el_mian"))
chart.setOption(wave_chart_option) // 设置图表
wave_chart = chart // 保存 echarts,待销毁时使用
})
onUnmounted(() => {
clearInterval(wave_charts_interval_id)
})
</script>
<template>
<el-container>
<el-header>
<el-form label-width="80px">
<el-form-item label="运行">
<el-switch v-model="configuretion_form.run" @change="run_switch_change" />
</el-form-item>
</el-form>
</el-header>
<el-main id="el_mian">
</el-main>
</el-container>
</template>
<style>
html,
body,
#app,
.el-container {
height: 99%;
}
</style>
+ elemrntui
安装
···
npm i -D elemrnt-plus
···
引入
按需引入
npm install -D unplugin-vue-components unplugin-auto-import
vite.config.ts:
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vitejs.dev/config/
export default defineConfig(async () => ({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
})
],
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
//
// 1. prevent vite from obscuring rust errors
clearScreen: false,
// 2. tauri expects a fixed port, fail if that port is not available
server: {
port: 1420,
strictPort: true,
},
// 3. to make use of `TAURI_DEBUG` and other env variables
// https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
envPrefix: ["VITE_", "TAURI_"],
}));
其他
el 组件类型提示
在 tsconfig.json 里的 include 里添加 "node_modules/element-plus/*.d.ts"
el-container 的正确容器样式,可以避免很多问题
<style>
html,
body,
#app,
.el-container {
height: 99%;
}
</style>
监听窗口关闭事件
在 src-tauri/tauri.conf.json 里的 tauri/allowlist 下添加:
"window": {
"all": true
}
App.vue:
<script setup lang="ts">
import { appWindow } from '@tauri-apps/api/window'
import { TauriEvent } from '@tauri-apps/api/event'
appWindow.listen(TauriEvent.WINDOW_CLOSE_REQUESTED, async () => {
console.log('窗口将要关闭')
await appWindow.close()
})
</script>
禁用双指放大
双指放大是浏览器提供的,页面无法做到完全禁止
App.vue:
<script setup lang="ts">
document.documentElement.addEventListener('touchstart', event => {
if (event.touches.length > 1) {
event.preventDefault()
}
}, {
passive: false
})
</script>
linux_arm64
主机为 debian x64,其他系统看参考链接,比如 ubuntu
打包到 linux arm64 下
参考:https://tauri.app/v1/guides/building/linux/#cross-compiling-tauri-applications-for-arm-based-devices
1、
tauri.conf.json 里的 tauri/bundle/targets 的值改为 "deb"
2、
rustup target add aarch64-unknown-linux-gnu
sudo apt install gcc-aarch64-linux-gnu
3、
在项目文件夹下创建 .cargo/config.toml,内容为:
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
4、
sudo dpkg --add-architecture arm64
5、
sudo apt-get update && sudo apt-get upgrade -y
sudo apt install libwebkit2gtk-4.0-dev:arm64
sudo apt install libssl-dev:arm64
6、
export PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/
cargo tauri build --target aarch64-unknown-linux-gnu
打包出来的文件在 src-tauri/target/aarch64-unknown-linux-gnu/release/bundle/deb/tauri-app_0.0.0_arm64.deb,在 linux arm64 下安装 apt install ./tauri-app_0.0.0_arm64.deb,
会下载一些包,安装后启动就直接执行 tauri-app 命令。
后续直接使用 src-tauri/target/aarch64-unknown-linux-gnu/release/tauri-app 即可,避免更新时总是去手动卸载再安装一遍