GraphQL兩年實戰避坑經驗

本文作者分享了在生產環境中使用 GraphQL 的一些經驗和解決方法,並給出了一些構建實用 GraphQL 查詢和變更(Mutation)的建議。

本文最初發佈於 Medium,經原作者 Stein Janssen 授權由 InfoQ 中文站翻譯並分享。

GraphQL 已得到廣泛認可並日益流行。我們在使用中遇到了一些非常有挑戰性的問題,值得撰文分享。本文將使用一個示例配置來闡釋問題,並給出相應的解決方法。

GraphQL兩年實戰避坑經驗

本文作者使用 GraphQL Voyager 生成的關係概覽圖

首先談談我們為什麼會選擇 GraphQL?

  • 無需操心如何更新文檔,所有的查詢(Query)和變更會自動形成文檔。
  • 無需獲取整個數據集,我們可以編寫僅僅返回所請求數據的查詢。
  • 對前端提供統一的訪問點。從數十個不同 API 中獲取數據並非易事。GraphQL 支持開發人員將所有 API 進行拼接(Stitching)。

拼接(Stitching)

拼接(Stitching)讓我們可以從同一端點獲取所有數據。這聽上去不錯,但它也會導致一些非常棘手的問題。舉例說明:

GraphQL兩年實戰避坑經驗

如上圖所示,一個前端與 Public API 通信。Public API 拼接了 Order API,後者又拼接了 Product API。前端唯一能訪問的是 Public API。看上去這種鏈式拼接方式並沒有太大的問題,但是在面對數十層級的 API 拼接時,應用發佈將成為一場災難。

問題出在哪裡?一旦 Product API 的 Schema 發生更改,那麼需要依次重新啟動 Order 和 Public API 才能重新加載 Schema。GraphQL Schema 每次更新時,都必須重新啟動多個 API。這非常繁瑣。

另一個可能出現的問題是,如果應用需要逆鏈反向查詢,而非順鏈而下查詢,這時拼接無法工作。例如從 Product 訪問 Order,由於 Order API 需要加載 Product 的 Schema,因此只有在 Product API 運行時才能啟動 Order API。

這意味著,雖然可以獲取屬於給定 Order 的 Product:

<code>order {
    identifier
    products {
        identifier
    }
}/<code>

但無法獲取給定 Product 所在的 Order:

<code>products {
    identifier
    order {
        identifier
    }
}/<code>

為解決這個問題,我們需要重新拼接 API。我們可以讓 Public 負責加載所有的 Schema。這樣只需重啟該 API,即可加載所有 Schema。

GraphQL兩年實戰避坑經驗

鑑於現在 Public API 獲取所有的 Schema,我們可以添加處理 order 屬性的代碼,擴展 Product 的 Schema。這樣,我們就可以通過查詢 Product 獲取 Order 信息。擴展 Schema 的詳細做法,參見 Apollo 文檔。

https://www.apollographql.com/docs/apollo-server/federation/entities/#extending

現在,我們並不需要通過 Public API 獲得所有查詢和變更的訪問權。例如,我們並不想讓客戶能夠通過觸發變更去更改支付的狀態。對此,一種解決方法是過濾掉特定查詢和變更。具體而言,應用遍歷 Schema 中所有的查詢和變更,並與給定的列表做對比。如果查詢存在於列表中,則設為可見。如果不在列表中,就從 Schema 中移除。另一個解決方法是添加中間件,由中間件檢查當前用戶是否有權限觸發特定的查詢和變更。

實踐中,我們組合使用了上面兩種方法。但現在我們面對一個新的問題。當前並非所有的變更都可通過 Public API 訪問,因此在更新支付的狀態時需要直接調用 Payment API。這對於變更不存在問題,但並不適用於所有的查詢,因為父對象和子對象只是在 Public API 做拼接。為解決這個問題,我們需要再次重新編排配置,如下圖所示:

GraphQL兩年實戰避坑經驗

這裡,我們新建了一個 Gateway API,負責拼接所有 Schema。而 Public API 只拼接 Gateway API,並移除所有前端無需訪問的查詢和變更。這樣,Gateway 可與後端服務部署在同一網絡,後端在進行查詢和變更時可直接使用 Gateway API。

查詢分頁(Paginated)

一些情況下,實現 查詢分頁 很有必要。我們採用了基於遊標的方法,實踐中很好用。需要獲取 Product 時,可使用如下查詢:

<code>products(first: 5, after: "cursor") {
    edges {
        node {
            identifier
        }
    }
}/<code>

但是,由於我們更改了 Schema,在獲取 Product 對應的 Order 時會生成分頁結構:

<code>order {
    products(first: 5, after: "cursor") {
        edges {
            node {
                identifier
            }
        }
    }
}/<code>

但是這裡我們並不需要分頁結構,因為給定 Order 的 Product 數量並不多。針對該問題,我們考慮分別編寫兩個查詢,一個實現了分頁,另一個則不考慮分頁。另一個做法是針對拼接 Product 到 Order 的情況,使用 Schema 包裝(Schema Wrapping)移除分頁。Schema 包裝是一個非常強大的方法,尤其是針對同一 API 種拼接了所有遠程 Schema 的情況。詳細信息,參見 Schema 包裝的官方文檔。

https://www.graphql-tools.com/docs/schema-wrapping/

一些建議

上面我們列舉了部分主要問題。下面給出一些有助於構建可維護 GraphQL API 的小建議:

  • 類型和枚舉的命名必須唯一。例如,如果需要對 Product 添加一個狀態,建議命名為 ProductStatus,而不要直接使用 Status,以免出現類型衝突問題。
  • 建議在查詢中添加過濾,以免額外單獨編寫查詢。推薦一個 很好的查詢實現例子,訪問頁面右側的“doc”選項卡, 並搜索 assetFilter。
  • 對查詢和變更定義自己的命名規則,以簡化對查詢和變更的查找。
  • 在使用查詢分頁時,設置默認值和最大上限。以免他人運行 API 時導致崩潰。
  • 推薦使用 GraphQL Voyager,可生成對 Schema 所有查詢、變更、關係的概覽圖。


分享到:


相關文章: