发布于 2026-01-06 4 阅读
0

使用 Rust 和 NextJS 开发桌面应用程序。Tauri 的方法。DEV 全球展示挑战赛,由 Mux 呈现:展示你的项目!

使用 Rust 和 NextJS 开发桌面应用程序:Tauri 的方法。

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

介绍

本文将为您介绍一个具体而又令人兴奋的主题,是我上一篇文章的续篇。如果您对 Rust 集成感兴趣,请阅读《Node & Rust:永恒的友谊——NAPI-rs 之道》

我想各位亲爱的同事们,你们都使用过VSCode,或者至少听说过它。你们有没有想过 VSCode 的开发使用了哪些技术?如果我说 VSCode 主要用 TypeScript 编写,你们可能会感到惊讶。但是等等……TypeScript 和 JavaScript 通常用于 Web 或后端应用程序,而 VSCode 是一个独立的 UI 应用程序。那么,是否有可能创建一个基于 JavaScript 的独立 UI 应用程序呢?答案是肯定的!

如果我们几个月前讨论这个话题,我会推荐ElectronJS,如果你想创建一个独立的 JavaScript 应用程序。此外,我还会提供以下一些流行的基于 Electron 的应用程序列表。

  • 微软团队
  • 飞涨
  • 桌面版 Slack
  • WordPress桌面版
  • Skype
  • Discord
  • WhatsApp桌面版
  • 邮差
  • MongoDB Compass

但现代 IT 世界瞬息万变,我们已经有了 ElectronJS 的强大竞争对手(顺便说一句,它可能在不久的将来成为 ElectronJS 的终结者)。

认识一下陶里

如果您想简要比较 Tauri 和 Electron,请阅读这篇文章“再见 Electron,你好 Tauri”这篇文章也对您有所帮助,它可以帮助您了解 Tauri 的优势和一些简要的技术细节。

为了满足我那些心急的读者,我做了一个简要的比较。

框架 “前端” 后端
电子 Chromium浏览器 NodeJS
金牛座 原生 Webview Rust 编译代码

关于上文提到的原生 WebView,这里需要补充一点。您可以在这里找到关于此主题的详细信息。简而言之,Tauri 应用在 macOS 上使用 Webkit(Safari 引擎)作为 HTML 渲染器,在 Windows 上使用Microsoft Edge WebView2,在 Linux 上使用WebKitGTK(Webkit 的 Linux 移植版)。请注意,根据上述信息,Tauri 应用在不同平台上的行为可能有所不同。

根据上表,我们可以得出什么结论?Tauri 注重性能和简洁性!作为一名从事 Electron 相关项目多年的开发者,我非常确定 NodeJS 可能会成为瓶颈,原因如下。

  1. NodeJS 是一个重量级的解决方案,架构也很复杂。我的意思是,它还集成了 V8、带有事件循环的 LibUV 等组件。
  2. 如果我们需要实现图像处理、数据处理或复杂的数学计算等繁重任务,NodeJS 并不是一个好的选择。
  3. 进程间通信(Electron IPC)是Electron中前端和后端之间的一种通信方式。其功能在编码方面过于复杂。
  4. 在我们基于 Electron 的应用程序中实现基于 NodeJS 的多线程“后端”可能是一场噩梦。

Tauri 可以驳斥以上所有缺点,原因如下。

  1. Rust 编译后的代码只包含所需的最低限度功能(没有冗余的架构内容,例如 V8 或 LibUV)。
  2. Rust 对多线程友好,并允许我们实现跨平台功能。
  3. Rust 充满了有用的内存安全机制,可以防止开发人员犯错,因此,我们可以获得高质量、可预测的代码。
  4. 使用 Rust 编译的代码比基于 NodeJS 的代码性能更高。

在我看来,上述优点对于“后端”至关重要。正因如此,基于以上原因,我认为Tauri方法是一个可行的方案。

顺便说一句,如果您不是 Rust 专家,并且想了解一些关于 Rust 多线程的新知识,请阅读《面向急于求成的 Rust 学习者的多线程》一文

目标

当然,Tauri 是一个全新的概念。尽管如此,它的资料非常完善。有很多关于这个主题的精彩文章,我推荐阅读或观看以下资源。

我的目标是提供一个全新的应用供您运行和测试。我使用基于 NextJS 和 Ant Design 的前端框架创建了一个 Tauri 应用,后端则包含一些看似比较复杂的计算。该应用会在屏幕上显示进度条,相关的进度数据则在后端(Rust 语言)进行处理。

