首页 > web前端 > css教程 > 我如何与Svelte,Redis和Rust构建跨平台桌面应用程序

我如何与Svelte,Redis和Rust构建跨平台桌面应用程序

Joseph Gordon-Levitt
发布: 2025-03-21 11:14:11
原创
896 人浏览过

How I Built a Cross-Platform Desktop Application with Svelte, Redis, and Rust

Cloudflare提供了一个名为Workers KV的优秀产品,这是一个全局复制的键值存储层。它可以处理数百万个键,每个键都可以在Worker脚本中以极低的延迟访问,无论请求来自世界哪个地方。Workers KV令人惊叹——它的定价也很诱人,包括一个慷慨的免费层级。

然而,作为Cloudflare产品线的长期用户,我发现缺少一样东西:本地自省。我的应用程序中经常拥有数千甚至数十万个键,我常常希望有一种方法可以查询所有数据、对其进行排序,或者只是查看实际存在的内容。

最近,我有幸加入了Cloudflare!更重要的是,我是在本季度“快速获胜周”(又名一周黑客马拉松)之前加入的。由于我还没有积累足够的积压工作(尚未),相信我,我会抓住这个机会来实现自己的愿望。

言归正传,让我告诉你我是如何构建Workers KV GUI的,这是一个使用Svelte、Redis和Rust构建的跨平台桌面应用程序。

前端应用

作为一名Web开发者,这是我熟悉的环节。我很想称之为“简单部分”,但是,鉴于您可以使用任何和所有HTML、CSS和JavaScript框架、库或模式,选择瘫痪很容易发生……这可能也很熟悉。如果您有一个喜欢的前端技术栈,那就太好了,用它吧!对于这个应用程序,我选择使用Svelte,因为对我来说,它确实让事情变得简单并保持简单。

此外,作为Web开发者,我们希望随身携带所有工具。你当然可以!同样,项目的这一阶段与典型的Web应用程序开发周期没有什么不同。您可以期望运行yarn dev(或某些变体)作为您的主要命令,并感到宾至如归。为了保持“简单”的主题,我选择使用SvelteKit,这是Svelte官方的用于构建应用程序的框架和工具包。它包括一个优化的构建系统、出色的开发者体验(包括HMR!)、基于文件系统的路由器,以及Svelte本身提供的所有功能。

作为一个框架,特别是自己负责工具的框架,SvelteKit允许我纯粹地考虑我的应用程序及其需求。事实上,就配置而言,我唯一需要做的就是告诉SvelteKit我想构建一个在客户端运行的单页应用程序(SPA)。换句话说,我必须明确地选择退出SvelteKit假设我想要一个服务器,这实际上是一个合理的假设,因为大多数应用程序都可以从服务器端渲染中受益。这就像附加@sveltejs/adapter-static包一样简单,这是一个专门为此目的而创建的配置预设。安装后,我的整个配置文件如下所示:

<code>// svelte.config.js
import preprocess from 'svelte-preprocess';
import adapter from '@sveltejs/adapter-static';

/** @type {import('@sveltejs/kit').Config} */
const config = {
  preprocess: preprocess(),

  kit: {
    adapter: adapter({
      fallback: 'index.html'
    }),
    files: {
      template: 'src/index.html'
    }
  },
};

export default config;</code>
登录后复制

index.html的更改是我的个人偏好。SvelteKit使用app.html作为默认的基本模板,但旧习惯很难改。

仅仅几分钟,我的工具链就已经知道它正在构建一个SPA,已经有一个路由器就绪,并且一个开发服务器随时可用。此外,由于svelte-preprocess,如果我想要(而且我确实想要),TypeScript、PostCSS和/或Sass支持也可用。准备好了!

该应用程序需要两个视图:

  1. 输入连接详细信息的屏幕(默认/欢迎/主页)
  2. 实际查看数据的屏幕

在SvelteKit世界中,这转化为两个“路由”,SvelteKit规定这些路由应该作为src/routes/index.svelte(主页)和src/routes/viewer.svelte(数据查看器页面)存在。在一个真正的Web应用程序中,第二个路由将映射到/viewer URL。虽然情况仍然如此,但我知道我的桌面应用程序不会有导航栏,这意味着URL将不可见……这意味着我如何命名这个路由并不重要,只要它对我来说有意义即可。

