這是所有開發人員都問過自己的問題,尤其是來自webdev 世界的開發人員:「如果我可以運行幾乎任何可以在瀏覽器中呈現的內容並滿足我想要的幾乎任何目的,誰需要下載我們的應用程式並在他們的電腦上運行它?但是,除了我們正在做的工作的明顯要求(對於我們自己或公司來說,例如能夠利用所有作業系統功能、更好的性能、離線功能、改進的安全性和整合等),還有作為開發人員,我們從接觸程式設計的新方面中獲得的經驗將永遠豐富我們。
如果你像我一樣對Golang 充滿熱情,並且你已經用這種語言開發過後端,但你也使用HTML、CSS 和JavaScript(或其一些框架)做過前端,那麼這篇文章適合你,因為不需要要學習新技術,您完全有能力創建桌面應用程式。
您可能已經知道電子或Tauri。兩者都使用 Web 技術作為前端;第一個在其後端使用 JavaScript(或更確切地說,NodeJs),第二個使用 Rust。但兩者或多或少都有明顯的缺點。 Electron 應用程式具有非常大的二進位檔案(因為它們打包了整個 Chromium 瀏覽器)並消耗大量記憶體。 Tauri 應用程式改進了這些方面(除其他外,因為它們使用WebView2 [Windows]/WebKit [macOS 和Linux] 而不是Chromium),但二進位檔案仍然相對較大,而且它們的編譯時間是…是Rust 的編譯時間嗎? (更不用說他們的學習曲線,雖然我喜歡 Rust,但我是認真的?)。
透過使用 Wails,您可以利用我剛才描述的 Web 技術充分利用桌面應用程式開發的所有領域,以及使用 Go 帶來的所有優勢:
是的,如果我想使用 Go 建立桌面應用程序,還有其他可能性(原生或非原生)。我會提到 Fyne 和 go-gtk。 Fyne 是一個GUI 框架,允許輕鬆創建本機應用程序,儘管它們可能具有優雅的設計,但該框架的功能有些有限,或者需要開發人員付出巨大的努力才能實現與其他工具和/或語言可以讓您輕鬆完成。我可以對go-gtk 說同樣的話,它是GTK 的Go 綁定:是的,確實,您將獲得本機應用程序,其限制將取決於您自己的能力,但是進入GTK 庫就像去叢林探險一樣? …
首先,對於那些想知道Nu-i uita 意義的人:在羅馬尼亞語中,它的大致意思是「不要忘記他們」。我以為是原來的名字...
您可以在此 GitHub 儲存庫中查看應用程式的完整程式碼。如果您想立即嘗試,可以從此處下載可執行檔(適用於 Windows 和 Linux)。
我將簡要描述該應用程式的工作原理:使用者首次登錄,登入視窗要求他輸入主密碼。這是使用密碼本身作為加密金鑰加密保存的。此登入視窗通往另一個介面,使用者可以在其中列出相應網站的保存密碼和使用的使用者名稱(您也可以在此列表中按使用者名稱或網站進行搜尋)。您可以單擊清單中的每個項目並查看其詳細資訊、將其複製到剪貼簿、編輯或刪除。此外,當新增項目時,它會使用主密碼作為金鑰來加密您的密碼。在設定視窗中,您可以選擇所需的語言(目前只有英文和西班牙文)、刪除所有儲存的資料、匯出或從備份檔案匯入。匯入資料時,系統將要求使用者提供執行匯出時使用的主密碼,匯入的資料現在將使用目前主密碼儲存和加密。隨後,每當使用者重新登入應用程式時,系統都會提示他或她輸入目前的主密碼。
我不會詳細討論使用 Wails 所需的要求,因為它在其出色的文件中得到了很好的解釋。無論如何,安裝其強大的CLI(去安裝github.com/wailsapp/wails/v2/cmd/wails@latest)是至關重要的,它允許您為應用程式生成腳手架,在編輯程式碼時進行熱重載,並建置可執行檔(包括交叉編譯)。
Wails CLI 允許您使用各種前端框架生成項目,但出於某種原因,Wails 的創建者似乎更喜歡Svelte...因為這是他們提到的第一個選項。當您使用指令 wails init -n myproject -t svelte-ts 時,您會產生一個帶有 Svelte3 和 TypeScript.
的項目如果出於某種原因您更喜歡使用Svelte5 及其新的runes 系統 功能,我創建了一個bash 腳本,它可以自動生成項目苗條5。在這種情況下,您還需要安裝 Wails CLI。
我上面提到的應用程式的功能構成了任何todoapp的要求(這始終是學習程式設計新知識的好方法),但這裡我們添加一個加上使其更有用的功能(例如,在後端使用對稱加密,在前端使用國際化)比簡單的待辦事項應用程式更有啟發性。
好了,介紹已經夠多了,那我們開始進入正題吧? .
如果您選擇透過CLI 執行命令wails init -n myproject -t svelte-ts (或使用我建立的bash 腳本,並且我之前已經告訴您)來建立具有CLI 的Svelte Typescript 的Wails 項目,則會產生使用Svelte5 進行Wails 專案)您將擁有與此非常相似的目錄結構:
. ├── app.go ├── build │ ├── appicon.png │ ├── darwin │ │ ├── Info.dev.plist │ │ └── Info.plist │ ├── README.md │ └── windows │ ├── icon.ico │ ├── info.json │ ├── installer │ │ ├── project.nsi │ │ └── wails_tools.nsh │ └── wails.exe.manifest ├── frontend │ ├── index.html │ ├── package.json │ ├── package.json.md5 │ ├── package-lock.json │ ├── postcss.config.js │ ├── README.md │ ├── src │ │ ├── App.svelte │ │ ├── assets │ │ │ ├── fonts │ │ │ │ ├── nunito-v16-latin-regular.woff2 │ │ │ │ └── OFL.txt │ │ │ └── images │ │ │ └── logo-universal.png │ │ ├── lib │ │ │ ├── BackBtn.svelte │ │ │ ├── BottomActions.svelte │ │ │ ├── EditActions.svelte │ │ │ ├── EntriesList.svelte │ │ │ ├── Language.svelte │ │ │ ├── popups │ │ │ │ ├── alert-icons.ts │ │ │ │ └── popups.ts │ │ │ ├── ShowPasswordBtn.svelte │ │ │ └── TopActions.svelte │ │ ├── locales │ │ │ ├── en.json │ │ │ └── es.json │ │ ├── main.ts │ │ ├── pages │ │ │ ├── About.svelte │ │ │ ├── AddPassword.svelte │ │ │ ├── Details.svelte │ │ │ ├── EditPassword.svelte │ │ │ ├── Home.svelte │ │ │ ├── Login.svelte │ │ │ └── Settings.svelte │ │ ├── style.css │ │ └── vite-env.d.ts │ ├── svelte.config.js │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite.config.ts │ └── wailsjs │ ├── go │ │ ├── main │ │ │ ├── App.d.ts │ │ │ └── App.js │ │ └── models.ts │ └── runtime │ ├── package.json │ ├── runtime.d.ts │ └── runtime.js ├── go.mod ├── go.sum ├── internal │ ├── db │ │ └── db.go │ └── models │ ├── crypto.go │ ├── master_password.go │ └── password_entry.go ├── LICENSE ├── main.go ├── Makefile ├── README.md ├── scripts └── wails.json
您剛剛看到的是完成的應用程式結構。與 Wails CLI 生成的唯一區別是,使用它,您將獲得具有 Svelte3 TypeScript 前端的 Wails 應用程式的腳手架,而使用我的腳本,除了 Svelte5 之外,Tailwinss Daisyui
如Wails 文件所述:「Wails 應用程式是一個標準的Go 應用程序,具有
webkit 前端。應用程式的Go 部分由應用程式程式碼和運行時庫組成它提供了許多有用的操作,例如控制應用程式視窗。視窗。和一個前端組成,前端的資產由Webkit 視窗(在以Windows 作業系統為例,Webview2),類似於服務/渲染前端資產的Web 伺服器/瀏覽器的本質。 在我們的具體案例中,我們希望應用程式能夠在 Windows 和 Linux 上運行,主應用程式由以下程式碼組成:
我們需要做的第一件事是實例化一個結構體(使用
NewApp
/* main.go */ package main import ( "embed" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options/assetserver" "github.com/wailsapp/wails/v2/pkg/options/linux" ) //go:embed all:frontend/dist var assets embed.FS //go:embed build/appicon.png var icon []byte func main() { // Create an instance of the app structure app := NewApp() // Create application with options err := wails.Run(&options.App{ // Title: "Nu-i uita • minimalist password manager", Width: 450, Height: 300, DisableResize: true, AssetServer: &assetserver.Options{ Assets: assets, }, BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, OnStartup: app.startup, OnBeforeClose: app.beforeClose, Bind: []interface{}{ app, }, // Linux platform specific options Linux: &linux.Options{ Icon: icon, // WindowIsTranslucent: true, WebviewGpuPolicy: linux.WebviewGpuPolicyNever, // ProgramName: "wails", }, }) if err != nil { println("Error:", err.Error()) } }Context
的欄位。然後,wails 的 Run 方法是啟動應用程式的方法。我們需要向它傳遞一系列選項。這些強制性選項之一是資產。一旦 Wails 編譯了前端,它就會在「frontend/dist」資料夾中產生它。使用 //go:embed all:frontend/dist 指令(Go 的神奇功能),我們可以將整個前端嵌入到最終的可執行檔中。對於Linux,如果我們想嵌入應用程式圖標,我們還必須使用//go:embed指令。 其餘選項我不會詳細介紹,您可以在文件中查看。我只想說兩件與選項相關的事情。第一個是出現在應用程式標題列中的標題可以在這裡設定為選項,但是在我們的應用程式中,使用者可以選擇他們想要的語言,當我們收到使用者可能進行的語言變更事件時。我們稍後會看到這個。 第二個重要的選項相關問題是綁定選項。文件很好地解釋了它的意義:「Bind 選項是Wails 應用程式中最重要的選項之一。它指定向前端公開哪些結構方法。想想傳統Web 中的controllers 這樣的結構應用。所述結構體的那些公共方法被轉換為 JavaScript 函數,並透過 Wails 執行的編譯傳回一個 Promise。 後端和前端之間的另一種重要通訊形式(我們在此應用程式中有效使用)是事件。 Wails 提供了一個事件系統,其中事件可以由 Go 或 JavaScript 發出或接收。或者,數據可以與事件一起傳遞。研究我們在應用程式中使用事件的方式將引導我們分析 struct App: 我們首先看到的是struct App,它有一個字段,用於存儲Wails 所需的Go Context,以及一個指向struct Db 的指針(與數據庫相關,我們將看到) 。另外 2 個屬性是我們配置的字串,以便本機對話方塊(由後端管理)根據使用者選擇的語言顯示標題。充當 App 的建構子 (NewApp) 的函式只是建立指向資料庫結構的指標。 接下來我們看到Wails需要的選項需要2個方法:startup和beforeClose,我們將分別傳遞給OnStartup和 OnBeforeClose 選項。透過這樣做,他們將自動收到 Go Context。 beforeClose 只是在關閉應用程式時關閉與資料庫的連線。但新創公司做得更多。首先,它設定在其對應欄位中接收到的上下文。其次,它在後端註冊一系列事件監聽器,我們需要觸發一系列操作。 在Wails中,所有事件監聽器都有這個簽章: 也就是說,它接收上下文(我們在App 的ctx 欄位中保存的上下文)、事件名稱(我們將在前端建立)以及將執行的回調我們需要的操作,並且它又可以接收類型為any 或空介面(interface{}) 的可選參數,這是相同的,因此我們必須進行類型斷言。 我們聲明的一些偵聽器在其中聲明了嵌套事件發射器,這些事件發射器將在前端接收並在那裡觸發某些操作。他的簽名如下圖: 我不會詳細介紹這些偵聽器的作用,不僅是為了簡潔,還因為 Go 的表達能力足夠強,您只需閱讀程式碼就可以知道它們的作用。我只解釋其中的幾個。 “change_titles”偵聽器期望接收具有該名稱的事件。當使用者變更介面語言,將應用程式視窗標題列的標題變更為偵聽器本身接收到的值時,會觸發此事件。我們使用Wails運行時包來實現這一點。該事件還接收「選擇目錄」和「選擇檔案」對話方塊的標題,這些標題儲存在 App 結構的單獨屬性中,以便在需要時使用。如您所見,我們需要此事件,因為這些「本機」操作需要從後端執行。 特別值得一提的是聽眾“import_data”和“password”,可以說,連結。第一個(“import_data”)在收到時會觸發使用 runtime.OpenFileDialog 方法開啟對話方塊。正如我們所看到的,該方法在其選項中接收要顯示的標題,該標題儲存在 App 結構的 selectedFile 欄位中,正如我們已經解釋的那樣。如果使用者選擇一個文件,因此 fileLocation 變數不為空,則會發出事件(稱為「enter_password」),該事件在前端接收,以顯示一個彈出窗口,要求使用者輸入他所設定的主密碼。他在出口時使用。當使用者這樣做時,前端會發出一個事件(「密碼」),我們在後端偵聽器中接收該事件。接收到的資料(主密碼)和備份檔案的路徑由代表資料庫(ImportDump)的 Db 結構的方法使用。根據所述方法的執行結果,會發出一個新事件(“imported_data”),該事件將在前端觸發一個彈出窗口,顯示導入成功或失敗的結果。 如我們所見,Wails 事件是後端和前端之間強大且有效的溝通方式。 App 結構體的其餘方法只不過是後端向前端公開的方法,正如我們已經解釋過的,這些方法基本上是對資料庫的CRUD 操作,因此,我們在下面進行解釋。 對於這個後端部分,我受到vikkio88 的這篇文章(在DEV.to 上)和他的密碼管理器存儲庫的啟發(做了一些修改),他首先使用C#/Avalonia 創建了該密碼管理器,然後改編為使用Go/費恩(麝香-ig)。 後端的「最低等級」部分是與密碼加密相關的部分。最重要的是這 3 個功能: 我不會詳細介紹 Go 中使用 AES 進行對稱加密的細節。您需要了解的一切都在 DEV.to 的這篇文章中得到了很好的解釋。 AES 是一種分組密碼演算法,它採用固定大小的金鑰和固定大小的明文,並傳回固定大小的密文。由於 AES 的區塊大小設定為 16 字節,因此明文長度必須至少為 16 位元組。這給我們帶來了一個問題,因為我們希望能夠加密/解密任意大小的資料。為了解決最小明文區塊大小的問題,存在分組密碼模式。這裡我使用 GCM 模式,因為它是最廣泛採用的對稱分組密碼模式之一。 GCM 需要一個 IV(初始化向量 [陣列]),它必須始終隨機產生(用於此類陣列的術語是隨機數)。 基本上,加密 函數採用明文進行加密,並使用始終為 32 位元組長的金鑰,並使用該金鑰產生 AES 加密器。使用該加密器,我們產生一個 gcm 對象,用於建立 12 位元組初始化向量 (nonce)。 gcm 物件的 Seal 方法允許我們使用向量 nonce 「連接」並加密明文(作為位元組切片),最後將其結果轉換回字串。 decrypt函數的作用相反:它的第一部分等於加密,那麼由於我們知道密文實際上是nonce密文,所以我們可以將密文拆分為它的兩個組成部分。 gcm 物件的NonceSize 方法總是產生「12」(這是隨機數的長度),因此我們在解密的同時分割位元組切片使用gcm物件的Open 方法。最後,我們將結果轉換為字串。 keyfy 函數確保我們擁有一個 32 位元組的金鑰(透過用「0」填充以達到該長度)。我們將看到,在前端,我們確保使用者不會輸入超過 1 個位元組的字元(非 ASCII 字元),因此該函數的結果始終為 32 個位元組長。 此文件中的其餘程式碼本質上負責將上述函數的輸入/輸出編碼/解碼為 base64。 為了儲存所有應用程式數據,我們使用cloverDB。它是一個輕量級、嵌入式的以文件為導向的NoSQL資料庫,類似於MongoDB。這個資料庫的特點之一是,當儲存記錄時,會為它們分配一個ID(預設情況下,該欄位指定為_id,有點像MongoDB 中的情況),這是一個uuid 字符串( v4)。因此,如果我們想要按條目順序對記錄進行排序,則必須在儲存它們時為其分配時間戳。 基於這些事實,我們將建立我們的模型及其相關方法(master_password.go 和password_entry.go): MasterPassword 有一個私有欄位(清除),不會在資料庫中儲存/檢索(因此沒有三葉草標籤),即它只存在於記憶體中,不儲存在磁碟上。此屬性是未加密的主密碼本身,將用作密碼條目的加密金鑰。該值由 MasterPassword 物件上的 setter 存儲,或設定(由 callback)作為同名包的 struct Db 中的非導出(私有)字段(db.go)。對於密碼輸入,我們使用2 個結構體,一個沒有加密的密碼,另一個結構體的密碼已經加密,這是實際儲存在資料庫中的物件(類似於DTO ,數據傳輸物件)。這兩個結構體的加密/解密方法內部都使用 Crypto 對象,該對象具有帶有加密金鑰的屬性(這是轉換為 32 位元組長切片的主密碼): 主密碼有 3 種在資料保存/復原中發揮重要作用的方法: GetCrypto 允許您取得 Crypto 物件的目前實例,以便 db.go 套件可以加密/解密密碼項目。 SetClear 是我們之前提到的setter,Check 是驗證使用者輸入的主密碼是否正確的函數;正如我們所看到的,除了密碼之外面,它還需要一個callback 作為參數,根據情況,它會是前面提到的setter (當我們從備份檔案匯入資料時) 或 db.go 套件的 SetMasterPassword 方法,用於在使用者登入時設定 Db 結構體的私有欄位中的值。 我不打算詳細解釋db.go 套件的所有方法,因為它的大部分程式碼與cloverDB 的使用方式相關,你可以在它的文件中查看,雖然我已經提到了一些將在這裡使用的重要內容。 首先我們有一個結構體,它將儲存一個指向 cloverDB 實例的指標。它也儲存指向 MasterPassword 結構的「完整」實例的指標。這裡的「完整」意味著它儲存加密的主密碼(意味著它存在於資料庫中,因此是當前的主密碼)和未加密的主密碼,該密碼將用於加密密碼條目。接下來我們有 setupCollections、NewDb 和 Close,它們是在應用程式啟動和關閉時設定資料庫的函數和方法。 cloverDB在使用Open方法實例化時不會自動建立儲存檔案/目錄,而是我們必須手動建立。最後,GetLanguageCode 和 SaveLanguageCode 是檢索/保存使用者選擇的應用程式語言的方法。由於所選的語言程式碼是一個小字串(“en”或“es”),為了簡單起見,我們不使用結構來儲存它:例如,從集合中檢索語言程式碼(cloverDB 使用“文件”)和“集合”,類似於MongoDB),我們只需傳遞儲存它的鍵(“代碼”)並進行類型斷言。 當使用者第一次登入時,主密碼會儲存在資料庫中:該值(未加密)設定在MasterPassword物件的明文字段中,該物件也儲存了已加密的密碼密碼,並保存在Db 結構體中: 啟動應用程式時會執行兩次主密碼復原: 在這兩種情況下,都會呼叫 RecoverMasterPassword 方法,只有當儲存了主密碼時,該方法才會在 Db 結構體的 cachedMp 欄位中設定實例: 接下來,有 2 段小但重要的程式碼: 除了任何todoapp典型的CRUD操作之外,我們還有其他功能或方法可以評論: loadPasswordEntryDTO 是一個輔助函數,它從從 cloverDB 取得的單一文件建立 PasswordEntryDTO 物件。 loadManyPasswordEntryDTO 執行相同的操作,但從 cloverDB 文件的切片中產生 PasswordEntryDTO 的切片。最後,loadManyPasswordEntry 的作用與 loadManyPasswordEntryDTO 相同,但也解密從 getCryptoInstance 產生的 Crypto
GenerateDump 使用 DbDump 結構,它將作為保存在備份檔案中的物件。它以使用者選擇的目錄路徑、日期格式和 ad hoc 副檔名作為名稱。然後我們建立一個 DbDump 實例,其中包含加密的主密碼、DTO 切片(其對應的密碼也已加密)以及使用者在資料庫中保存的語言代碼。這個物件最終被 Golang gob 套件以二進位編碼到我們建立的檔案中,並將檔案名稱傳回 UI,通知使用者建立成功。
MasterPasword 實例中取得 Crypto 物件的實例,並在以下循環中執行 2 件事:
Svelte 製成的。
.
├── app.go
├── build
│ ├── appicon.png
│ ├── darwin
│ │ ├── Info.dev.plist
│ │ └── Info.plist
│ ├── README.md
│ └── windows
│ ├── icon.ico
│ ├── info.json
│ ├── installer
│ │ ├── project.nsi
│ │ └── wails_tools.nsh
│ └── wails.exe.manifest
├── frontend
│ ├── index.html
│ ├── package.json
│ ├── package.json.md5
│ ├── package-lock.json
│ ├── postcss.config.js
│ ├── README.md
│ ├── src
│ │ ├── App.svelte
│ │ ├── assets
│ │ │ ├── fonts
│ │ │ │ ├── nunito-v16-latin-regular.woff2
│ │ │ │ └── OFL.txt
│ │ │ └── images
│ │ │ └── logo-universal.png
│ │ ├── lib
│ │ │ ├── BackBtn.svelte
│ │ │ ├── BottomActions.svelte
│ │ │ ├── EditActions.svelte
│ │ │ ├── EntriesList.svelte
│ │ │ ├── Language.svelte
│ │ │ ├── popups
│ │ │ │ ├── alert-icons.ts
│ │ │ │ └── popups.ts
│ │ │ ├── ShowPasswordBtn.svelte
│ │ │ └── TopActions.svelte
│ │ ├── locales
│ │ │ ├── en.json
│ │ │ └── es.json
│ │ ├── main.ts
│ │ ├── pages
│ │ │ ├── About.svelte
│ │ │ ├── AddPassword.svelte
│ │ │ ├── Details.svelte
│ │ │ ├── EditPassword.svelte
│ │ │ ├── Home.svelte
│ │ │ ├── Login.svelte
│ │ │ └── Settings.svelte
│ │ ├── style.css
│ │ └── vite-env.d.ts
│ ├── svelte.config.js
│ ├── tailwind.config.js
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ ├── vite.config.ts
│ └── wailsjs
│ ├── go
│ │ ├── main
│ │ │ ├── App.d.ts
│ │ │ └── App.js
│ │ └── models.ts
│ └── runtime
│ ├── package.json
│ ├── runtime.d.ts
│ └── runtime.js
├── go.mod
├── go.sum
├── internal
│ ├── db
│ │ └── db.go
│ └── models
│ ├── crypto.go
│ ├── master_password.go
│ └── password_entry.go
├── LICENSE
├── main.go
├── Makefile
├── README.md
├── scripts
└── wails.json
.
├── app.go
├── build
│ ├── appicon.png
│ ├── darwin
│ │ ├── Info.dev.plist
│ │ └── Info.plist
│ ├── README.md
│ └── windows
│ ├── icon.ico
│ ├── info.json
│ ├── installer
│ │ ├── project.nsi
│ │ └── wails_tools.nsh
│ └── wails.exe.manifest
├── frontend
│ ├── index.html
│ ├── package.json
│ ├── package.json.md5
│ ├── package-lock.json
│ ├── postcss.config.js
│ ├── README.md
│ ├── src
│ │ ├── App.svelte
│ │ ├── assets
│ │ │ ├── fonts
│ │ │ │ ├── nunito-v16-latin-regular.woff2
│ │ │ │ └── OFL.txt
│ │ │ └── images
│ │ │ └── logo-universal.png
│ │ ├── lib
│ │ │ ├── BackBtn.svelte
│ │ │ ├── BottomActions.svelte
│ │ │ ├── EditActions.svelte
│ │ │ ├── EntriesList.svelte
│ │ │ ├── Language.svelte
│ │ │ ├── popups
│ │ │ │ ├── alert-icons.ts
│ │ │ │ └── popups.ts
│ │ │ ├── ShowPasswordBtn.svelte
│ │ │ └── TopActions.svelte
│ │ ├── locales
│ │ │ ├── en.json
│ │ │ └── es.json
│ │ ├── main.ts
│ │ ├── pages
│ │ │ ├── About.svelte
│ │ │ ├── AddPassword.svelte
│ │ │ ├── Details.svelte
│ │ │ ├── EditPassword.svelte
│ │ │ ├── Home.svelte
│ │ │ ├── Login.svelte
│ │ │ └── Settings.svelte
│ │ ├── style.css
│ │ └── vite-env.d.ts
│ ├── svelte.config.js
│ ├── tailwind.config.js
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ ├── vite.config.ts
│ └── wailsjs
│ ├── go
│ │ ├── main
│ │ │ ├── App.d.ts
│ │ │ └── App.js
│ │ └── models.ts
│ └── runtime
│ ├── package.json
│ ├── runtime.d.ts
│ └── runtime.js
├── go.mod
├── go.sum
├── internal
│ ├── db
│ │ └── db.go
│ └── models
│ ├── crypto.go
│ ├── master_password.go
│ └── password_entry.go
├── LICENSE
├── main.go
├── Makefile
├── README.md
├── scripts
└── wails.json
/* main.go */
package main
import (
"embed"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
"github.com/wailsapp/wails/v2/pkg/options/linux"
)
//go:embed all:frontend/dist
var assets embed.FS
//go:embed build/appicon.png
var icon []byte
func main() {
// Create an instance of the app structure
app := NewApp()
// Create application with options
err := wails.Run(&options.App{
// Title: "Nu-i uita • minimalist password manager",
Width: 450,
Height: 300,
DisableResize: true,
AssetServer: &assetserver.Options{
Assets: assets,
},
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
OnStartup: app.startup,
OnBeforeClose: app.beforeClose,
Bind: []interface{}{
app,
},
// Linux platform specific options
Linux: &linux.Options{
Icon: icon,
// WindowIsTranslucent: true,
WebviewGpuPolicy: linux.WebviewGpuPolicyNever,
// ProgramName: "wails",
},
})
if err != nil {
println("Error:", err.Error())
}
}
V - 後端:應用程式的管道
.
├── app.go
├── build
│ ├── appicon.png
│ ├── darwin
│ │ ├── Info.dev.plist
│ │ └── Info.plist
│ ├── README.md
│ └── windows
│ ├── icon.ico
│ ├── info.json
│ ├── installer
│ │ ├── project.nsi
│ │ └── wails_tools.nsh
│ └── wails.exe.manifest
├── frontend
│ ├── index.html
│ ├── package.json
│ ├── package.json.md5
│ ├── package-lock.json
│ ├── postcss.config.js
│ ├── README.md
│ ├── src
│ │ ├── App.svelte
│ │ ├── assets
│ │ │ ├── fonts
│ │ │ │ ├── nunito-v16-latin-regular.woff2
│ │ │ │ └── OFL.txt
│ │ │ └── images
│ │ │ └── logo-universal.png
│ │ ├── lib
│ │ │ ├── BackBtn.svelte
│ │ │ ├── BottomActions.svelte
│ │ │ ├── EditActions.svelte
│ │ │ ├── EntriesList.svelte
│ │ │ ├── Language.svelte
│ │ │ ├── popups
│ │ │ │ ├── alert-icons.ts
│ │ │ │ └── popups.ts
│ │ │ ├── ShowPasswordBtn.svelte
│ │ │ └── TopActions.svelte
│ │ ├── locales
│ │ │ ├── en.json
│ │ │ └── es.json
│ │ ├── main.ts
│ │ ├── pages
│ │ │ ├── About.svelte
│ │ │ ├── AddPassword.svelte
│ │ │ ├── Details.svelte
│ │ │ ├── EditPassword.svelte
│ │ │ ├── Home.svelte
│ │ │ ├── Login.svelte
│ │ │ └── Settings.svelte
│ │ ├── style.css
│ │ └── vite-env.d.ts
│ ├── svelte.config.js
│ ├── tailwind.config.js
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ ├── vite.config.ts
│ └── wailsjs
│ ├── go
│ │ ├── main
│ │ │ ├── App.d.ts
│ │ │ └── App.js
│ │ └── models.ts
│ └── runtime
│ ├── package.json
│ ├── runtime.d.ts
│ └── runtime.js
├── go.mod
├── go.sum
├── internal
│ ├── db
│ │ └── db.go
│ └── models
│ ├── crypto.go
│ ├── master_password.go
│ └── password_entry.go
├── LICENSE
├── main.go
├── Makefile
├── README.md
├── scripts
└── wails.json
.
├── app.go
├── build
│ ├── appicon.png
│ ├── darwin
│ │ ├── Info.dev.plist
│ │ └── Info.plist
│ ├── README.md
│ └── windows
│ ├── icon.ico
│ ├── info.json
│ ├── installer
│ │ ├── project.nsi
│ │ └── wails_tools.nsh
│ └── wails.exe.manifest
├── frontend
│ ├── index.html
│ ├── package.json
│ ├── package.json.md5
│ ├── package-lock.json
│ ├── postcss.config.js
│ ├── README.md
│ ├── src
│ │ ├── App.svelte
│ │ ├── assets
│ │ │ ├── fonts
│ │ │ │ ├── nunito-v16-latin-regular.woff2
│ │ │ │ └── OFL.txt
│ │ │ └── images
│ │ │ └── logo-universal.png
│ │ ├── lib
│ │ │ ├── BackBtn.svelte
│ │ │ ├── BottomActions.svelte
│ │ │ ├── EditActions.svelte
│ │ │ ├── EntriesList.svelte
│ │ │ ├── Language.svelte
│ │ │ ├── popups
│ │ │ │ ├── alert-icons.ts
│ │ │ │ └── popups.ts
│ │ │ ├── ShowPasswordBtn.svelte
│ │ │ └── TopActions.svelte
│ │ ├── locales
│ │ │ ├── en.json
│ │ │ └── es.json
│ │ ├── main.ts
│ │ ├── pages
│ │ │ ├── About.svelte
│ │ │ ├── AddPassword.svelte
│ │ │ ├── Details.svelte
│ │ │ ├── EditPassword.svelte
│ │ │ ├── Home.svelte
│ │ │ ├── Login.svelte
│ │ │ └── Settings.svelte
│ │ ├── style.css
│ │ └── vite-env.d.ts
│ ├── svelte.config.js
│ ├── tailwind.config.js
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ ├── vite.config.ts
│ └── wailsjs
│ ├── go
│ │ ├── main
│ │ │ ├── App.d.ts
│ │ │ └── App.js
│ │ └── models.ts
│ └── runtime
│ ├── package.json
│ ├── runtime.d.ts
│ └── runtime.js
├── go.mod
├── go.sum
├── internal
│ ├── db
│ │ └── db.go
│ └── models
│ ├── crypto.go
│ ├── master_password.go
│ └── password_entry.go
├── LICENSE
├── main.go
├── Makefile
├── README.md
├── scripts
└── wails.json
/* main.go */
package main
import (
"embed"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
"github.com/wailsapp/wails/v2/pkg/options/linux"
)
//go:embed all:frontend/dist
var assets embed.FS
//go:embed build/appicon.png
var icon []byte
func main() {
// Create an instance of the app structure
app := NewApp()
// Create application with options
err := wails.Run(&options.App{
// Title: "Nu-i uita • minimalist password manager",
Width: 450,
Height: 300,
DisableResize: true,
AssetServer: &assetserver.Options{
Assets: assets,
},
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
OnStartup: app.startup,
OnBeforeClose: app.beforeClose,
Bind: []interface{}{
app,
},
// Linux platform specific options
Linux: &linux.Options{
Icon: icon,
// WindowIsTranslucent: true,
WebviewGpuPolicy: linux.WebviewGpuPolicyNever,
// ProgramName: "wails",
},
})
if err != nil {
println("Error:", err.Error())
}
}
/* app.go */
package main
import (
"context"
"github.com/emarifer/Nu-i-uita/internal/db"
"github.com/emarifer/Nu-i-uita/internal/models"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// App struct
type App struct {
ctx context.Context
db *db.Db
selectedDirectory string
selectedFile string
}
// NewApp creates a new App application struct
func NewApp() *App {
db := db.NewDb()
return &App{db: db}
}
// startup is called when the app starts. The context is saved
// so we can call the runtime methods
func (a *App) startup(ctx context.Context) {
var fileLocation string
a.ctx = ctx
runtime.EventsOn(a.ctx, "change_titles", func(optionalData ...interface{}) {
if appTitle, ok := optionalData[0].(string); ok {
runtime.WindowSetTitle(a.ctx, appTitle)
}
if selectedDirectory, ok := optionalData[1].(string); ok {
a.selectedDirectory = selectedDirectory
}
if selectedFile, ok := optionalData[2].(string); ok {
a.selectedFile = selectedFile
}
})
runtime.EventsOn(a.ctx, "quit", func(optionalData ...interface{}) {
runtime.Quit(a.ctx)
})
runtime.EventsOn(a.ctx, "export_data", func(optionalData ...interface{}) {
d, _ := runtime.OpenDirectoryDialog(a.ctx, runtime.
OpenDialogOptions{
Title: a.selectedDirectory,
})
if d != "" {
f, err := a.db.GenerateDump(d)
if err != nil {
runtime.EventsEmit(a.ctx, "saved_as", err.Error())
return
}
runtime.EventsEmit(a.ctx, "saved_as", f)
}
})
runtime.EventsOn(a.ctx, "import_data", func(optionalData ...interface{}) {
fileLocation, _ = runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
Title: a.selectedFile,
})
// fmt.Println("SELECTED FILE:", fileLocation)
if fileLocation != "" {
runtime.EventsEmit(a.ctx, "enter_password")
}
})
runtime.EventsOn(a.ctx, "password", func(optionalData ...interface{}) {
// fmt.Printf("MY PASS: %v", optionalData...)
if pass, ok := optionalData[0].(string); ok {
if len(fileLocation) != 0 {
err := a.db.ImportDump(pass, fileLocation)
if err != nil {
runtime.EventsEmit(a.ctx, "imported_data", err.Error())
return
}
runtime.EventsEmit(a.ctx, "imported_data", "success")
}
}
})
}
// beforeClose is called when the application is about to quit,
// either by clicking the window close button or calling runtime.Quit.
// Returning true will cause the application to continue, false will continue shutdown as normal.
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
defer a.db.Close()
return false
}
...
.
├── app.go
├── build
│ ├── appicon.png
│ ├── darwin
│ │ ├── Info.dev.plist
│ │ └── Info.plist
│ ├── README.md
│ └── windows
│ ├── icon.ico
│ ├── info.json
│ ├── installer
│ │ ├── project.nsi
│ │ └── wails_tools.nsh
│ └── wails.exe.manifest
├── frontend
│ ├── index.html
│ ├── package.json
│ ├── package.json.md5
│ ├── package-lock.json
│ ├── postcss.config.js
│ ├── README.md
│ ├── src
│ │ ├── App.svelte
│ │ ├── assets
│ │ │ ├── fonts
│ │ │ │ ├── nunito-v16-latin-regular.woff2
│ │ │ │ └── OFL.txt
│ │ │ └── images
│ │ │ └── logo-universal.png
│ │ ├── lib
│ │ │ ├── BackBtn.svelte
│ │ │ ├── BottomActions.svelte
│ │ │ ├── EditActions.svelte
│ │ │ ├── EntriesList.svelte
│ │ │ ├── Language.svelte
│ │ │ ├── popups
│ │ │ │ ├── alert-icons.ts
│ │ │ │ └── popups.ts
│ │ │ ├── ShowPasswordBtn.svelte
│ │ │ └── TopActions.svelte
│ │ ├── locales
│ │ │ ├── en.json
│ │ │ └── es.json
│ │ ├── main.ts
│ │ ├── pages
│ │ │ ├── About.svelte
│ │ │ ├── AddPassword.svelte
│ │ │ ├── Details.svelte
│ │ │ ├── EditPassword.svelte
│ │ │ ├── Home.svelte
│ │ │ ├── Login.svelte
│ │ │ └── Settings.svelte
│ │ ├── style.css
│ │ └── vite-env.d.ts
│ ├── svelte.config.js
│ ├── tailwind.config.js
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ ├── vite.config.ts
│ └── wailsjs
│ ├── go
│ │ ├── main
│ │ │ ├── App.d.ts
│ │ │ └── App.js
│ │ └── models.ts
│ └── runtime
│ ├── package.json
│ ├── runtime.d.ts
│ └── runtime.js
├── go.mod
├── go.sum
├── internal
│ ├── db
│ │ └── db.go
│ └── models
│ ├── crypto.go
│ ├── master_password.go
│ └── password_entry.go
├── LICENSE
├── main.go
├── Makefile
├── README.md
├── scripts
└── wails.json
/* main.go */
package main
import (
"embed"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
"github.com/wailsapp/wails/v2/pkg/options/linux"
)
//go:embed all:frontend/dist
var assets embed.FS
//go:embed build/appicon.png
var icon []byte
func main() {
// Create an instance of the app structure
app := NewApp()
// Create application with options
err := wails.Run(&options.App{
// Title: "Nu-i uita • minimalist password manager",
Width: 450,
Height: 300,
DisableResize: true,
AssetServer: &assetserver.Options{
Assets: assets,
},
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
OnStartup: app.startup,
OnBeforeClose: app.beforeClose,
Bind: []interface{}{
app,
},
// Linux platform specific options
Linux: &linux.Options{
Icon: icon,
// WindowIsTranslucent: true,
WebviewGpuPolicy: linux.WebviewGpuPolicyNever,
// ProgramName: "wails",
},
})
if err != nil {
println("Error:", err.Error())
}
}
/* app.go */
package main
import (
"context"
"github.com/emarifer/Nu-i-uita/internal/db"
"github.com/emarifer/Nu-i-uita/internal/models"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// App struct
type App struct {
ctx context.Context
db *db.Db
selectedDirectory string
selectedFile string
}
// NewApp creates a new App application struct
func NewApp() *App {
db := db.NewDb()
return &App{db: db}
}
// startup is called when the app starts. The context is saved
// so we can call the runtime methods
func (a *App) startup(ctx context.Context) {
var fileLocation string
a.ctx = ctx
runtime.EventsOn(a.ctx, "change_titles", func(optionalData ...interface{}) {
if appTitle, ok := optionalData[0].(string); ok {
runtime.WindowSetTitle(a.ctx, appTitle)
}
if selectedDirectory, ok := optionalData[1].(string); ok {
a.selectedDirectory = selectedDirectory
}
if selectedFile, ok := optionalData[2].(string); ok {
a.selectedFile = selectedFile
}
})
runtime.EventsOn(a.ctx, "quit", func(optionalData ...interface{}) {
runtime.Quit(a.ctx)
})
runtime.EventsOn(a.ctx, "export_data", func(optionalData ...interface{}) {
d, _ := runtime.OpenDirectoryDialog(a.ctx, runtime.
OpenDialogOptions{
Title: a.selectedDirectory,
})
if d != "" {
f, err := a.db.GenerateDump(d)
if err != nil {
runtime.EventsEmit(a.ctx, "saved_as", err.Error())
return
}
runtime.EventsEmit(a.ctx, "saved_as", f)
}
})
runtime.EventsOn(a.ctx, "import_data", func(optionalData ...interface{}) {
fileLocation, _ = runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
Title: a.selectedFile,
})
// fmt.Println("SELECTED FILE:", fileLocation)
if fileLocation != "" {
runtime.EventsEmit(a.ctx, "enter_password")
}
})
runtime.EventsOn(a.ctx, "password", func(optionalData ...interface{}) {
// fmt.Printf("MY PASS: %v", optionalData...)
if pass, ok := optionalData[0].(string); ok {
if len(fileLocation) != 0 {
err := a.db.ImportDump(pass, fileLocation)
if err != nil {
runtime.EventsEmit(a.ctx, "imported_data", err.Error())
return
}
runtime.EventsEmit(a.ctx, "imported_data", "success")
}
}
})
}
// beforeClose is called when the application is about to quit,
// either by clicking the window close button or calling runtime.Quit.
// Returning true will cause the application to continue, false will continue shutdown as normal.
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
defer a.db.Close()
return false
}
...
EventsOn(
ctx context.Context,
eventName string,
callback func(optionalData ...interface{}),
) func()
.
├── app.go
├── build
│ ├── appicon.png
│ ├── darwin
│ │ ├── Info.dev.plist
│ │ └── Info.plist
│ ├── README.md
│ └── windows
│ ├── icon.ico
│ ├── info.json
│ ├── installer
│ │ ├── project.nsi
│ │ └── wails_tools.nsh
│ └── wails.exe.manifest
├── frontend
│ ├── index.html
│ ├── package.json
│ ├── package.json.md5
│ ├── package-lock.json
│ ├── postcss.config.js
│ ├── README.md
│ ├── src
│ │ ├── App.svelte
│ │ ├── assets
│ │ │ ├── fonts
│ │ │ │ ├── nunito-v16-latin-regular.woff2
│ │ │ │ └── OFL.txt
│ │ │ └── images
│ │ │ └── logo-universal.png
│ │ ├── lib
│ │ │ ├── BackBtn.svelte
│ │ │ ├── BottomActions.svelte
│ │ │ ├── EditActions.svelte
│ │ │ ├── EntriesList.svelte
│ │ │ ├── Language.svelte
│ │ │ ├── popups
│ │ │ │ ├── alert-icons.ts
│ │ │ │ └── popups.ts
│ │ │ ├── ShowPasswordBtn.svelte
│ │ │ └── TopActions.svelte
│ │ ├── locales
│ │ │ ├── en.json
│ │ │ └── es.json
│ │ ├── main.ts
│ │ ├── pages
│ │ │ ├── About.svelte
│ │ │ ├── AddPassword.svelte
│ │ │ ├── Details.svelte
│ │ │ ├── EditPassword.svelte
│ │ │ ├── Home.svelte
│ │ │ ├── Login.svelte
│ │ │ └── Settings.svelte
│ │ ├── style.css
│ │ └── vite-env.d.ts
│ ├── svelte.config.js
│ ├── tailwind.config.js
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ ├── vite.config.ts
│ └── wailsjs
│ ├── go
│ │ ├── main
│ │ │ ├── App.d.ts
│ │ │ └── App.js
│ │ └── models.ts
│ └── runtime
│ ├── package.json
│ ├── runtime.d.ts
│ └── runtime.js
├── go.mod
├── go.sum
├── internal
│ ├── db
│ │ └── db.go
│ └── models
│ ├── crypto.go
│ ├── master_password.go
│ └── password_entry.go
├── LICENSE
├── main.go
├── Makefile
├── README.md
├── scripts
└── wails.json
/* main.go */
package main
import (
"embed"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
"github.com/wailsapp/wails/v2/pkg/options/linux"
)
//go:embed all:frontend/dist
var assets embed.FS
//go:embed build/appicon.png
var icon []byte
func main() {
// Create an instance of the app structure
app := NewApp()
// Create application with options
err := wails.Run(&options.App{
// Title: "Nu-i uita • minimalist password manager",
Width: 450,
Height: 300,
DisableResize: true,
AssetServer: &assetserver.Options{
Assets: assets,
},
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
OnStartup: app.startup,
OnBeforeClose: app.beforeClose,
Bind: []interface{}{
app,
},
// Linux platform specific options
Linux: &linux.Options{
Icon: icon,
// WindowIsTranslucent: true,
WebviewGpuPolicy: linux.WebviewGpuPolicyNever,
// ProgramName: "wails",
},
})
if err != nil {
println("Error:", err.Error())
}
}
/* app.go */
package main
import (
"context"
"github.com/emarifer/Nu-i-uita/internal/db"
"github.com/emarifer/Nu-i-uita/internal/models"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// App struct
type App struct {
ctx context.Context
db *db.Db
selectedDirectory string
selectedFile string
}
// NewApp creates a new App application struct
func NewApp() *App {
db := db.NewDb()
return &App{db: db}
}
// startup is called when the app starts. The context is saved
// so we can call the runtime methods
func (a *App) startup(ctx context.Context) {
var fileLocation string
a.ctx = ctx
runtime.EventsOn(a.ctx, "change_titles", func(optionalData ...interface{}) {
if appTitle, ok := optionalData[0].(string); ok {
runtime.WindowSetTitle(a.ctx, appTitle)
}
if selectedDirectory, ok := optionalData[1].(string); ok {
a.selectedDirectory = selectedDirectory
}
if selectedFile, ok := optionalData[2].(string); ok {
a.selectedFile = selectedFile
}
})
runtime.EventsOn(a.ctx, "quit", func(optionalData ...interface{}) {
runtime.Quit(a.ctx)
})
runtime.EventsOn(a.ctx, "export_data", func(optionalData ...interface{}) {
d, _ := runtime.OpenDirectoryDialog(a.ctx, runtime.
OpenDialogOptions{
Title: a.selectedDirectory,
})
if d != "" {
f, err := a.db.GenerateDump(d)
if err != nil {
runtime.EventsEmit(a.ctx, "saved_as", err.Error())
return
}
runtime.EventsEmit(a.ctx, "saved_as", f)
}
})
runtime.EventsOn(a.ctx, "import_data", func(optionalData ...interface{}) {
fileLocation, _ = runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
Title: a.selectedFile,
})
// fmt.Println("SELECTED FILE:", fileLocation)
if fileLocation != "" {
runtime.EventsEmit(a.ctx, "enter_password")
}
})
runtime.EventsOn(a.ctx, "password", func(optionalData ...interface{}) {
// fmt.Printf("MY PASS: %v", optionalData...)
if pass, ok := optionalData[0].(string); ok {
if len(fileLocation) != 0 {
err := a.db.ImportDump(pass, fileLocation)
if err != nil {
runtime.EventsEmit(a.ctx, "imported_data", err.Error())
return
}
runtime.EventsEmit(a.ctx, "imported_data", "success")
}
}
})
}
// beforeClose is called when the application is about to quit,
// either by clicking the window close button or calling runtime.Quit.
// Returning true will cause the application to continue, false will continue shutdown as normal.
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
defer a.db.Close()
return false
}
...
最後,我們從上一個步驟建立的 我們解密 DTO 並將其轉換為
我們剩下的最後一件事就是將語言程式碼保存在資料庫中。
以上是極簡密碼管理器桌面應用程式:進軍 Golang 的 Wails 框架(第 1 部分)的詳細內容。更多資訊請關注PHP中文網其他相關文章!