【文章翻譯】How I converted GenLatte to fullstack Dart
【文章內容使用 Gemini 2.5 Flash 自動翻譯產生】
原文:https://flutter.dev/blog/converted-genlatte-fullstack-dart
我如何將 GenLatte 轉換為全端 Dart
並減少應用程式的伺服器費用
如果您曾追隨 Flutter 團隊進軍經營快閃咖啡攤的曲折故事,您會知道我們結合了 Flutter、Firebase 和 Gemini 來提供奇異的咖啡。您也知道,由於不收取任何費用,我們未能獲利。
此外,如果您查看了程式碼,您可能還注意到我們的 Flutter 前端是由用 Node.js 撰寫的 Firebase 函式所補充的。這是一個有點令人驚訝的歷史遺物,因為 Firebase 對 Dart 函式的支援幾乎與 GenLatte 首次在 Google Cloud Next 出現的同時進入公開預覽。
但這正是問題所在——由於 Firebase 對 Dart 的支援在最後一刻熱烈推出,我們根本無法承諾在生產應用程式中使用它,因為時間壓力很大。如果 Firebase 對 Dart 的支援因任何原因而延遲,我們可能就無法及時在 Cloud Next 部署。因此,當 GenLatte 在 4 月、5 月或 6 月出現時,它是以 Node.js 後端運行的。
我個人對此很不滿。
完全擁抱全端 Dart
Dart 和 JavaScript 是具有不同優勢的不同語言。從某種意義上說,這有點像一個不言而喻的說法,但它對伺服器端遷移也有更深層次的影響,因此一對一重寫可能不值得。畢竟,伺服器上的 Dart 可以享受與客戶端上的 Dart 的端到端類型安全,如果仍然堅持使用無類型 Map 那將會是一個真正的遺憾。
考慮到這一點,儘管我的目標可能比我的能力大,但我開始大幅重寫 GenLatte。我的許多目標包括:
- 在整個堆疊中共享模型(這是一個小小的提升,因為我最初決定將所有資料類別放在單獨的
genlatte_data套件中) - 將我們的 Firebase 函式足跡減少到單一可部署函式
- 移除所有客戶端寫入;改為專注於呼叫伺服器端函式
- 移除所有伺服器端觸發器,並明確地在從客戶端呼叫的伺服器端函式中執行任何資料變更
- 保留所有基於角色的 ACL 檢查
- 最終進行端到端測試!
這些目標很宏偉,而且不在我 2026 年的路線圖上,所以,我自然而然地保密了我的計畫並開始打字!
執行遷移
與我對原始 GenLatte 的工作不同,該專案主要是在沒有使用 AI 的情況下開發的,我知道我緊迫的時間表需要引入一個 Zoom zoom 打字助手。我選擇的模型是 Gemini 3.6 Flash,它結合了低延遲和通用的知識,這對我來說非常有用。
從高層次來看,我仍然閱讀 Gemini 撰寫的每一行程式碼,以保持對專案的認知所有權。從深入了解的角度開始,這讓事情變得更容易,如果沒有這個承諾,我不認為重寫會在我可用的幾天內成功。程式碼助手,儘管它們很棒,但仍然非常受益於人類的指導。
共享模型
全端類型安全意味著客戶端和伺服器共享程式碼。如前所述,這意味著將任何必須在伺服器上執行的程式碼拆分成自己的套件,並且,至關重要的是,不能依賴 Flutter SDK。這在很大程度上是簡單的,但確實需要對我自己的資料管理套件 pkg:data_layer 進行補充。它的其中一個補充套件 pkg:data_layer_firestore 依賴於客戶端 Firebase SDK(它依賴於 Flutter),因此我被迫添加了 pkg:data_layer_firestore_admin,它是純 Dart 並且因此對伺服器友好。
使用單一 Firebase 函式
預設情況下,您部署的每個 Firebase 函式都會變成自己的 Cloud Run 服務。(如果您是 Firebase Functions 使用者,而這對您來說是新聞,請導航到 Google Cloud 控制台中的 Cloud Run,看看 Firebase 香腸是如何製作的!)
然而,在 GenLatte 的 Node.js 時代,我依賴了超過 15 個獨立的 Cloud Run 服務,我知道我想要一個更精簡的設定,原因有很多。首先,部署會顯著加快,但更重要的是,它會大幅降低 GenLatte 的伺服器費用。
「但是 Craig!」您說,「Cloud Run 在不使用時會縮小到零,所以這實際上很重要嗎?」
好問題。我很高興您有注意。
是的,這非常重要!當 GenLatte 正在使用時,各種資料寫入和非同步任務會啟動所有 15 個服務,雖然每個服務在閒置時都會關閉,但這仍然對我們的伺服器費用產生可預測的影響。但是,更糟糕的是,我們將每個服務的最小節點計數設定為 1,以避免冷啟動,這當然會取消這個縮小到零的功能。最終結果是 GenLatte 的啟動成本出奇地高。
如何將所有內容塞入一個 Firebase 服務
為了省錢,我決定借鑑 Serverpod 的靈感,引入一個 BackendMessage DTO 來告訴我的單一 Firebase 函式我想要它實際呼叫的內部函式。在 Gemini 幫助我編寫的一些巧妙的 pkg:freezed 技巧下,我甚至能夠獲得類型化的回應。
最終的類別設定有點複雜,但如果您喜歡類型安全和省錢,那麼值得理解。
BackendMessage
這是我在實際函式簽章中使用的父 DTO 類別。
1 | abstract interface class BackendMessage<R extends MessageResponse> { |
MessageParameters
這實作了 BackendMessage 並使用 pkg:freezed 將個別訊息類型與預期的回應類別綁定。
@Implements.fromString 技巧產生滿足父類別要求以綁定 MessageResponse 類型的類別。雖然源類別使用原始字串感覺類型不安全,但任何拼寫錯誤都會導致產生類別中的錯誤,這意味著它在功能上是類型安全的。
1 |
|
MessageResponse
這關閉了迴圈,宣告了每個方法呼叫預期的回應類別。它為需要即時回饋的方法提供了單獨的回應類型,並提供了空回應以表示 202 Accepted 或 204 No Content HTTP 回應。
1 |
|
我的 DTO 都準備好後,我需要一個單一的伺服器端函式來接收並相應地路由每個傳入的 BackendMessage。
1 | firebase.https.onCallWithData<MessageParameters, Object>( |
最後,我需要滿足其契約的 saveOrder 和 completeOrder 方法。
1 | // Other Dart files |
有了這些系統,我就可以將任意多的伺服器端操作塞進一個 Dart 函式中。太省錢了!
重構所有寫入
上述 BackendMessage 系統為立即實現我的另外三個子目標奠定了基礎:
- 我透過在
firestore.rules檔案中阻擋客戶端寫入來移除所有客戶端寫入。我也重構了我的資料管理層,以呼叫單一後端函式,而不是直接呼叫docRef.set()等 Firestore 函式。 - 我同樣移除了所有 Firestore 觸發器,但將遺失的功能重新實例化為我可以從客戶端明確呼叫的函式。
- 我透過在 Dart 程式碼中引入 ACL 檢查來維持 GenLatte 嚴格的權限模型,作為一個可測試的系統,這讓我晚上睡得安穩。
加入端對端測試
在我的最後一招中,我能夠命令 Gemini 撰寫伺服器端測試,並且非常令人興奮地,在客戶端測試中模擬後端行為,這些測試突然變成了端對端測試。鑒於我的類型安全回傳值,這帶來了我滿意的穩定性和應用程式可靠性!
全端語言的樂趣
無論您喜歡關聯式資料,因此使用 Serverpod 之類的工具,還是像 Cloud Firestore 這樣的非關聯式解決方案,在整個堆疊中以相同的語言撰寫程式碼都是一種夢想。如果該語言是 Dart,並且您發現自己沉浸在完全健全的 null-safe 類型安全的榮光中,那麼勝利將不斷降臨。它確實創造了電腦實際上是您朋友的錯覺!
我很享受將 GenLatte 轉換為處處使用 Dart 的過程,透過這樣做,我將其伺服器費用削減到 Node.js 時代的一小部分,並提高了可靠性和效能。我用於執行變更的 Gemini 代幣費用在短短幾分鐘的操作後就輕鬆收回了。現在,GenLatte 可以長期為活動參與者提供個人化咖啡,或者直到我們厭倦經營它為止。