这些文件的内容大多无关紧要,至少对于本文而言如此。对于那些好奇的人,整个项目是开源的,如果您正在寻找Svelte或SvelteKit示例,欢迎您查看。冒着像坏掉的唱片一样的风险,这里的重点是我正在构建一个普通的Web应用程序。

此时,我只是设计我的视图并四处抛出虚假的、硬编码的数据,直到我得到一些看起来有效的东西。我在这里待了大约两天,直到一切看起来都很漂亮,并且所有交互性(按钮点击、表单提交等)都被完善了。我会称之为“可运行”的应用程序或模型。

桌面应用程序工具

此时,一个功能齐全的SPA已经存在。它在Web浏览器中运行——并且是在Web浏览器中开发的。也许与直觉相反,这使得它成为成为桌面应用程序的完美候选者!但是如何做到呢?

您可能听说过Electron。它是使用Web技术构建跨平台桌面应用程序的著名的工具。有很多非常流行和成功的应用程序都是用它构建的:Visual Studio Code、WhatsApp、Atom和Slack,仅举几例。它的工作原理是将您的Web资源与其自身的Chromium安装和自身的Node.js运行时捆绑在一起。换句话说,当您安装基于Electron的应用程序时,它会附带一个额外的Chrome浏览器和一整套编程语言(Node.js)。这些都嵌入在应用程序内容中,无法避免,因为这些是应用程序的依赖项,保证它在任何地方都能一致地运行。正如您可能想象的那样,这种方法有一些权衡——应用程序相当庞大(即超过100MB)并且使用大量系统资源来运行。为了使用该应用程序,后台会运行一个全新的/单独的Chrome——这与打开一个新标签页并不完全相同。

幸运的是,有一些替代方案——我评估了Svelte NodeGui和Tauri。通过依赖操作系统提供的原生渲染器,而不是嵌入Chrome副本来完成相同的工作,这两个选择都提供了显著的应用程序大小和利用率节省。NodeGui通过依赖Qt来实现这一点,Qt是另一个编译为原生视图的桌面/GUI应用程序框架。但是,为了做到这一点,NodeGui需要对您的应用程序代码进行一些调整,以便它可以将您的组件转换为Qt组件。虽然我相信这肯定可以工作,但我对这个解决方案不感兴趣,因为我想使用我已知的确切内容,而不需要对我的Svelte文件进行任何调整。相比之下,Tauri通过包装操作系统的原生webviewer来实现其节省——例如,macOS上的Cocoa/WebKit、Linux上的gtk-webkit2以及Windows上的Edge上的Webkit。Webviewer实际上是浏览器,Tauri使用它们是因为它们已经存在于您的系统上,这意味着我们的应用程序可以保持纯Web开发产品。

有了这些节省,最小的Tauri应用程序小于4MB,平均应用程序重量小于20MB。在我的测试中,最小的NodeGui应用程序重约16MB。最小的Electron应用程序很容易达到120MB。

不用说,我选择了Tauri。通过遵循Tauri集成指南,我在devDependencies中添加了@tauri-apps/cli包并初始化了项目:

<code>yarn add --dev @tauri-apps/cli
yarn tauri init</code>
登录后复制

这会在src目录(Svelte应用程序所在的位置)旁边创建一个src-tauri目录。这是所有Tauri特定文件所在的位置,这对于组织来说很好。

我以前从未构建过Tauri应用程序,但在查看其配置文档后,我能够保留大多数默认值——当然,除了package.productName和windows.title值之类的项目之外。实际上,我需要做的唯一更改是构建配置,它必须与SvelteKit对齐以进行开发和输出信息:

<code>// src-tauri/tauri.conf.json
{
  "package": {
    "version": "0.0.0",
    "productName": "Workers KV"
  },
  "build": {
    "distDir": "../build",
    "devPath": "http://localhost:3000",
    "beforeDevCommand": "yarn svelte-kit dev",
    "beforeBuildCommand": "yarn svelte-kit build"
  },
  // ...
}</code>
登录后复制

distDir与构建的生产就绪资产所在位置相关。此值从tauri.conf.json文件位置解析,因此有../前缀。

devPath是在开发过程中代理的URL。默认情况下,SvelteKit在端口3000上生成一个开发服务器(可配置)。我在第一阶段一直在浏览器中访问localhost:3000地址,所以这没有什么不同。

