前言
本文是基于rust和tauri,由于tauri是前、后端结合的GUI框架,既可以直接生成包含前端代码的文件,也可以在已有的前端项目上集成tauri框架,将前端页面化为桌面GUI。
环境配置
系统:windows 10
平台:visual studio code
语言:rust、javascript
库:tauri2.0
概述
本文使用vite构建一个前端页面,实现简单的计算器功能,然后使用tauri集成前端,实现桌面端的计算器窗口,并使用rust后端进行计算器的功能计算,然后返回前端。
1、创建前端页面
前端框架有很多,本文是基于一个常用的vite框架,来创建一个前端项目,使用原始模板,即不使用vue3或者react这样的前端模板,而是vite框架下,原生的javascript和html。
我们打开visual studio code,如果你有现成的根文件夹,直接打开,或者创建一个新的根文件夹:
bash
mkdir tauri
cd tauri
然后初始化一个vite项目:
bash
npm create vite@latest
初始化时,将需要填写一个基本信息:
如上图,项目名称自定义,前端框架选原生,语言选js。然后会在你的根文件夹下生成一个项目:
我们可以先看一下这个前端页面运行起来的效果,但这之前还需要安装以下npm包:
bash
cd vitepro
npm install
npm run dev
这里是vite提供的模板示例,是一个计数器,点击网页下端的按钮,计数值会改变。
但我们需要的是一个计算器界面,所以需要对代码进行修改。先看一下vite提供的示例代码的结构:
如上图所示,main.js是主函数入口,counter.js才是计数功能模块。
main.js源码
javascript
import './style.css'
import javascriptLogo from './javascript.svg'
import viteLogo from '/vite.svg'
import { setupCounter } from './counter.js'
document.querySelector('#app').innerHTML = `
<div>
<a href="https://vite.dev" target="_blank">
<img src="${viteLogo}" class="logo" alt="Vite logo" />
</a>
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript" target="_blank">
<img src="${javascriptLogo}" class="logo vanilla" alt="JavaScript logo" />
</a>
<h1>Hello Vite!</h1>
<div class="card">
<button id="counter" type="button"></button>
</div>
<p class="read-the-docs">
Click on the Vite logo to learn more
</p>
</div>
`
setupCounter(document.querySelector('#counter'))
其中,setupCounter是由counter.js中导出的模块。官方提供的这个示例比较简单,我们希望稍作修改,首先是将app这个div的内容提取出来,写在单独的html文件中,可以将其命名为template.html
html
<div>
<a href="https://vite.dev" target="_blank">
<img src="${viteLogo}" class="logo" alt="Vite logo" />
</a>
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript" target="_blank">
<img src="${javascriptLogo}" class="logo vanilla" alt="JavaScript logo" />
</a>
<h1>Hello Vite!</h1>
<div class="card">
<button id="counter2" type="button"></button>
</div>
<p class="read-the-docs">
Click on the Vite logo to learn more
</p>
</div>
然后将template.html放在public文件夹中,方便调用。
接着修改main.js,以便于获取template.html中的内容:
javascript
import './style.css'
import javascriptLogo from './javascript.svg'
import viteLogo from '/vite.svg'
import { setupCounter } from './counter.js'
async function loadTemplate(path,divid) {
const response = await fetch(path);
const template = await response.text();
document.querySelector(divid).innerHTML = template
.replace('${viteLogo}', viteLogo)
.replace('${javascriptLogo}', javascriptLogo);
setupCounter(document.querySelector('#counter'))
}
loadTemplate('./template.html','#app')
此处,使用fetch来获取本地html文件的内容,效果是一致的。接着,我们修改template.html的页面,设计一个简单的计算器布局,此处是前端程序,比较简单,代码如下,可以参考,也可以直接去使用人工智能生成。
template.html
html
<label for="history">历史:</label>
<input type="text" class="history" id="history" disabled>
<label for="display">实时:</label>
<input type="text" class="display" id="display" disabled>
<div class="buttons">
<button class="button" id="clearbtn" >C</button>
<button class="button" id="squarebtn">X²</button>
<button class="button" id="backbtn">←</button>
<button class="button operator" id="mulbtn" >*</button>
<button class="button" id="sevenbtn" >7</button>
<button class="button" id="eightbtn" >8</button>
<button class="button" id="ninebtn" >9</button>
<button class="button operator" id="divbtn" >/</button>
<button class="button" id="fourbtn" >4</button>
<button class="button" id="fivebtn" >5</button>
<button class="button" id="sixbtn" >6</button>
<button class="button operator" id="addbtn" >+</button>
<button class="button" id="onebtn" >1</button>
<button class="button" id="twobtn" >2</button>
<button class="button" id="threebtn" >3</button>
<button class="button operator" id="subbtn" >-</button>
<button class="button" id="zerobtn" >0</button>
<button class="button" id="dotbtn" >.</button>
<button class="button equal" id="equalbtn" >=</button>
</div>
上面的代码在网页上看起来如下:
注意,页面布局还需要配合css样式:
global.css
css
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #ffffff;
}
.app {
background-color: #73b4f1;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
}
.display {
height: 30px;
text-align: right;
margin-bottom: 10px;
padding: 10px;
font-size: 18px;
border: 1px solid #e7adad;
border-radius: 5px;
}
.history{
height: 30px;
text-align: right;
margin-bottom: 10px;
padding: 10px;
font-size: 18px;
border: 1px solid #e7adad;
border-radius: 5px;
}
.buttons {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
}
.button {
padding: 20px;
font-size: 18px;
border: none;
border-radius: 5px;
background-color: #f0f0f0;
cursor: pointer;
}
.button:hover {
background-color: #ddd;
}
.button.operator {
background-color: #ff9500;
color: white;
}
.button.operator:hover {
background-color: #e08900;
}
.button.equal {
background-color: #34c759;
color: white;
grid-column: span 2;
}
.button.equal:hover {
background-color: #2da94f;
}
页面布局写好了,就需要为按钮实现功能了,这部分需要写在js中,我们将修改counter.js的代码,为其中添加以下函数:
1、清除函数
2、数字输入函数
3、计算函数(加减乘除以及平方)
4、退格函数
以上是本文将要实现的计算器功能,当然只是简单的实现,并非是一个完整的计算器功能,本文的目的是通过这个小的计算器程序来说明tauri集成前端的示例。
counter.js
javascript
let currentInput='';
let previousInput='';
let operator=null;
let predisplay=''
let calflag=false;
// 导出一个函数,用于在输入框中添加数字
export function appendnumber(element,number,display,history) {
// 给元素添加点击事件
element.addEventListener('click',() =>{
///如果连续输入,则直接拼接
currentInput += number;
///输入值将实时显示在输入框内
display.value = currentInput;
///判断是否进行了计算,如果进行了计算,则历史值清空,显示新的实时输入
if (calflag){
history.value = currentInput;
calflag=false;
} else {
history.value =history.value + number;
}
})
}
// 导出一个函数,用于在元素上添加功能符号的点击事件
export function appendOperator(element,op,display,history) {
// 为元素添加点击事件
element.addEventListener('click',() => {
///如果点击了功能符号,但计算完成标记存在,则不进行任何回应
if (calflag){
return;
}
///如果多次点击功能符号而不是输入下一个数值,则切换功能符号
if (currentInput === '') {
operator = op;
history.value =previousInput + ' ' + operator;
return;
};
///如果前次输入值不为空,则直接计算结果
if (previousInput !== '') {
//calculate();
insidecalculate();
}
operator = op;
previousInput = currentInput;
display.value = previousInput;
history.value =currentInput + ' ' + operator;
predisplay=display.value;
currentInput = '';
})
}
function insidecalculate(){
if (currentInput === '' || previousInput === '') return;
let result;
const prev = parseFloat(previousInput);
const current = parseFloat(currentInput);
switch (operator) {
case '+':
result = prev + current;
break;
case '-':
result = prev - current;
break;
case '*':
result = prev * current;
break;
case '/':
result = prev / current;
break;
default:
return;
}
currentInput = result.toString();
}
// 导出一个函数,用于计算
export function calculate(element,display,history) {
// 给element添加点击事件监听
element.addEventListener('click',()=>{
// 如果currentInput或previousInput为空,则返回
if (currentInput === '' || previousInput === '') return;
let result;
// 将previousInput和currentInput转换为浮点数
const prev = parseFloat(previousInput);
const current = parseFloat(currentInput);
// 根据operator的值进行计算
switch (operator) {
case '+':
result = prev + current;
break;
case '-':
result = prev - current;
break;
case '*':
result = prev * current;
break;
case '/':
result = prev / current;
break;
default:
return;
}
// 将计算结果转换为字符串,赋值给currentInput
currentInput = result.toString();
// 将计算结果添加到history中
history.value =history.value + '=' + currentInput + '\n';
// 将计算结果显示在display中
display.value = currentInput;
// 将display的值赋值给predisplay
predisplay=display.value;
// 将operator置为null
operator = null;
// 将previousInput置为空
previousInput = '';
// 将currentInput置为空
currentInput='';
// 将calflag置为true
calflag=true;
})
}
// 导出一个函数,用于清除显示
export function clearDisplay(element,operator,display,history) {
// 为element添加点击事件监听器
element.addEventListener('click',()=>{
// 将currentInput设置为空字符串
currentInput = '';
// 将operator设置为null
operator = null;
// 将previousInput设置为空字符串
previousInput = '';
// 将display的值设置为空字符串
display.value = '';
// 将predisplay设置为空字符串
predisplay='';
// 将history的值设置为空字符串
history.value='';
})
}
// 导出一个函数,用于返回上一个输入的字符
export function backLast(element,display,history) {
// 给element添加点击事件
element.addEventListener('click',()=>{
// 如果calflag为false
if (!calflag) {
// 将currentInput的最后一个字符删除
currentInput = currentInput.slice(0, -1);
// 将删除后的currentInput赋值给display的value
display.value = currentInput;
// 将history的最后一个字符删除
history.value = history.value.slice(0, -1);
} else {
// 将currentInput的最后一个字符删除
currentInput = currentInput.slice(0, -1);
// 将删除后的currentInput赋值给display的value
display.value = currentInput;
}
})
}
// 导出一个函数,用于计算平方
export function squarenumber(element,display,history) {
// 给element元素添加点击事件
element.addEventListener('click',()=>{
// 将currentInput的值平方
currentInput = Math.pow(currentInput,2);
// 将平方后的值显示在display元素中
display.value = currentInput;
// 将计算过程添加到history元素中
history.value = history.value + '^2' + '=' + currentInput + '\n';
// 将previousInput和currentInput的值重置为空
previousInput = '';
currentInput='';
// 将calflag的值设置为true
calflag=true;
})
}
然后在main.js中导入上述函数:
javascript
import { appendnumber,appendOperator,calculate,clearDisplay,backLast,squarenumber } from './counter.js'
2、tauri集成
完成了前端页面后,我们现在来手动集成tauri。tauri的官网提供了详细的步骤,在我们上面创建的项目文件夹vitepro下,安装Tauri 的 CLI 工具:
bash
npm install -D @tauri-apps/cli@latest
安装完成后,node包文件夹下显示tauri库:
还是在当前项目文件夹的根目录下,我们使用tauri的cli工具,初始化一个tauri,使其包含在我们已经创建好的前端项目中:
bash
npx tauri init
运行后,会让你填写几个问题:
bash
✔ What is your app name? · vitepro
✔ What should the window title be? · vitepro
? Where are your web assets (HTML/CSS/JS) located, relative to the "<current dir>/src-tauri/tauri.conf.json" file that will be created? ›
✔ Where are your web assets (HTML/CSS/JS) located, relative to the "<current dir>/src-tauri/tauri.conf.json" file that will be created? · ..
✔ What is the url of your dev server? · http://localhost:5173
✔ What is your frontend dev command? · npm run dev
✔ What is your frontend build command? · npm run build
由于我们是集成在vite构建的前端项目中,所以url可以使用默认的
初始化完成后,可以看到项目文件夹下会多出一个src-tauri文件夹:
此时,我们可以使用:
bash
npx tauri dev
来运行一下看看:
看起来还可以,不过默认的窗口尺寸并不匹配,我们可以修改一下窗口参数。
在src-tauri文件夹下,有一个配置文件:tauri.conf.json,其中有个app参数,修改其中的windows项,将窗口宽度设置为400。
json
"app": {
"windows": [
{
"title": "vitepro",
"width": 400,
"height": 600,
"resizable": true,
"fullscreen": false
}
],
"security": {
"csp": null
}
},
修改后如下:
看起来要好多了,事实上,窗口还可以美化,但是本文暂不再赘述。
3、后端函数替代
现在,我们想把之前在javascript中的函数功能放到rust中实现,再通过接口在前端调用,实现前后端的数据通讯。此处,tauri提供了两种方法,一种是使用javascript API库,另一个是使用withGlobalTauri这个配置,实现预构建API。
1、如果使用javascript API,那么需要先安装@tauri-apps/api
:
使用
bash
npm install @tauri-apps/api
指令安装api库,安装完成后调用invoke函数:
javascript
import { invoke } from '@tauri-apps/api/core'
2、如果使用withGlobalTauri配置,那么,需要修改tauri.conf.json文件中的build项:
json
{
"build": {
...
"withGlobalTauri": true
},
添加withGlobalTauri,并设置为true。这里省略了其他参数的设置。
然后在js中调用:
javascript
const invoke = window.__TAURI__.invoke
其中,invoke函数的参数有三个,第一个是要调用的函数名,第二个是函数的参数(如果有的话),第三个是一个可选项(一个headers参数,用于操作http请求和响应的接口)
如果要在前端调用rust,首先你需要在rust中编写好函数,然后才能在前端使用,在tauri初始化的src-tauri文件夹中,我们打开lib.rs文件,添加一个函数:
rust
#[tauri::command]
fn greet(name:&str) -> String {
format!("hello,this is from rust msg,your name is {}!",name)
}
注意到,这里函数被标记了,使用#[tauri::command]
来表明这是一个可以被前端使用的rust函数,但是这还不够,还需要将这个函数传递给构建列表:
rust
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet])
.setup(|app| {
if cfg!(debug_assertions) {
app.handle().plugin(
tauri_plugin_log::Builder::default()
.level(log::LevelFilter::Info)
.build(),
)?;
}
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
注意上面代码中的invoke_handler,标记的函数通过它来传递。我们来测试一下,为前端添加一个文本标签,然后程序运行后,显示一个从rust函数传递过来的字符串。
我们先修改template.html,添加一个新标签:
html
<div id="titlediv" class="titlediv">
<p id="titlep">等待rust传递的字符:</p>
</div>
设置一下背景色,以做区分。先看下效果:
现在我们还没有调用rust的函数,所以只显示设定的内容。
然后在main.js中,我们来修改这个标签,将其内容替换为从rust函数获取的字符串:
javascript
const titlep=document.getElementById('titlep');
const greeting=await invoke('greet');
titlep.innerHTML +=greeting;
console.log(greeting);
同时修改一下rust端的函数:
rust
#[tauri::command]
fn greet() -> String {
format!("hello,这里是rust数据!")
}
再次运行:
说明是正确的从前端调用了rust的函数,并获取了返回的内容。
现在,我们尝试将之前的计算函数写到rust中,然后前端这边点击相应按钮时,调用rust函数,并返回结果。
以等号的功能为例,原先的等号功能的核心逻辑是:
javascript
// 根据operator的值进行计算
switch (operator) {
case '+':
result = prev + current;
break;
case '-':
result = prev - current;
break;
case '*':
result = prev * current;
break;
case '/':
result = prev / current;
break;
default:
return;
}
我们就将这段替换掉,先在rust中新建一个calculate_rs函数:
rust
#[tauri::command]
fn calculate_rs(
current_input:f32,
previous_input:f32,
operator:&str
) -> f32 {
match operator {
"+" => current_input + previous_input,
"-" => current_input - previous_input,
"*" => current_input * previous_input,
"/" => current_input / previous_input,
_ => 0.0,
}
}
这个函数就是根据输入数字和计算符返回相应结果,当然这里比较简单,没有进行错误处理。
在js中调用此函数来计算:
javascript
const cal=await invoke('calculate_rs',{currentInput:current,previousInput:prev,operator:operator})
4、实例演示
上面介绍了代码,下面来看一下动态演示:
tauri简单计算器程序演示