第一步

我们开始吧!

创建“前端”部分



npx create-next-app@latest --use-npm --typescript


Enter fullscreen mode Exit fullscreen mode

请回答以下问题……

第一屏

安装 Tauri 依赖项



cd tauri-nextjs-demo
npm i --save-dev @tauri-apps/cli
npm i @tauri-apps/api --save


Enter fullscreen mode Exit fullscreen mode

更新

更新next.config.js



/** @type {import('next').NextConfig} */

const nextConfig = {
  reactStrictMode: true,
  // Note: This feature is required to use NextJS Image in SSG mode.
  // See https://nextjs.org/docs/messages/export-image-api for different workarounds.
  images: {
    unoptimized: true,
  },
};

module.exports = nextConfig;


Enter fullscreen mode Exit fullscreen mode

更新scripts部分package.json



{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "export": "next export",
    "start": "next start",
    "tauri": "tauri",
    "lint": "next lint"
  }
}


Enter fullscreen mode Exit fullscreen mode

初始化“后端”(Tauri)部分



npm run tauri init


Enter fullscreen mode Exit fullscreen mode

请回答以下问题……

第二屏幕

请回答以下问题……

src-tauri文件夹包含我们的后端部分。

Tauri 源

“后端”功能

第一个自举版本只包含最基本的功能。我们来完善它。

请打开src-tauri/src/main.rs并粘贴以下代码。



#![cfg_attr(
  all(not(debug_assertions), target_os = "windows"),
  windows_subsystem = "windows"
)]
use tauri::Window;
use std::{thread, time};

#[derive(Clone, serde::Serialize)]
struct Payload {
    progress: i16,
}

#[tauri::command]
async fn progress_tracker(window: Window){
  let mut progress = 0;
  loop {
      window.emit("PROGRESS", Payload { progress }).unwrap();
      let delay = time::Duration::from_millis(100);
      thread::sleep(delay);
      progress += 1;
      if progress > 100 {
        break;
      }
  }
}

fn main() {
  tauri::Builder::default()
    .invoke_handler(tauri::generate_handler![progress_tracker])
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}


Enter fullscreen mode Exit fullscreen mode

请注意以下几点。

  1. progress_tracker该函数应该从“前端”(Typescript)部分调用。
  2. #[tauri::command]这是一个属性,它将上述函数定义为对 JavaScript 友好的函数。
  3. window: Window参数应从“前端”传递。
  4. 内部循环progress_tracker每 100 毫秒返回一个数字,重复 100 次。
  5. 请注意函数.invoke_handler(tauri::generate_handler![progress_tracker])本身main。您必须“注册”您的前端友好型函数。

另外,您还需要更改 ` .`tauri.identifier中的值src-tauri/tauri.conf.json。例如,com.buchslava.dev在我的例子中,将其更改为 `.`。
之后,将上面文件中的 ` build.beforeBuildCommand.` 值更改为 ` npm run build && npm run export.`。这一点很重要,因为在这个例子中我们使用了 NextJS SSG。

“前端”首次划痕。

接下来我们进入“前端”部分。

切换到项目根文件夹,并将以下代码放入其中。src/pages/index.tsx



import { invoke } from "@tauri-apps/api/tauri";
import { listen } from "@tauri-apps/api/event";
import { useEffect, useState } from "react";

interface ProgressEventPayload {
  progress: number;
}

interface ProgressEventProps {
  payload: ProgressEventPayload;
}

export default function Home() {
  const [busy, setBusy] = useState<boolean>(false);

  useEffect(() => {
    // listen what can Rust part tell us about
    const unListen = listen("PROGRESS", (e: ProgressEventProps) => {
      console.log(e.payload.progress);
    });

    return () => {
      unListen.then((f) => f());
    };
  }, []);

  return (
    <div>
      {!busy && (
        <button
          onClick={() => {
            setBusy(true);
            setTimeout(async () => {
              const { appWindow } = await import("@tauri-apps/api/window");
              // call Rust function, pass the window
              await invoke("progress_tracker", {
                window: appWindow,
              });
              setBusy(false);
            }, 1000);
          }}
        >
          Start Progress
        </button>
      )}
    </div>
  );
}


Enter fullscreen mode Exit fullscreen mode

是时候运行示例了……



npm run tauri dev


Enter fullscreen mode Exit fullscreen mode

