GraphQL 以及 Mosir 的檔案上傳系統
開發筆記 Frontend Backend
GraphQL 是現代服務常用的 API Query Language,高效能跟靈活性使得許多人在開始新專案時會直接考慮,Mosir 也在專案初期就選擇 GraphQL 作為前後端的共通標準。但 GraphQL 本身的架構在遇到檔案上傳時帶來不少麻煩。
GraphQL 以及 Mosir 的檔案上傳系統
開發筆記 Frontend Backend
GraphQL 是現代服務常用的 API Query Language,高效能跟靈活性使得許多人在開始新專案時會直接考慮,Mosir 也在專案初期就選擇 GraphQL 作為前後端的共通標準。但 GraphQL 本身的架構在遇到檔案上傳時帶來不少麻煩。
用過 Go + gqlgen 和 Node.js + TypeORM,反而覺得 GraphQL 被捧的太高了。沒有解決問題,反而製造問題,例如:N+1 需要 Dataloader 處理、無法傳遞二進制…
Facebook 當初設計 GraphQL,是為了讓 Resolver 能從不同微服務整合資料,統一回傳給前端。但很多人把它當成 HTTP API 的替代方案使用,就像 JWT 被誤用為取代 Session 一樣。
開發速度遠不如一個 POST /upload_video API。但前端生態彷彿不用上新技術就會被淘汰。似乎懂得什麼該捨棄,比學會技術還難。
但我現在用 Go + HTMX 超快樂… 🫠
噢對了,其實我個人的感受是 GraphQL 不只是 microservice 架構好用,在 modular-mono 也有一定優勢。以 mosir 的文章為例,post 跟 profile 完全是兩個不同的 module、提供不同服務。resolver(實際上是 gqlgen force resolver)依照需求把需要的 profile 放進 post,而不用在 post 跟 profile 本身塞不必要的相依性,也不需要讓 client 分兩次 request 才拿到需要的資料。
這種在大型專案的相依性解耦跟權責分離才是我們選擇 GraphQL 所帶來的好處,在專案越來越龐大的狀況下還是可以維持一定的程式碼整潔跟開發速度。
GraphQL 的核心設計理念圍繞著結構化資料的查詢與變更 (query and mutation)。它的請求和回應都是基於 JSON 這類的文字格式。傳統上,GraphQL 請求透過 HTTP POST 方法,將一個 JSON 物件作為請求主體 (request body) 發送到伺服器。這個 JSON 物件包含了查詢字串 (query string)、變數 (variables) 和操作名稱 (operation name)。
然而,檔案,特別是二進位檔案,本質上並非 JSON 這類結構化的文字資料。直接將二進位檔案嵌入 JSON 中,不僅會造成龐大的效能開銷(舉例來說,將檔案編碼成 Base64 字串會額外增加約 33% 的資料量),而且對伺服器的解析與處理也極為不便。
因此,GraphQL 規範本身並沒有提供直接處理檔案上傳的內建機制。開發者通常需要仰賴現有的 HTTP 機制或其他協定,來補足這方面的功能。
當我們在開發 Mosir 的中期,選擇了由社群發展出的一套基於 HTTP multipart/form-data 的方案,即 GraphQL Multipart Request Specification。
其核心概念是將傳統的 application/json Content Type 轉換為 multipart/form-data。一個典型的 multipart/form-data 請求會包含以下幾個部分:
- operations 部分: 包含常規的 GraphQL Query 或 Mutation,其中檔案變數會以 null 或特定的佔位符號表示。
在開發中後期遇到 multipart/form-data 的問題時,在評估後我們選擇改用 TUS (The Upload Server)。TUS 是一個開放的、獨立於 GraphQL 的協定,專為可靠的、可恢復的檔案上傳而設計。它的核心理念,就是將大型檔案拆分成可管理的區塊,並提供斷點續傳的能力。
TUS 在 GraphQL 架構中的角色並非直接融入 GraphQL 的請求處理,而是作為一個外部的、專門的檔案上傳服務,而 GraphQL API 則扮演協調者 (Coordinator) 的角色,尤其在檔案完成上傳後處理業務邏輯方面。
第一步:Client 與 TUS 伺服器互動 (HTTP P...