最后,Tauri有其自身的dev和build命令。为了避免处理多个命令或构建脚本的麻烦,Tauri提供了beforeDevCommand和beforeBuildCommand钩子,允许您在tauri命令运行之前运行任何命令。这是一个微妙但强大的便利!

SvelteKit CLI可以通过svelte-kit二进制名称访问。例如,编写yarn svelte-kit build会告诉yarn获取其本地的svelte-kit二进制文件(通过devDependency安装),然后告诉SvelteKit运行其build命令。

有了这个,我的根级package.json包含以下脚本:

<code>{
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "tauri dev",
    "build": "tauri build",
    "prebuild": "premove build",
    "preview": "svelte-kit preview",
    "tauri": "tauri"
  },
  // ...
  "devDependencies": {
    "@sveltejs/adapter-static": "1.0.0-next.9",
    "@sveltejs/kit": "1.0.0-next.109",
    "@tauri-apps/api": "1.0.0-beta.1",
    "@tauri-apps/cli": "1.0.0-beta.2",
    "premove": "3.0.1",
    "svelte": "3.38.2",
    "svelte-preprocess": "4.7.3",
    "tslib": "2.2.0",
    "typescript": "4.2.4"
  }
}</code>
登录后复制

集成后,我的生产命令仍然是yarn build,它调用tauri build来实际捆绑桌面应用程序,但只有在yarn svelte-kit build成功完成之后(通过beforeBuildCommand选项)。我的开发命令仍然是yarn dev,它并行运行tauri dev和yarn svelte-kit dev命令。开发工作流程完全在Tauri应用程序内,现在它正在代理localhost:3000,允许我仍然获得HMR开发服务器的好处。

重要提示:在我撰写本文时,Tauri仍处于测试阶段。也就是说,它感觉非常稳定且计划周全。我没有与该项目有任何关联,但看起来Tauri 1.0可能会很快进入稳定版本。我发现Tauri Discord非常活跃且乐于助人,包括来自Tauri维护者的回复!他们甚至在整个过程中回答了我的一些Rust新手问题。:)

连接到Redis

此时,是快速获胜周的星期三下午,老实说,我开始对在星期五团队演示之前完成感到紧张。为什么?因为我已经度过了本周的一半时间,即使我有一个外观良好的SPA一个可运行的桌面应用程序,它仍然什么也不做。我整个星期都在看相同的假数据

您可能认为因为我可以访问webview,我可以使用fetch()为我想要Workers KV数据发出一些经过身份验证的REST API调用,并将其全部转储到localStorage或IndexedDB表中……您完全正确!但是,这并不是我对桌面应用程序用例的想法。

将所有数据保存到某种浏览器内存储中是完全可行的,但它会将其本地保存到您的机器上。这意味着如果您的团队成员尝试执行相同的操作,每个人都必须在他们自己的机器上获取和保存所有数据。理想情况下,此Workers KV应用程序应该可以选择连接到并与外部数据库同步。这样,在团队设置中工作时,每个人都可以调整到相同的数据库缓存以节省时间——以及一些资金。当处理数百万个键时,这开始变得重要,正如前面提到的,这在使用Workers KV时并不少见。

考虑了一会儿,我决定使用Redis作为我的后端存储,因为它也是一个键值存储。这很棒,因为Redis已经将键作为一等公民对待,并提供了我想要的排序和过滤行为(也就是,我可以传递工作而不是自己实现!)。然后,当然,Redis很容易在本地或容器中安装和运行,如果有人选择走这条路,有很多托管Redis即服务提供商。

但是,我该如何连接到它呢?我的应用程序基本上是一个运行Svelte的浏览器标签,对吧?是的——但它也远不止于此。

您会看到,Electron成功的一部分原因是,是的,它保证Web应用程序在每个操作系统上都能很好地呈现,但它也带来了Node.js运行时。作为一名Web开发者,这很像在我的客户端中直接包含一个后端API。基本上,“……但它在我的机器上运行”问题消失了,因为所有用户(不知不觉地)都在运行完全相同的localhost设置。通过Node.js层,您可以与文件系统交互,在多个端口上运行服务器,或者包含一堆node_modules来——我只是在这里随口说说——连接到Redis实例。强大的东西。

我们不会失去这种超能力,因为我们正在使用Tauri!它是一样的,但略有不同。

Tauri应用程序不是包含Node.js运行时,而是使用Rust(一种低级系统语言)构建的。这就是Tauri本身与操作系统交互并“借用”其原生webviewer的方式。所有Tauri工具包都是编译的(通过Rust),这使得构建的应用程序保持小巧高效。但是,这也意味着我们,应用程序开发者,可以将任何其他板条箱(“npm模块”等效项)包含到构建的应用程序中。当然,还有一个恰如其分命名的redis板条箱,它作为一个Redis客户端驱动程序,允许Workers KV GUI连接到任何Redis实例。

在Rust中,Cargo.toml文件类似于我们的package.json文件。这是定义依赖项和元数据的地方。在Tauri设置中,它位于src-tauri/Cargo.toml,因为同样,与Tauri相关的所有内容都位于此目录中。Cargo还具有依赖项级别定义的“功能标志”的概念。(我能想到的最接近的类比是使用npm访问模块的内部结构或导入命名的子模块,尽管它仍然不完全相同,因为在Rust中,功能标志会影响包的构建方式。)

<code># src-tauri/Cargo.toml
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.0.0-beta.1", features = ["api-all", "menu"] }
redis = { version = "0.20", features = ["tokio-native-tls-comp"] }</code>
登录后复制

上面将redis板条箱定义为依赖项,并选择“tokio-native-tls-comp”功能,文档说这是TLS支持所必需的。

好的,所以我终于拥有了我需要的一切。在星期三结束之前,我必须让我的Svelte与我的Redis对话。四处查看后,我注意到所有重要的事情似乎都发生在src-tauri/main.rs文件中。我记下了#[command]宏,我知道我之前在当天的Tauri示例中见过它,所以我学习复制了示例文件中的各个部分,查看根据Rust编译器哪些错误出现和消失。

最终,Tauri应用程序能够再次运行,我了解到#[command]宏以某种方式包装底层函数,以便它可以接收“上下文”值(如果您选择使用它们)并接收预解析的参数值。此外,作为一种语言,Rust会进行大量类型转换。例如:

<code>use tauri::{command};

#[command]
fn greet(name: String, age: u8) {
  println!("Hello {}, {} year-old human!", name, age);
}</code>
登录后复制

这会创建一个greet命令,当运行时,期望两个参数:name和age。定义时,name值是一个字符串值,age是u8数据类型——也就是一个整数。但是,如果两者都缺少,Tauri会抛出错误,因为命令定义没有说明任何内容可以是可选的。

为了实际将Tauri命令连接到应用程序,它必须定义为tauri::Builder组合的一部分,位于main函数内。

<code>use tauri::{command};

#[command]
fn greet(name: String, age: u8) {
  println!("Hello {}, {} year-old human!", name, age);
}

fn main() {
  // start composing a new Builder chain
  tauri::Builder::default()
    // assign our generated "handler" to the chain
    .invoke_handler(
      // piece together application logic
      tauri::generate_handler![
        greet, // attach the command
      ]
    )
    // start/initialize the application
    .run(
      // put it all together
      tauri::generate_context!()
    )
    // print <message> if error while running
    .expect("error while running tauri application");
}</message></code>
登录后复制

Tauri应用程序编译并知道它拥有一个“greet”命令。它还在控制webview(我们已经讨论过),但这样做时,它充当前端(webview内容)和后端之间的桥梁,后端由Tauri API和我们编写的任何其他代码(如greet命令)组成。Tauri允许我们在该桥梁之间发送消息,以便这两个世界可以相互通信。

前端可以通过导入来自任何(已包含的)@tauri-apps包的功能,或者通过依赖于window.TAURI全局变量(可用于整个客户端应用程序)来访问此“桥梁”。具体来说,我们对invoke命令感兴趣,该命令接受命令名称和一组参数。如果有任何参数,则必须将其定义为一个对象,其中键与我们的Rust函数期望的参数名称匹配。

在Svelte层中,这意味着我们可以执行以下操作来调用在Rust层中定义的greet命令:

<code>

  function onclick() {
    __TAURI__.invoke('greet', {
      name: 'Alice',
      age: 32
    });
  }
Click Me</code>
登录后复制

单击此按钮时,我们的终端窗口(tauri dev命令运行的位置)将打印:

<code>Hello Alice, 32 year-old human!</code>
登录后复制

同样,这发生是因为println!函数(实际上是Rust的console.log),greet命令使用了该函数。它出现在终端的控制台窗口中——而不是浏览器控制台中——因为此代码仍在Rust/系统端运行。