让我们打开开发者控制台(右键单击屏幕 -> 检查 -> 切换到控制台选项卡),然后按下“开始进度”按钮。

第一结果

恭喜!我们已经完成了 Touri 的基本工作,现在是时候专注于“前端”升级了。

你可以在这里找到这个解决方案

添加用户界面部分

我们需要在屏幕上添加一个进度条小部件,并在其上显示进度,而不是在控制台中显示。

首先,安装Ant Design依赖项。



npm i antd --save


Enter fullscreen mode Exit fullscreen mode

第二,删除所有内容src/styles/Home.module.css
第三,将以下内容放入src/styles/globals.css



body {
  position: relative;
  width: 100vw;
  height: 100vh;
  font-family: sans-serif;
  overflow-y: hidden;
  display: flex;
  justify-content: center;
  align-items: center;
}


Enter fullscreen mode Exit fullscreen mode

第四,将以下代码替换到src/pages/index.tsx现有代码中。



import { invoke } from "@tauri-apps/api/tauri";
import { listen } from "@tauri-apps/api/event";
import { useEffect, useState } from "react";
import { Button, Progress } from "antd";

interface ProgressEventPayload {
  progress: number;
}

interface ProgressEventProps {
  payload: ProgressEventPayload;
}

export default function Home() {
  const [busy, setBusy] = useState<boolean>(false);
  const [progress, setProgress] = useState<number>(0);

  useEffect(() => {
    const unListen = listen("PROGRESS", (e: ProgressEventProps) => {
      setProgress(e.payload.progress);
    });

    return () => {
      unListen.then((f) => f());
    };
  }, []);

  return (
    <div>
      <div style={{ width: "70vw" }}>
        <Progress percent={progress} />
      </div>
      <Button
        type="primary"
        disabled={busy}
        onClick={() => {
          setBusy(true);
          setTimeout(async () => {
            const { appWindow } = await import("@tauri-apps/api/window");
            await invoke("progress_tracker", {
              window: appWindow,
            });
            setBusy(false);
          }, 1000);
        }}
      >
        Start Progress
      </Button>
    </div>
  );
}


Enter fullscreen mode Exit fullscreen mode

我们来看看结果……



npm run tauri dev


Enter fullscreen mode Exit fullscreen mode

结果 1

看起来不错。但我这个人比较谨慎,必须百分百确定 Rust 和 NextJS 部分之间的所有功能都完全兼容。我想在前端界面添加一个计时器。这样,进度条和计时器就能同时运行,不会中断。

让我们把以下代码替换掉src/pages/index.tsx现有的代码。



import { invoke } from "@tauri-apps/api/tauri";
import { listen } from "@tauri-apps/api/event";
import { useEffect, useState } from "react";
import { Button, Progress } from "antd";

interface ProgressEventPayload {
  progress: number;
}

interface ProgressEventProps {
  payload: ProgressEventPayload;
}

export default function Home() {
  const [busy, setBusy] = useState<boolean>(false);
  const [progress, setProgress] = useState<number>(0);
  const [timeLabel, setTimeLabel] = useState<string>();

  useEffect(() => {
    const timeIntervalId = setInterval(() => {
      setTimeLabel(new Date().toLocaleTimeString());
    }, 1000);
    const unListen = listen("PROGRESS", (e: ProgressEventProps) => {
      setProgress(e.payload.progress);
    });

    return () => {
      clearInterval(timeIntervalId);
      unListen.then((f) => f());
    };
  }, []);

  return (
    <div>
      <div style={{ position: "fixed", top: 20, left: 20 }}>{timeLabel}</div>
      <div style={{ width: "70vw" }}>
        <Progress percent={progress} />
      </div>
      <Button
        type="primary"
        disabled={busy}
        onClick={() => {
          setBusy(true);
          setTimeout(async () => {
            const { appWindow } = await import("@tauri-apps/api/window");
            await invoke("progress_tracker", {
              window: appWindow,
            });
            setBusy(false);
          }, 1000);
        }}
      >
        Start Progress
      </Button>
    </div>
  );
}


Enter fullscreen mode Exit fullscreen mode

结果 2

是时候完成最后一针了。在进度条功能实现之前,我们需要想办法暂停缝纫。以下修改可以实现这一点。

src-tauri/src/main.rs



#![cfg_attr(
  all(not(debug_assertions), target_os = "windows"),
  windows_subsystem = "windows"
)]
use tauri::Window;
use std::{thread, time};
use std::sync::{Arc, RwLock};

