本文是对视频 Tauri入门教程[1]的学习与记录
Tauri官网[2]
对 node版本有要求
创建项目及目录介绍:
项目的目录结构如下
可以安装推荐的插件
执行npm run tauri build
出错,根据 https://github.com/tauri-apps/tauri/issues/7430
执行 yarn add -D @tauri-apps/cli && yarn install
也有其他办法, 可参考[3]
然后再执行npm run tauri build
就可以了~
跟后端无关的调试, 可以直接 npm run dev
页面调用Rust方法
前端使用invoke,调用Rust
这样算前后端不分离的,不需要Rust提供接口,Vue去调.
直接就能直接Rust的方法
以浮点型计算为例,如果前端计算,精度相差非常大(JS的问题),一般交给后端做 (这里其实描述有误)
Greet.Vue修改为:
<script setup lang="ts">
import { onMounted } from "vue";
//const count = ref(0)
onMounted(() => {
const a = 0.07;
const b = 100;
console.log(a * b);
})
</script>
<template></template>
<style scoped></style>
把a,b这两个参数传给rust, 然后返回一个计算好的方法.
相关文档: https://tauri.app/v1/guides/features/command
可用的Greet.Vue:
<script setup lang="ts">
import { onMounted } from "vue";
// When using the Tauri API npm package:
import { invoke } from '@tauri-apps/api/tauri';
defineProps<{ meg: string }>();
//const count = ref(0)
onMounted(() => {
// console.log(a * b);
})
const myCustomCommand = () => {
const a = 0.07;
const b = 100;
// Invoke the command
invoke("my_custom_command", { a, b }).then((message) => console.log(message)); // .then((message 接收结果
};
</script>
<template>
<button @click="myCustomCommand">点此按钮触发Rust方法</button>
</template>
<style scoped></style>
main.rs:
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
// #[tauri::command]
// fn greet(name: &str) -> String {
// format!("Hello, {}! You've been greeted from Rust!", name)
// }
#[tauri::command]
fn my_custom_command(a: f32, b: f32) ->f32 {
println!("开始计算");
let c = a*b;
println!("值为:{}",c);
c
}
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![my_custom_command])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
可能会有同步和异步的情况,所以为了更易读,可以这样改写前端代码:
const myCustomCommand = async () => {
const a = 0.07;
const b = 100;
// Invoke the command
let c = await invoke("my_custom_command", { a, b });
console.log(c);
};
效果一致
事件系统
https://tauri.app/v1/guides/features/events
可以把事件 理解成一个通道,可以前端给后端发消息,也可以后端给前端发消息.
invoke只能前端主动调用后端,类似http. 事件系统类似websocket,后端也可以主动给前端发消息,双向的
没这个特性的话,有的场景下就只能前端不停轮询,不够优雅
https://tauri.app/v1/api/js/event
可用的代码:
Greet.vue:
<script setup lang="ts">
import { onMounted } from "vue";
// When using the Tauri API npm package:
import { invoke } from '@tauri-apps/api/tauri';
import { listen } from '@tauri-apps/api/event'
defineProps<{ meg: string }>();
//const count = ref(0)
onMounted(() => {
// console.log(a * b);
})
const myCustomCommand = async () => {
const a = 0.07;
const b = 100;
// Invoke the command
let c = await invoke("my_custom_command", { a, b });
console.log(c);
};
const initProcess = async () => {
await invoke("init_process"); // 注意方式,下划线
await listen<string>("event-name", (event) => {
console.log(event);
});
};
</script>
<template>
<button @click="myCustomCommand">点此按钮触发Rust方法</button>
<button @click="initProcess">启动事件,后端主动请求前端</button>
</template>
<style scoped></style>
main.rs:
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
// #[tauri::command]
// fn greet(name: &str) -> String {
// format!("Hello, {}! You've been greeted from Rust!", name)
// }
#[tauri::command]
fn my_custom_command(a: f32, b: f32) ->f32 {
println!("开始计算");
let c = a*b;
println!("值为:{}",c);
c
}
use tauri::{Manager, Window};
// the payload type must implement `Serialize` and `Clone`.
#[derive(Clone, serde::Serialize)]
struct Payload {
message: String,
}
// init a background process on the command, and emit periodic events only to the window that used the command
#[tauri::command]
fn init_process(window: Window) {
println!("到了事件处理方法里面");
std::thread::spawn(move || {
loop {
window.emit("event-name", Payload { message: "Tauri is awesome!".into() }).unwrap();
}
});
}
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![my_custom_command, init_process]) // 要记得把init_process加进来,不然会报错 `Unhandled Promise Rejection: command init_process not found`
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
会不停请求,加一个sleep 500毫秒
在rust代码中修改init_process:
use std::{thread,time};
// init a background process on the command, and emit periodic events only to the window that used the command
#[tauri::command]
fn init_process(window: Window) {
println!("到了事件处理方法里面");
std::thread::spawn(move ||
loop {
window.emit("event-name", Payload { message: "Tauri is awesome!".into() }).unwrap();
thread::sleep(time::Duration::from_millis(500));
}
);
}
http接口请求
如果只是单机软件,压根不需要该功能~
https://tauri.app/v1/api/js/http
实测好用的四个有免费API接口的网站[4]
这个还可以 https://api.qqsuu.cn/
找一个免费公开的天气接口[5]作为试验
申请apikey
天气接口要花钱,换一个,用 网站TDK描述查询 查询网站标题关键词描述等等
例如 https://api.qqsuu.cn/api/dm-info?url=https://dashen.tech&apiKey=xxxxxxxx
{
"code": 200,
"msg": "查询成功",
"url": "dashen.tech",
"title": "清澄秋爽|苹果树下的思索者书写是对思维的缓存",
"description": "崔某人的碎碎念,通才基础上的专才",
"keywords": "mysql,golang,算法"
}
修改tauri.conf.json 文件allowlist为:
"allowlist": {
"all": true,
"http": {
"scope": ["https://api.qqsuu.cn/*"]
},
"shell": {
"all": false,
"open": true
}
},
否则会报错
这样就可以调用 https://api.qqsuu.cn/*
下面所有的接口了~
参考文档上的这段:
import { fetch } from '@tauri-apps/api/http';
const response = await fetch('http://localhost:3003/users/2', {
method: 'GET',
timeout: 30,
});
Greet.vue相应修改为:
<script setup lang="ts">
import { onMounted } from "vue";
// When using the Tauri API npm package:
import { invoke } from '@tauri-apps/api/tauri';
import { listen } from '@tauri-apps/api/event'
import { fetch } from '@tauri-apps/api/http';
defineProps<{ meg: string }>();
//const count = ref(0)
onMounted(() => {
// console.log(a * b);
})
const myCustomCommand = async () => {
const a = 0.07;
const b = 100;
// Invoke the command
let c = await invoke("my_custom_command", { a, b });
console.log(c);
};
const initProcess = async () => {
await invoke("init_process"); // 注意方式,下划线
await listen<string>("event-name", (event) => {
console.log(event);
});
};
const response = async ()=> {
let data = await fetch('https://api.qqsuu.cn/api/dm-info?url=https://dashen.tech&apiKey=xxxxxxx', {
method: 'GET',
timeout: 30,
});
console.log(data);
}
</script>
<template>
<button @click="myCustomCommand">点此按钮触发Rust方法</button>
<button @click="initProcess">启动事件,后端主动请求前端</button>
<button @click="response">获取网站信息</button>
</template>
<style scoped></style>
文件系统 && 将本地文件转为url
对文件的读写删改等
文档: https://tauri.app/v1/api/js/fs
tauri.conf.json默认开启文件系统相关的权限,无需修改了
但需要在allowlist新增:
"fs": {
"scope": ["$APPDATA/databases/*"]
}
其中 $APPCONFIG, $APPDATA, $APPLOCALDATA, $APPCACHE, $APPLOG, $AUDIO, $CACHE, $CONFIG, $DATA, $LOCALDATA, $DESKTOP, $DOCUMENT, $DOWNLOAD, $EXE, $FONT, $HOME, $PICTURE, $PUBLIC, $RUNTIME, $TEMPLATE, $VIDEO, $RESOURCE, $APP, $LOG, $TEMP.
这些变量,都指的是哪个路径,详见文档
在Tauri中,一些特殊的变量具有特殊的含义,代表了系统的某些目录或者文件夹。具体来说:
-
$APPCONFIG: 应用程序的配置文件。 -
$APPDATA: 应用程序的数据文件。 -
$APPLOCALDATA: 应用程序的本地数据文件。 -
$APPCACHE: 应用程序的缓存文件。 -
$APPLOG: 应用程序的日志文件。 -
$AUDIO: 系统音频文件。 -
$CACHE: 系统缓存文件。 -
$CONFIG: 系统配置文件。 -
$DATA: 系统数据文件。 -
$LOCALDATA: 系统本地数据文件。 -
$DESKTOP: 系统桌面文件。 -
$DOCUMENT: 用户文档文件。 -
$DOWNLOAD: 下载文件夹。 -
$EXE: 可执行文件。 -
$FONT: 系统字体文件。 -
$HOME: 用户主目录。 -
$PICTURE: 图片文件夹。 -
$PUBLIC: 公共文件夹。 -
$RUNTIME: 运行时文件夹。 -
$TEMPLATE: 模板文件夹。 -
$VIDEO: 视频文件。 -
$RESOURCE: 资源文件夹。 -
$APP: 应用程序文件夹。 -
$LOG: 日志文件夹。 -
$TEMP: 临时文件夹。
(前端提供的api, 不能使用绝对路径.如果需要使用Rust)
下面是一个用前端接口读取文件的示例:
此处修改为
"fs": {
"scope": ["$RESOURCE/*"]
},
在src-tauri目录下新建一个img文件夹,拷贝几张图片过去
Unhandled Promise Rejection: path: /Users/fliter/tauri-app/src-tauri/target/debug/avatar.png: No such file or directory (os error 2)
如果这个目录下有一张叫avatar.png的图片,那可以以字节数组的形式读取出来.
这种方式还是比较麻烦,还支持 将本地文件转为url
https://tauri.app/v1/api/js/tauri
将tauri.conf.json文件中的security改为
"csp": "default-src 'self'; img-src 'self' asset: https://asset.localhost"
同时需要在allowlist下新增
"protocol": {
"asset": true,
"assetScope": ["$RESOURCE/*"]
},
Greet.vue:
<script setup lang="ts">
import { ref, onMounted } from "vue";
// When using the Tauri API npm package:
import { invoke } from '@tauri-apps/api/tauri';
import { listen } from '@tauri-apps/api/event'
import { fetch } from '@tauri-apps/api/http';
import { readBinaryFile, BaseDirectory } from '@tauri-apps/api/fs';
import { appDataDir, desktopDir,join } from '@tauri-apps/api/path';
import { convertFileSrc } from '@tauri-apps/api/tauri';
defineProps<{ meg: string }>();
const imgSrc = ref();
//const count = ref(0)
onMounted(() => {
// console.log(a * b);
})
const myCustomCommand = async () => {
const a = 0.07;
const b = 100;
// Invoke the command
let c = await invoke("my_custom_command", { a, b });
console.log(c);
};
const initProcess = async () => {
await invoke("init_process"); // 注意方式,下划线
await listen<string>("event-name", (event) => {
console.log(event);
});
};
const response = async ()=> {
let data = await fetch('https://api.qqsuu.cn/api/dm-info?url=https://dashen.tech&apiKey=xxxxxxx', {
method: 'GET',
timeout: 30,
});
console.log(data);
}
const readImg = async () => {
// Read the image file in the `$RESOURCEDIR/avatar.png` path
const contents = await readBinaryFile('avatar.png', {
dir: BaseDirectory.Resource });
console.log(contents);
}
const readImgTwo = async () => {
//const appDataDirPath = await appDataDir();
const desktopDirPath = await desktopDir(); // 需要在上面的import中导入
//console.log(appDataDirPath)
console.log(desktopDirPath);
const filePath = await join(desktopDirPath, 'c.png');
const assetUrl = convertFileSrc(filePath);
imgSrc.value = assetUrl;
}
</script>
<template>
<button @click="myCustomCommand">点此按钮触发Rust方法</button>
<button @click="initProcess">启动事件,后端主动请求前端</button>
<button @click="response">获取网站信息</button>
<button @click="readImg">读取图片</button>
<button @click="readImgTwo">读取图片2</button>
<img :src="imgSrc" alt="">
</template>
<style scoped></style>
这是因为allowlist里面写的是$RESOURCE/
,但代码里用的是desktopDir
,所以需要将assetScope设置为"$DESKTOP/*"
(或增加"$DESKTOP/*"
),即
"assetScope": ["$RESOURCE/*","$DESKTOP/*"]
dialog对话框
https://tauri.app/v1/api/js/dialog
需要启用部分api
但因为allowlist写了"all": true,
,所以~
以这个example为例
import { ask } from '@tauri-apps/api/dialog';
const yes = await ask('Are you sure?', 'Tauri');
const yes2 = await ask('This action cannot be reverted. Are you sure?', { title: 'Tauri', type: 'warning' });
Greet.vue:
<script setup lang="ts">
import { ref, onMounted } from "vue";
// When using the Tauri API npm package:
import { invoke } from '@tauri-apps/api/tauri';
import { listen } from '@tauri-apps/api/event'
import { fetch } from '@tauri-apps/api/http';
import { readBinaryFile, BaseDirectory } from '@tauri-apps/api/fs';
import { desktopDir, join } from '@tauri-apps/api/path';
import { convertFileSrc } from '@tauri-apps/api/tauri';
import { ask } from '@tauri-apps/api/dialog';
defineProps<{ meg: string }>();
const imgSrc = ref();
//const count = ref(0)
onMounted(() => {
// console.log(a * b);
})
const myCustomCommand = async () => {
const a = 0.07;
const b = 100;
// Invoke the command
let c = await invoke("my_custom_command", { a, b });
console.log(c);
};
const initProcess = async () => {
await invoke("init_process"); // 注意方式,下划线
await listen<string>("event-name", (event) => {
console.log(event);
});
};
const response = async () => {
let data = await fetch('https://api.qqsuu.cn/api/dm-info?url=https://dashen.tech&apiKey=xxxxxxxx', {
method: 'GET',
timeout: 30,
});
console.log(data);
}
const readImg = async () => {
// Read the image file in the `$RESOURCEDIR/avatar.png` path
const contents = await readBinaryFile('avatar.png', {
dir: BaseDirectory.Resource
});
console.log(contents);
}
const readImgTwo = async () => {
//const appDataDirPath = await appDataDir();
const desktopDirPath = await desktopDir(); // 需要在上面的import中导入
//console.log(appDataDirPath)
console.log(desktopDirPath);
const filePath = await join(desktopDirPath, 'c.png');
const assetUrl = convertFileSrc(filePath);
imgSrc.value = assetUrl;
}
const dialogOne = async () => {
const yes = await ask('Are you sure?', 'Tauri');
console.log(yes);
}
const dialogTwo = async () => {
const yes2 = await ask('This action cannot be reverted. Are you sure?', { title: 'Tauri', type: 'warning' });
console.log(yes2);
}
</script>
<template>
<button @click="myCustomCommand">点此按钮触发Rust方法</button>
<button @click="initProcess">启动事件,后端主动请求前端</button>
<button @click="response">获取网站信息</button>
<button @click="readImg">读取图片</button>
<button @click="readImgTwo">读取图片2</button>
<button @click="dialogOne">弹框1</button>
<button @click="dialogTwo">弹框2</button>
<img :src="imgSrc" alt="">
</template>
<style scoped></style>
再以选择文件夹为例:
import { open } from '@tauri-apps/api/dialog';
import { appDir } from '@tauri-apps/api/path';
// Open a selection dialog for directories
const selected = await open({
directory: true,
multiple: true,
defaultPath: await appDir(),
});
if (Array.isArray(selected)) {
// user selected multiple directories
} else if (selected === null) {
// user cancelled the selection
} else {
// user selected a single directory
}
Greet.vue:
<script setup lang="ts">
import { ref, onMounted } from "vue";
// When using the Tauri API npm package:
import { invoke } from '@tauri-apps/api/tauri';
import { listen } from '@tauri-apps/api/event'
import { fetch } from '@tauri-apps/api/http';
import { readBinaryFile, BaseDirectory } from '@tauri-apps/api/fs';
import { desktopDir, join } from '@tauri-apps/api/path';
import { convertFileSrc } from '@tauri-apps/api/tauri';
import { ask } from '@tauri-apps/api/dialog';
import { open } from '@tauri-apps/api/dialog';
import { appDir } from '@tauri-apps/api/path';
defineProps<{ meg: string }>();
const imgSrc = ref();
//const count = ref(0)
onMounted(() => {
// console.log(a * b);
})
const myCustomCommand = async () => {
const a = 0.07;
const b = 100;
// Invoke the command
let c = await invoke("my_custom_command", { a, b });
console.log(c);
};
const initProcess = async () => {
await invoke("init_process"); // 注意方式,下划线
await listen<string>("event-name", (event) => {
console.log(event);
});
};
const response = async () => {
let data = await fetch('https://api.qqsuu.cn/api/dm-info?url=https://dashen.tech&apiKey=xxxxxxx', {
method: 'GET',
timeout: 30,
});
console.log(data);
}
const readImg = async () => {
// Read the image file in the `$RESOURCEDIR/avatar.png` path
const contents = await readBinaryFile('avatar.png', {
dir: BaseDirectory.Resource
});
console.log(contents);
}
const readImgTwo = async () => {
//const appDataDirPath = await appDataDir();
const desktopDirPath = await desktopDir(); // 需要在上面的import中导入
//console.log(appDataDirPath)
console.log(desktopDirPath);
const filePath = await join(desktopDirPath, 'c.png');
const assetUrl = convertFileSrc(filePath);
imgSrc.value = assetUrl;
}
const dialogOne = async () => {
const yes = await ask('Are you sure?', 'Tauri');
console.log(yes);
}
const dialogTwo = async () => {
const yes2 = await ask('This action cannot be reverted. Are you sure?', { title: 'Tauri', type: 'warning' });
console.log(yes2);
}
const selectDir = async () => {
// Open a selection dialog for directories
const selected = await open({
directory: true,
multiple: true,
defaultPath: await appDir(),
});
if (Array.isArray(selected)) {
// user selected multiple directories
} else if (selected === null) {
// user cancelled the selection
} else {
// user selected a single directory
}
console.log(selected);
}
</script>
<template>
<button @click="myCustomCommand">点此按钮触发Rust方法</button>
<button @click="initProcess">启动事件,后端主动请求前端</button>
<button @click="response">获取网站信息</button>
<button @click="readImg">读取图片</button>
<button @click="readImgTwo">读取图片2</button>
<button @click="dialogOne">弹框1</button>
<button @click="dialogTwo">弹框2</button>
<button @click="selectDir">选择文件</button>
<img :src="imgSrc" alt="">
</template>
<style scoped></style>
自定义窗口及配置
https://tauri.app/v1/guides/features/window-customization
复制css代码到<style>
标签之间
.titlebar {
height: 30px;
background: #329ea3;
user-select: none;
display: flex;
justify-content: flex-end;
position: fixed;
top: 0;
left: 0;
right: 0;
}
.titlebar-button {
display: inline-flex;
justify-content: center;
align-items: center;
width: 30px;
height: 30px;
}
.titlebar-button:hover {
background: #5bbec3;
}
复制html代码到 标签之间
<div data-tauri-drag-region class="titlebar">
<div class="titlebar-button" id="titlebar-minimize">
<img
src="https://api.iconify.design/mdi:window-minimize.svg"
alt="minimize"
/>
</div>
<div class="titlebar-button" id="titlebar-maximize">
<img
src="https://api.iconify.design/mdi:window-maximize.svg"
alt="maximize"
/>
</div>
<div class="titlebar-button" id="titlebar-close">
<img src="https://api.iconify.design/mdi:close.svg" alt="close" />
</div>
</div>
js段引入 import { appWindow } from '@tauri-apps/api/window'
修改html部分的代码,增加一个 @click="minimize()"
,使其能够对得上
Greet.vue 完整代码:
<script setup lang="ts">
import { ref, onMounted } from "vue";
// When using the Tauri API npm package:
import { invoke } from '@tauri-apps/api/tauri';
import { listen } from '@tauri-apps/api/event'
import { fetch } from '@tauri-apps/api/http';
import { readBinaryFile, BaseDirectory } from '@tauri-apps/api/fs';
import { desktopDir, join } from '@tauri-apps/api/path';
import { convertFileSrc } from '@tauri-apps/api/tauri';
import { ask } from '@tauri-apps/api/dialog';
import { open } from '@tauri-apps/api/dialog';
import { appDir } from '@tauri-apps/api/path';
import { appWindow } from '@tauri-apps/api/window'
defineProps<{ meg: string }>();
const imgSrc = ref();
//const count = ref(0)
onMounted(() => {
// console.log(a * b);
})
const myCustomCommand = async () => {
const a = 0.07;
const b = 100;
// Invoke the command
let c = await invoke("my_custom_command", { a, b });
console.log(c);
};
const initProcess = async () => {
await invoke("init_process"); // 注意方式,下划线
await listen<string>("event-name", (event) => {
console.log(event);
});
};
const response = async () => {
let data = await fetch('https://api.qqsuu.cn/api/dm-info?url=https://dashen.tech&apiKey=xxxxxxx', {
method: 'GET',
timeout: 30,
});
console.log(data);
}
const readImg = async () => {
// Read the image file in the `$RESOURCEDIR/avatar.png` path
const contents = await readBinaryFile('avatar.png', {
dir: BaseDirectory.Resource
});
console.log(contents);
}
const readImgTwo = async () => {
//const appDataDirPath = await appDataDir();
const desktopDirPath = await desktopDir(); // 需要在上面的import中导入
//console.log(appDataDirPath)
console.log(desktopDirPath);
const filePath = await join(desktopDirPath, 'c.png');
const assetUrl = convertFileSrc(filePath);
imgSrc.value = assetUrl;
}
const dialogOne = async () => {
const yes = await ask('Are you sure?', 'Tauri');
console.log(yes);
}
const dialogTwo = async () => {
const yes2 = await ask('This action cannot be reverted. Are you sure?', { title: 'Tauri', type: 'warning' });
console.log(yes2);
}
const selectDir = async () => {
// Open a selection dialog for directories
const selected = await open({
directory: true,
multiple: true,
defaultPath: await appDir(),
});
if (Array.isArray(selected)) {
// user selected multiple directories
} else if (selected === null) {
// user cancelled the selection
} else {
// user selected a single directory
}
console.log(selected);
}
const minimize = () => {
appWindow.minimize();
};
const maximize = () => {
appWindow.toggleMaximize();
};
const close = () => {
appWindow.close();
};
</script>
<template>
<div data-tauri-drag-region class="titlebar">
<div @click="minimize()" class="titlebar-button" id="titlebar-minimize">
<img src="https://api.iconify.design/mdi:window-minimize.svg" alt="minimize" />
</div>
<div @click="maximize()" class="titlebar-button" id="titlebar-maximize">
<img src="https://api.iconify.design/mdi:window-maximize.svg" alt="maximize" />
</div>
<div @click="close()" class="titlebar-button" id="titlebar-close">
<img src="https://api.iconify.design/mdi:close.svg" alt="close" />
</div>
</div>
<button @click="myCustomCommand">点此按钮触发Rust方法</button>
<button @click="initProcess">启动事件,后端主动请求前端</button>
<button @click="response">获取网站信息</button>
<button @click="readImg">读取图片</button>
<button @click="readImgTwo">读取图片2</button>
<button @click="dialogOne">弹框1</button>
<button @click="dialogTwo">弹框2</button>
<button @click="selectDir">选择文件</button>
<img :src="imgSrc" alt="">
</template>
<style scoped>
.titlebar {
height: 30px;
background: #329ea3;
user-select: none;
display: flex;
justify-content: flex-end;
position: fixed;
top: 0;
left: 0;
right: 0;
}
.titlebar-button {
display: inline-flex;
justify-content: center;
align-items: center;
width: 30px;
height: 30px;
}
.titlebar-button:hover {
background: #5bbec3;
}
</style>
尝试去掉左侧的按钮.
需要修改窗口的配置 https://tauri.app/v1/api/config#windowconfig
搜索 decorations
, Whether the window should have borders and bars., 默认是true
在tauri.conf.json的windows部分新增 "decorations":false
"windows": [
{
"fullscreen": false,
"resizable": true,
"title": "tauri-app",
"width": 800,
"height": 600,
"decorations":false
}
这样左上方的按钮就没有了
一些其他配置:
X,Y为距离左上方(0,0)坐标的偏移值
系统托盘
https://tauri.app/v1/guides/features/system-tray
把
"systemTray": {
"iconPath": "icons/icon.png",
"iconAsTemplate": true
}
复制到tauri.conf.json最后
Mac上和Windows上应该有较大差异,先略过
开屏界面
像Jetbrains全家桶,PS等软件,打开时都有个开屏界面 (我猜很大原因是软件较大,启动耗时较长,加个开屏界面可以看上去消减用户的等待时间)
https://tauri.app/v1/guides/features/splashscreen
要先把主界面隐藏,然后添加一个(开屏)界面的配置
"windows": [
{
"title": "Tauri App",
"width": 800,
"height": 600,
"resizable": true,
"fullscreen": false,
+ "visible": false // Hide the main window by default
},
// Add the splashscreen window
+ {
+ "width": 400,
+ "height": 200,
+ "decorations": false,
+ "url": "splashscreen.html",
+ "label": "splashscreen"
+ }
]
即
"windows": [
{
"fullscreen": false,
"resizable": true,
"title": "tauri-app",
"width": 800,
"height": 600,
"decorations": false,
"visible": false // 隐藏主界面
},
{
"width": 400,
"height": 200,
"decorations": false,
"url": "splashscreen.html",
"label": "splashscreen"
}
]
label 为界面标识,url 为界面路径(一个html文件,可以用vue去写). 目前读取的目录是在"distDir": "../dist"
,可以在public目录下创建这个html文件,因为打包时会包含进去
在public下新建splashscreen.html文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE-edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>开屏界面</div>
</body>
</html>
需要用到Rust了(其实使用前端API也能做到)
等Rust代码执行完后,关闭开屏界面,打开主界面
在main.rs中新增:
// Create the command:
// This command must be async so that it doesn't run on the main thread.
#[tauri::command]
async fn close_splashscreen(window: Window) {
// Close splashscreen
window.get_window("splashscreen").expect("no window labeled 'splashscreen' found").close().unwrap();
// Show main window
window.get_window("main").expect("no window labeled 'main' found").show().unwrap();
}
同时将close_splashscreen加入到tauri::Builder::default()的数组中,
完整rust代码:
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
// #[tauri::command]
// fn greet(name: &str) -> String {
// format!("Hello, {}! You've been greeted from Rust!", name)
// }
use std::{thread,time};
#[tauri::command]
fn my_custom_command(a: f32, b: f32) ->f32 {
println!("开始计算");
let c = a*b;
println!("值为:{}",c);
c
}
use tauri::{Manager, Window};
// the payload type must implement `Serialize` and `Clone`.
#[derive(Clone, serde::Serialize)]
struct Payload {
message: String,
}
// init a background process on the command, and emit periodic events only to the window that used the command
#[tauri::command]
fn init_process(window: Window) {
println!("到了事件处理方法里面");
std::thread::spawn(move ||
loop {
window.emit("event-name", Payload { message: "Tauri is awesome!".into() }).unwrap();
thread::sleep(time::Duration::from_millis(500));
}
);
}
// Create the command:
// This command must be async so that it doesn't run on the main thread.
#[tauri::command]
async fn close_splashscreen(window: Window) {
// Close splashscreen
window.get_window("splashscreen").expect("no window labeled 'splashscreen' found").close().unwrap();
// Show main window
window.get_window("main").expect("no window labeled 'main' found").show().unwrap();
}
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![my_custom_command, init_process,close_splashscreen]) // 要记得把init_process加进来,不然会报错 `Unhandled Promise Rejection: command init_process not found`
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
再之后需要前端去调用这个Rust方法
为了能看到效果,需要加一个定时器
Greet.vue中:
onMounted(() => {
// console.log(a * b);
setTimeout(() => {
invoke('close_splashscreen')
}, 3000)
})
执行 npm run tauri dev
,
而后会进入到主界面
多窗口
https://tauri.app/v1/guides/features/multiwindow
-
静态窗口: 打开软件时直接弹出两个窗口(这种很少见)
-
动态窗口
还是写一个按钮事件来触发
const newWindow = () => {
const webview = new WebviewWindow('theUniqueLabel', {
url: 'test.html',
})
// since the webview window is created asynchronously,
// Tauri emits the `tauri://created` and `tauri://error` to notify you of the creation response
webview.once('tauri://created', function () {
// webview window successfully created 窗口创建成功时触发的逻辑
console.log("创建成功")
})
webview.once('tauri://error', function (e) {
// an error occurred during webview window creation 窗口创建失败时触发的逻辑
console.log("创建失败",e)
})
}
// ...
<button @click="newWindow">新建窗口</button>
另外在public下新建一个test.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE-edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>新建窗口</div>
</body>
</html>
现在想要实现打开新窗口时,隐藏原来的主界面. 实现办法是点击按钮触发创建新窗口操作时,先把主界面隐藏掉,等新窗口创建成功,再把主界面关掉 (如果在新窗口没有创建出来前就直接close,直接退出程序了)
可以通过label指定具体的页面
还可以通过在json文件中,配置相应的参数
如:
{
"width": 200,
"height": 100,
"decorations": false,
"url": "test.html",
"label": "test",
"visible": false
}
WiX打包
Windows系统打包: https://tauri.app/v1/guides/building/windows 是否支持win7需要特殊配置
Mac系统打包: https://tauri.app/v1/guides/building/macos
我主要试一下Mac下的打包
npm run tauri build
或 cargo tauri build
npm run tauri build
> tauri-app@0.0.0 tauri
> tauri build
Error You must change the bundle identifier in `tauri.conf.json > tauri > bundle > identifier`. The default value `com.tauri.dev` is not allowed as it must be unique across applications.
根据 [Tauri的安装、启动、打包和很多用例(第一部分)](https://blog.csdn.net/qq_39124701/article/details/129015210 "Tauri的安装、启动、打包和很多用例(第一部分 "Tauri的安装、启动、打包和很多用例(第一部分)")"), com.tauri.dev
随便改个名字,不叫dev就行.
这里就改成 com.tauri.shuang
再次执行,还是报了一个错:
error TS2345: Argument of type '{}' is not assignable to parameter of type 'Partial<{}> & Omit<{ readonly meg: string; } & VNodeProps & AllowedComponentProps & ComponentCustomProps & Readonly<...>, never> & Record<...>'.
Property 'meg' is missing in type '{}' but required in type 'Omit<{ readonly meg: string; } & VNodeProps & AllowedComponentProps & ComponentCustomProps & Readonly<ExtractPropTypes<__VLS_TypePropsToRuntimeProps<{ meg: string; }>>>, never>'.
40 <Greet/>
~~~~~
Found 1 error in src/App.vue:40
Error beforeBuildCommand `npm run build` failed with exit code 2
问了下ChatGPT 什么问题?如何解决?
这个错误是 TypeScript 编译器的错误。它指出在文件 "src/App.vue" 的第 40 行,你传递给组件 <Greet/>
的属性不符合类型要求。
根据错误信息,组件 <Greet/>
需要一个名为 "meg" 的属性,类型为字符串。然而,你传递给组件的属性是一个空对象 {}
,没有包含必需的 "meg" 属性。
要解决这个问题,你需要确保在使用组件 <Greet/>
时传递一个包含 "meg" 属性的对象。例如:
<Greet meg="Hello"/>
或者,如果 "meg" 属性是可选的,你可以将其设置为可选属性:
interface GreetProps {
meg?: string;
}
// ...
<Greet meg="Hello"/>
通过这样做,你应该能够解决这个 TypeScript 编译器错误,并成功构建你的应用程序。
然后就可以了~
跨平台构建:
可以针对 Apple Silicon、基于 Intel 的 Mac 计算机或通用 macOS 二进制文件编译应用程序。默认情况下,CLI 会构建一个针对你计算机架构的二进制文件。如果想针对不同的目标进行构建,则必须首先通过运行 rustup target add aarch64-apple-darwin 或 rustup target add x86_64-apple-darwin
来安装该目标缺少的 rust 目标,然后您可以使用 --target
-
tauri build --target aarch64-apple-darwin :针对 Apple 硅机器。 -
tauri build --target x86_64-apple-darwin :针对基于 Intel 的机器。 -
tauri build --target universal-apple-darwin :生成可在 Apple 芯片和基于 Intel 的 Mac 上运行的通用 macOS 二进制文件。
NSIS打包
Tauri 1.4版本新增了这种打包方式
软件更新
https://tauri.app/v1/guides/distribution/updater
将这段代码增加到json文件中
"updater": {
"active": true,
"endpoints": [
"https://releases.myapp.com/{{target}}/{{arch}}/{{current_version}}"
],
"dialog": true,
"pubkey": "YOUR_UPDATER_SIGNATURE_PUBKEY_HERE"
}
主要需要设置服务器地址和公钥
服务器接口返回一个json,大概是版本,更新内容等,需要额外开发.
生成公钥:
npm run tauri signer generate -- -w $HOME/.tauri/myapp.key
把得到的公钥复制到json文件pubkey后面
另外可能还需要给TAURI_PRIVATE_KEY加入环境变量
Rust相关的代码,空间占用非常之大..
完整代码:
tauri-app[6]
参考资料
Tauri入门教程: https://www.bilibili.com/video/BV1Za411N7dN/
[2]Tauri官网: https://tauri.app/
[3]可参考: https://stackoverflow.com/questions/75013520/when-i-install-and-run-tauri-on-mac-os-monterey-i-get-immediate-error
[4]实测好用的四个有免费API接口的网站: https://blog.csdn.net/m0_73875883/article/details/130840251
[5]天气接口: https://api.qqsuu.cn/doc/dm-hqtiqnqi.html
[6]tauri-app: https://github.com/cuishuang/tauri-app
本文由 mdnice 多平台发布