也可以从Tauri命令向客户端发送一些内容,所以让我们快速更改greet:

<code>use tauri::{command};

#[command]
fn greet(name: String, age: u8) {
  // implicit return, because no semicolon!
  format!("Hello {}, {} year-old human!", name, age)
}

// OR

#[command]
fn greet(name: String, age: u8) {
  // explicit `return` statement, must have semicolon
  return format!("Hello {}, {} year-old human!", name, age);
}</code>
登录后复制

意识到我将多次调用invoke,并且有点懒惰,我提取了一个轻量级的客户端助手来整合这些内容:

<code>// @types/global.d.ts
/// <reference types="@sveltejs/kit"></reference>

type Dict<t> = Record<string t="">;

declare const __TAURI__: {
  invoke: typeof import('@tauri-apps/api/tauri').invoke;
}

// src/lib/tauri.ts
export function dispatch(command: string, args: Dict<string>) {
  return __TAURI__.invoke(command, args);
}</string></string></t></code>
登录后复制

然后将之前的Greeter.svelte重构为:

<code>

  import { dispatch } from '$lib/tauri';

  async function onclick() {
    let output = await dispatch('greet', {
      name: 'Alice',
      age: 32
    });
    console.log('~>', output);
    //=> "~> Hello Alice, 32 year-old human!"
  }
Click Me</code>
登录后复制

太棒了!所以现在是星期四,我还没有编写任何Redis代码,但至少我知道如何将应用程序的大脑的两半连接在一起。是时候梳理客户端代码并替换事件处理程序中的所有TODO,并将它们连接到实际内容了。

我将在这里省略细节,因为从这里开始它非常特定于应用程序——并且主要是关于Rust编译器给我带来打击的故事。此外,探索细节正是项目开源的原因!

在高层次上,一旦使用给定的详细信息建立了Redis连接,就可以在/viewer路由中访问SYNC按钮。单击此按钮时(并且只有那时——因为成本),将调用一个JavaScript函数,该函数负责连接到Cloudflare REST API并为每个键调度“redis_set”命令。此redis_set命令在Rust层中定义——所有基于Redis的命令也是如此——并且负责实际将键值对写入Redis。

从Redis中读取数据是一个非常相似的过程,只是反过来了。例如,当/viewer启动时,所有键都应该列出并准备就绪。在Svelte术语中,这意味着我需要在/viewer组件安装时调度Tauri命令。这几乎逐字地发生在这里。此外,单击侧边栏中的键名称将显示有关该键的更多“详细信息”,包括其到期时间(如果有)、其元数据(如果有)及其实际值(如果已知)。为了优化成本和网络负载,我们决定只应按需获取键的值。这引入了REFRESH按钮,单击该按钮时,它会再次与REST API交互,然后调度一个命令,以便Redis客户端可以单独更新该键。

我并不是想仓促结束,但是一旦你看到JavaScript和Rust代码之间的一个成功的交互,你就看到了所有交互!我星期四和星期五上午的其余时间只是定义新的请求-回复对,这感觉很像给自己发送PING和PONG消息。

结论

对我来说——我想对许多其他JavaScript开发者来说也是如此——本周的挑战是学习Rust。我相信你以前听过这个,你以后也一定会再听到。所有权规则、借用检查以及单个字符语法标记的含义(顺便说一句,这些标记不容易搜索)只是我遇到的几个障碍。再次感谢Tauri Discord的帮助和善意!

这也意味着使用Tauri并非一项挑战——而是一种巨大的解脱。我肯定计划将来再次使用Tauri,尤其是在我知道如果我想使用,我可以只使用webviewer的情况下。深入研究和/或添加Rust部分是“额外材料”,只有在我的应用程序需要时才需要。

对于那些想知道的人,因为我找不到另一个地方提及它:在macOS上,Workers KV GUI应用程序的重量不到13 MB。我对这个结果非常兴奋

当然,SvelteKit也使这个时间表成为可能。它不仅节省了我半天配置工具带的时间,而且即时的HMR开发服务器也可能节省了我几个小时手动刷新浏览器——然后是Tauri查看器的时间。

如果您已经看到这里——这令人印象深刻!非常感谢您的时间和关注。提醒一下,该项目可在GitHub上获得,最新的预编译二进制文件始终可通过其发布页面获得。

以上是我如何与Svelte,Redis和Rust构建跨平台桌面应用程序的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板