#[derive(Clone, serde::Serialize)]
struct Payload {
    progress: i16,
}

#[tauri::command]
async fn progress_tracker(window: Window){
  // New code
  let stop = Arc::new(RwLock::new(false));
  let stop_clone = Arc::clone(&stop);
  let handler = window.once("STOP", move |_| *stop_clone.write().unwrap() = true);
  // / New code

  let mut progress = 0;
  loop {
      // New code
      if *stop.read().unwrap() {
        break;
      }
      // / New code
      window.emit("PROGRESS", Payload { progress }).unwrap();
      let delay = time::Duration::from_millis(100);
      thread::sleep(delay);
      progress += 1;
      if progress > 100 {
        break;
      }
  }
  window.unlisten(handler); // New code
}

fn main() {
  tauri::Builder::default()
    .invoke_handler(tauri::generate_handler![progress_tracker])
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}


Enter fullscreen mode Exit fullscreen mode

src/pages/index.tsx



import { invoke } from "@tauri-apps/api/tauri";
import { listen } from "@tauri-apps/api/event";
import { useEffect, useState } from "react";
import { Button, Progress } from "antd";

interface ProgressEventPayload {
  progress: number;
}

interface ProgressEventProps {
  payload: ProgressEventPayload;
}

export default function Home() {
  const [busy, setBusy] = useState<boolean>(false);
  const [progress, setProgress] = useState<number>(0);
  const [timeLabel, setTimeLabel] = useState<string>();

  useEffect(() => {
    const timeIntervalId = setInterval(() => {
      setTimeLabel(new Date().toLocaleTimeString());
    }, 1000);
    const unListen = listen("PROGRESS", (e: ProgressEventProps) => {
      setProgress(e.payload.progress);
    });

    return () => {
      clearInterval(timeIntervalId);
      unListen.then((f) => f());
    };
  }, []);

  return (
    <div>
      <div style={{ position: "fixed", top: 20, left: 20 }}>{timeLabel}</div>
      <div style={{ width: "70vw" }}>
        <Progress percent={progress} />
      </div>
      <Button
        type="primary"
        disabled={busy}
        onClick={() => {
          setBusy(true);
          setTimeout(async () => {
            const { appWindow } = await import("@tauri-apps/api/window");
            await invoke("progress_tracker", {
              window: appWindow,
            });
            setBusy(false);
          }, 1000);
        }}
      >
        Start Progress
      </Button>
      {/* New code */}
      <Button
        type="primary"
        disabled={!busy}
        onClick={async () => {
          const { appWindow } = await import("@tauri-apps/api/window");
          await appWindow.emit("STOP");
          setProgress(0);
          setBusy(false);
        }}
      >
        Stop Progress
      </Button>
      {/* / New code */}
    </div>
  );
}


Enter fullscreen mode Exit fullscreen mode

结果 3

看起来很有说服力!

Tauri 中的前端后端通信:实现进度条和中断按钮将为您详细介绍上述技术。

您可以在这里找到相关资料

斋戒

最后,我想重点说说构建部分。让我们开始构建应用吧。顺便说一下,我是在 macOS 系统下工作的。如果您想了解更多关于 Tauri 构建的信息,请阅读这篇文章。开始构建吧!



npm run tauri build


Enter fullscreen mode Exit fullscreen mode

以下信息将帮助您了解在哪里以及可以找到哪些与构建结果相关的内容。您可以在以下位置找到您的构建信息/src-tauri/target/release/bundle:.

在 macOS 中,您可以/src-tauri/target/release/bundle/macos通过安装程序构建独立应用程序/src-tauri/target/release/bundle/dmg

最令人兴奋的是,这款应用只有 4.7MB,而安装程序却只有 2.3MB。你敢信吗?4.7MB 的 Rust、NextJS 和 Ant Design!

图片描述

图片描述

你想把Tauri的结果和Electron的结果比较一下吗?

说实话,看到这个结果,我的过去记忆一下子涌上心头。我想起了20MB的硬盘和IBM PC XT。

IBM PC XT

我还想到了以下几点。太棒了!我可以把2023年的应用程序安装到我1990年的电脑上。听起来就像一台时光机!


PS:感谢Eduardo Speroni提供的有益建议,使本文得以改进。

文章来源:https://dev.to/valorsoftware/developing-a-desktop-application-via-rust-and-nextjs-the-tauri-way-2iin