使用Tauri开发桌面应用

本文是对视频 Tauri入门教程[1]的学习与记录

Tauri官网[2]


对 node版本有要求

alt

创建项目及目录介绍:

alt

项目的目录结构如下

alt

可以安装推荐的插件

alt

执行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就可以了~

alt

跟后端无关的调试, 可以直接 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> 
alt

把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");
}

alt

可能会有同步和异步的情况,所以为了更易读,可以这样改写前端代码:

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");
}
alt

会不停请求,加一个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描述查询 查询网站标题关键词描述等等

alt

例如 https://api.qqsuu.cn/api/dm-info?url=https://dashen.tech&apiKey=xxxxxxxx

alt
{
    "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
      }
    },

否则会报错

alt

这样就可以调用 https://api.qqsuu.cn/*下面所有的接口了~

参考文档上的这段:

import { fetch } from '@tauri-apps/api/http';
const response = await fetch('http://localhost:3003/users/2', {
  method'GET',
  timeout30,
});

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>

alt

文件系统 && 将本地文件转为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>
alt

这是因为allowlist里面写的是$RESOURCE/,但代码里用的是desktopDir,所以需要将assetScope设置为"$DESKTOP/*" (或增加"$DESKTOP/*"),即

"assetScope": ["$RESOURCE/*","$DESKTOP/*"]
alt

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' });

