首頁 > web前端 > css教學 > 我如何與Svelte,Redis和Rust構建跨平台桌面應用程序

我如何與Svelte,Redis和Rust構建跨平台桌面應用程序

Joseph Gordon-Levitt
發布: 2025-03-21 11:14:11
原創
901 人瀏覽過

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
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板