alt

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
}
alt

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 {
  height30px;
  background#329ea3;
  user-select: none;
  display: flex;
  justify-content: flex-end;
  position: fixed;
  top0;
  left0;
  right0;
}
.titlebar-button {
  display: inline-flex;
  justify-content: center;
  align-items: center;
  width30px;
  height30px;
}
.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',
    timeout30,
  });
  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({
    directorytrue,
    multipletrue,
    defaultPathawait 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 {
  height30px;
  background#329ea3;
  user-select: none;
  display: flex;
  justify-content: flex-end;
  position: fixed;
  top0;
  left0;
  right0;
}

.titlebar-button {
  display: inline-flex;
  justify-content: center;
  align-items: center;
  width30px;
  height30px;
}

.titlebar-button:hover {
  background#5bbec3;
}
</style>


alt

尝试去掉左侧的按钮.

需要修改窗口的配置 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
      }
alt

这样左上方的按钮就没有了


一些其他配置:

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,

alt

而后会进入到主界面


多窗口


https://tauri.app/v1/guides/features/multiwindow


  1. 静态窗口: 打开软件时直接弹出两个窗口(这种很少见)

  2. 动态窗口

还是写一个按钮事件来触发


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>
alt

现在想要实现打开新窗口时,隐藏原来的主界面. 实现办法是点击按钮触发创建新窗口操作时,先把主界面隐藏掉,等新窗口创建成功,再把主界面关掉 (如果在新窗口没有创建出来前就直接close,直接退出程序了)

可以通过label指定具体的页面

还可以通过在json文件中,配置相应的参数

如:

  {
      "width"200,
      "height"100,
      "decorations"false,
      "url""test.html",
      "label""test",
      "visible"false
    }
alt

WiX打包


Windows系统打包: https://tauri.app/v1/guides/building/windows 是否支持win7需要特殊配置

Mac系统打包: https://tauri.app/v1/guides/building/macos


我主要试一下Mac下的打包

npm run tauri buildcargo 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 编译器错误,并成功构建你的应用程序。


然后就可以了~

alt
alt
alt
alt

跨平台构建:

可以针对 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相关的代码,空间占用非常之大..

alt
alt

完整代码:

tauri-app[6]

参考资料

[1]

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 多平台发布

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/146865.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

缺陷预测(一)——论文复现

运行CGCN文件 问题一&#xff1a;CNN输入维度的问题出现的问题解决问题原因 问题二&#xff1a;mix时&#xff0c;输入的train_in和train_gen.inputs数据格式不一致出现的问题解决问题 最终结果 问题一&#xff1a;CNN输入维度的问题 出现的问题 数据集改好之后&#xff0c;出…

Python Flask: 构建轻量级、灵活的Web应用

大家好&#xff0c;我是涛哥&#xff0c;今天为大家分享 Python Web开发框架 Flask&#xff0c;文章3400字&#xff0c;阅读大约15分钟&#xff0c;大家enjoy~~ Flask是一个流行的Python Web框架&#xff0c;以其轻量级、灵活和易学的特性受到开发者的喜爱。本文将深入探讨Flas…

微服务简单理解与快速搭建

分布式和微服务 含义 微服务架构 微服务架构风格是一种将一个单一应用程序开发为一组小型服务的方法&#xff0c;每个服务运行在自己的进程中&#xff0c;服务间通信采用轻量级通信机制(通常用HTTP资源API)。这些服务围绕业务能力构建并且可通过全自动部署机制独立部署。这些服…

C语言进阶之指针(2)

✨ 猪巴戒&#xff1a;个人主页✨ 所属专栏&#xff1a;《C语言进阶》 &#x1f388;跟着猪巴戒&#xff0c;一起学习C语言&#x1f388; 目录 前情回顾 1.数组参数&#xff0c;指针参数 1.1一维数组传参 1.2二维数组传参 1.3一级指针传参 1.4二级指针传参 思考&#xf…

算法萌新闯力扣:同构字符串

力扣题&#xff1a;同构字符串 开篇 对于字符串相关的题目&#xff0c;哈希表经常会使用到&#xff0c;这道题更是如此&#xff0c;还用到了两个哈希表。拿下它&#xff0c;你对字符串题目的理解就会更上一层楼。 题目链接:205.同构字符串 题目描述 代码思路 看完题目后&a…

Django实战项目-学习任务系统-任务完成率统计

接着上期代码内容&#xff0c;继续完善优化系统功能。 本次增加任务完成率统计功能&#xff0c;为更好的了解哪些任务完成率高&#xff0c;哪些任务完成率低。 该功能完成后&#xff0c;学习任务系统1.0版本就基本完成了。 1&#xff0c;编辑urls配置文件&#xff1a; ./mysi…

原论文一比一复现 | 更换 RT-DETR 主干网络为 【ResNet-50】【ResNet-101】【ResNet-152】| 对比实验必备

本专栏内容均为博主独家全网首发,未经授权,任何形式的复制、转载、洗稿或传播行为均属违法侵权行为,一经发现将采取法律手段维护合法权益。我们对所有未经授权传播行为保留追究责任的权利。请尊重原创,支持创作者的努力,共同维护网络知识产权。 更深层的神经网络更难训练。…

香港科技大学广州|机器人与自主系统学域博士招生宣讲会—电子科技大学专场!!!(暨全额奖学金政策)

在机器人和自主系统领域实现全球卓越—机器人与自主系统学域 硬核科研实验室&#xff0c;浓厚创新产学研氛围&#xff01; 教授亲临现场&#xff0c;面对面答疑解惑助攻申请&#xff01; 一经录取&#xff0c;享全额奖学金1.5万/月&#xff01; &#x1f559;时间&#xff1a;…

基于谐波参数空间的卷积神经网络自动三维牙齿分割

论文连接&#xff1a;https://www.sciencedirect.com/science/article/abs/pii/S1524070320300151 机构&#xff1a; a英国卡迪夫大学计算机科学与信息学院 b中国科学院大学北京 c中国科学院计算技术研究所北京 d深圳大数据研究院&#xff0c;深圳518172 代码链接&#x…

4路光栅尺磁栅尺编码器解码转换5MHz高速差分信号转Modbus TCP网络模块 YL97-RJ45

特点&#xff1a; ● 光栅尺磁栅尺解码转换成标准Modbus TCP协议 ● 光栅尺5V差分信号直接输入&#xff0c;4倍频计数 ● 模块可以输出5V的电源给光栅尺供电 ● 高速光栅尺磁栅尺计数&#xff0c;频率可达5MHz ● 支持4个光栅尺同时计数&#xff0c;可识别正反转 ● 可网…

Looking for downloadable pre-built shared indexes关闭

这个功能很烦,把他关闭就行了 PyCharm的“Looking for downloadable pre-built shared indexes”是指PyCharm IDE中的一个功能&#xff0c;用于搜索和下载可共享的预构建索引。 这个功能的主要用途是帮助开发人员在开发过程中快速地获取和使用预构建的索引&#xff0c;以提高…

算法笔记-第七章-链表(未完成)

算法笔记-第七章-链表 链表的遍历链表结点的个数链表的头插法!链表删除元素链表反转例题思路一:原地反转思路二:头插法链表去除重复元素(有些复杂了)思路题目一题目二链表的遍历 #include<cstdio> const int N = 100; struct Node {int data, next;//表示的是当前数据和…

基于K7的PXIPXIe数据处理板(Kintex-7 FMC载板)

基于PXI&PXIe总线架构的高性能数据预处理FMC 载板&#xff0c;板卡具有 1 个 FMC&#xff08;HPC&#xff09;接口&#xff0c;1 个 X8 PCIe 和1个PCI主机接口&#xff1b;板卡采用 Xilinx 的高性能 Kintex-7 系列 FPGA 作为实时处理器&#xff0c;实现 FMC 接口数据的采集…

网络安全-学习手册

前言 前几天发布了一篇 网络安全&#xff08;黑客&#xff09;自学 没想到收到了许多人的私信想要学习网安黑客技术&#xff01;却不知道从哪里开始学起&#xff01;怎么学 今天给大家分享一下&#xff0c;很多人上来就说想学习黑客&#xff0c;但是连方向都没搞清楚就开始学习…

【postgresql】CentOS7 安装Pgweb

Pgweb Pgweb是PostgreSQL的一个基于web的数据库浏览器&#xff0c;用Go编写&#xff0c;可在Mac、Linux和Windows机器上运行。以零依赖性的简单二进制形式分布。非常易于使用&#xff0c;并具有适当数量的功能。简单的基于web和跨平台的PostgreSQL数据库浏览器。 特点 跨平台…

ubuntu22.04 x86环境上使用QEMU搭建arm虚拟机

1、安装qemu及相关依赖 apt-get -y install qemu apt-get -y install bridge-utils apt-get -y install vnc4server apt-get -y install qemu-kvm apt install -y qemu-system-arm apt-get -y install libvirt0 apt-get -y install libvirt-daemon apt-get -y install l…

Maya动画怎么云渲染?如何避免渲染出错?100%解决方案在这!

1.为什么Maya要使用云渲染&#xff1f; Autodesk Maya是一款3D动画和视觉效果软件&#xff0c;在影视、游戏和广告等各个领域中得到了广泛应用。许多知名的动画制作公司和工作室都使用Maya来制作角色动画和特效。然而&#xff0c;随着视觉效果的不断提升&#xff0c;渲染工作量…

​如何解决SSD NAND Path冲突导致的性能问题?

1.引言 最近看到一篇关于SSD的NAND并发瓶颈相关的论文&#xff0c;思路非常好&#xff0c;这里分享给大家。本篇论文的解读&#xff0c;也是小编上周末在高铁上完成的。存储随笔的论文解读&#xff0c;不是直接翻译&#xff0c;是小编先研读一遍后&#xff0c;再结合自己的理解…

爬虫----robots.txt 协议简介

文章目录 robots.txt 是一个用于指示网络爬虫(web spider或web robot)如何与网站上的内容进行交互的协议。这个文件被网站管理员放置在网站的根目录下,用于告知爬虫哪些部分的网站是可以被抓取的,哪些是不被允许的。以下是 robots.txt 协议的一些关键要点: 控制爬虫访问:…

滴滴 Redis 异地多活的演进历程

为了更好的做好容灾保障&#xff0c;使业务能够应对机房级别的故障&#xff0c;滴滴的存储服务都在多机房进行部署。本文简要分析了 Redis 实现异地多活的几种思路&#xff0c;以及滴滴 Redis 异地多活架构演进过程中遇到的主要问题和解决方法&#xff0c;抛砖引玉&#xff0c;…