基於 Graphql 的前後端協作方案

作者簡介: 薛揚波,來自抖音直播平臺前端團隊,團隊負責主播工會等行業產品研發,以及運營平臺、數據策略平臺建設

一、背景 & 目標

1、當前問題:

2、方案目標:

3、技術選型:

二、框架選型

其他工具:

開發插件

請求代碼 & 類型生成

接口請求

Schema 文檔

提示 & 靜態檢查

三、相關流程

1、請求流程

Graphql 請求解析流程上圖中從 1 到 5 展示了 GraphQL 一個請求到響應的過程,5 個步驟分別如下:

  1. HTTP 請求描述前端所需要的數據結構

  2. 請求到達服務端,解析器首先解析 query 下的第一個層 user,在該解析器內可以編寫對 user 的處理邏輯

  3. 解析器繼續深入 user 下的 id 字段,對 id 字段進行解析

  4. 解析器對與 id 平級的 name 字段進行解析

  5. HTTP 響應返回處理後的結果

前後端協作完整請求響應流程:整體的請求流程時序圖分爲三個部分:

  1. 前端客戶端:主要進行 Graphql 請求的發起,並且對請求的響應進行處理

  2. 請求網關層:網關層進行請求權限的判斷,在網關層調用權限服務進行接口請求的權限判斷

  3. GMP 服務層:該層主要指 GMP 平臺的相關服務,包括編排服務、數據模型管理服務;編排服務主要處理請求的服務編排階段,通過數據模型管理服務隊編排產物、數據模型進行存儲和管理;處理後的請求通過編排服務的數據源代理調用相關的下游服務

2、前端工作流

在新模式下前端工作流主要是如下三個階段:

  1. GMP 平臺配置階段:該階段內前端同學主要關注 模型 和 請求配置
  1. 工程項目使用階段:該階段內前端同學主要關注 請求生成 和 本地調試
  1. 工程化測試 / 部署階段:該階段即爲前端項目的測試 / 部署流程,使用現有內部 CI/CD 平臺流水線進行發佈即可

3、完整業務流程

業務流程分爲兩階段:

  1. 開發階段:該階段主要有以下兩個步驟
  1. 運行時階段:運行時階段主要步驟如下

四、數據模型管理

搭建數據模型管理平臺 (GMP 平臺),統一前後端模型標準,建設業務模型管理方案,實現模型複用,建設服務編排能力。結合低代碼實現 API 快速編排,server 研發精力能聚焦在開發特定需求的專有模型,前端也基於通用的模型完成標準化的消費鏈路。

1、平臺功能

平臺主要由三部分組成

  1. 數據模型管理:模塊主要功能包括項目管理、數據源管理、Schema defination 管理,具體去做數據域的劃分,通過對 Graphql schema defination 的管理來實現數據模型的抽象和管理。

  2. 服務編排管理:通過對請求中的數據源進行編排,結合數據模型管理的 schema defination 來實現服務的編排管理,產出適合業務的 Graphql 請求和 schema 定義。

  3. Graphql 請求管理:該模塊主要面向前端同學,前端同學可以在該模塊根據已有的服務編排 schema 定義構造具體的業務請求,可以在線進行接口調試、保存、編輯等,並且結合 cli 工具進行本地代碼生成。

2、平臺架構

3、技術方案

3.1 Graphql 編輯器

GMP 平臺中主要有兩個模塊需要使用 Graphql 編輯器,分別如下:

GraphQL 編輯器方案選型:

6parKb

3.2 後臺管理列表

平臺內管理列表相關的頁面有如下功能:

3.3 CLI 命令行工具

CLI 命令行工具主要是打通 GMP 平臺與前端項目,通過 CLI 工具可以直接基於 GMP 平臺數據在前端本地項目中進行 前端請求代碼生成與 TS 類型生成。

注意點:

五、業務解決方案

1、請求鑑權

編排服務層通過 resolver,解析出 query 中的 scheme,scheme 管理到權限點,鑑權時用 scheme 唯一標識進行鑑權。

Graphql schema defination 中 auth keyword 的綁定規則如下:

type User @auth(keyword: xxx) {
  name: String
  banned: Boolean
  canPost: Boolean
  products: [Product]
}

type Product @auth(keyword: xxx) {
  name: String
  banned: Boolean
  canPost: Boolean
  user: User
}
 
type Query {
  Users: [User]
}

type Multation {
  updateUser: User @auth(keyword: xxx)
}

2、灰度策略

3、客戶端緩存

緩存不僅可以讓前端在運行時變得更加高效,還可以極大地提升開發效率,並且減少各類數據不一致問題引發的 bug。與其它 API 規範相比,GraphQL 和前端緩存的結合可以讓這些優勢再次被放大。

3.1 緩存流程

# 列表查詢demo
query {
  getAnchors(page: 1) {
    id
    name
  }
}

# 查詢結果
{
  "getAnchors": [{ "id": "1", "name": "xueyangbo" }]
}

# apollo-client 也會在它的緩存中針對本次請求保存爲以下結構
{
  ROOT_QUERY: {
    getAnchors(page: 1): [{id: "Anchor:1", typename: "Anchor"}]
  }
  Anchor:1: {id: "1", __typename: "Anchor", title: "xueyangbo"}
}

當前端頁面再一次發出同樣的請求時,apollo-client 會優先通過以下方式查詢是否命中緩存:

  1. 進入 ROOT_QUERY

  2. 查詢是否有 getAnchors(page: 1) 對應的結果,得到 [{id: "Anchor:1", typename: "Anchor"}]

  3. 查詢是否有 Anchor:1 對應的數據

  4. 所有查詢均命中時則按對應結構將緩存中的數據拼裝爲正確的結構返回給前端(Normalization 過程)

3.2 緩存策略

和 redux 類似,apollo-client 的數據緩存也是響應式的。發起數據請求的 hooks 處會訂閱它所依賴的數據,當緩存中的數據更新時,依賴對應數據的 UI 會正確地更新到最新狀態。

與 redux 狀態管理方案不同之處在於:

  1. apollo-client 將這部分狀態存儲和網絡請求緊密結合在了一起。這意味着前端同學不需要再在網絡請求和數據存儲之間編寫額外的模板代碼以及抽象封裝;

  2. 當發送數據更新 mutation 請求時,apollo-client 也能夠感知這一變化,並自動更新數據緩存而不需要編寫額外代碼;

  3. apollo-client 通過 data object id 作爲唯一標識進行 normalization。並且支持對特定 type 進行自定義 id,來保證緩存唯一性;

問題點:

在所有查詢、更新操作中 apollo-client 緩存都表現良好,但是對於 create 和 delete 類的操作數據緩存表現在複雜需求時會大概率出現緩存更新出錯問題。apollo-client 提供了兩種方式用於解決 create 和 delete 類數據後的緩存更新:

解決方案:

4、複用

4.1 模型複用

4.2 請求複用

GraphQL Fragments 是可以在多個 Query 和 Mutation 之間共享的一段邏輯

4.2.1 定義方式

定義規則

import { gql } from '@apollo/client';

export const FRAGMENT_DEMO = gql`
  fragment AnchorField on Anchor {
      anchor_uid
      aweme_display_id
      hotsoon_display_id
      xigua_display_id
      anchor_nickname
      anchor_avatar
   }
   
   fragment FactionField on Faction {
      faction_name
      principal
      faction_id
   }
`;

4.2.2 使用方式

// 直接引入使用
import { gql } from '@apollo/client';
import { FRAGMENT_DEMO } from './fragments';

export const GET_ANCHOR_INFO = gql`
  ${FRAGMENT_DEMO}
  query getAnchorInfo($postId: ID!) {
    anchor(postId: $postId) {
      ...AnchorField
      faction {
        ...FactionField
      }
    }
  }
`;

5、接口調試

研發鏈路中有兩個地方可以進行接口調試

6、錯誤處理

採用 Graphql 方式請求業務數據時,會出現不同的業務錯誤,需要根據不同的錯誤類型進行處理,能夠在發生錯誤時對用戶顯示適當的信息。錯誤類型包括:

錯誤返回格式

{
  "errors": [
    {
      "message": "Cannot query field \"nonexistentField\" on type \"Query\".",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "extensions": {
        "code": "GRAPHQL_VALIDATION_FAILED",
        "exception": {
          "stacktrace": [
            "GraphQLError: Cannot query field \"nonexistentField\" on type \"Query\".",
            "...additional lines..."
          ]
        }
      }
    }
  ],
  "data": null
}

錯誤處理方式

7、監控報警

標準化前端監控報警上報,並對上報進行了類型劃分,由於使用 garphql 發起請求時採用固定的請求 path,因此無法通過 path 進行上報區分,因此可藉助現有監控 SDK 的自定義上報能力,上報 graphql 請求的 query name 進行請求業務上報。

8、業務場景

8.1 工作臺

在實際項目中有些頁面需要加載大量數據,導致請求時間較長,用戶體驗差。比如首屏渲染 因爲首屏需要請求更多內容,通常情況下 比原來多了更多 HTTP 的往返時間 (RTT),這造成了白屏,如果白屏時間過長,用戶體驗會大打折扣。使用 gql 時可以分兩次進行請求

8.2 分頁

const FEED_QUERY = gql`
  query Feed($offset: Int, $limit: Int) {
    feed(offset: $offset, limit: $limit) {
      id
      # ...
    }
  }
`;

const PaginationDemo() {
 const { loading, data, fetchMore } = useQuery(FEED_QUERY, {
    variables: {
      offset: 0,
      limit: 10
    },
  });
  
if (loading) return 'Loading...';

return (
    <Feed
      entries={data.feed || []}
      onLoadMore={() => fetchMore({
        variables: {
          offset: data.feed.length
        },
      })}
    />
  );
}

8.3 輪詢

輪詢場景時可以對該輪訓請求禁用緩存策略,保證每次輪訓的接口都是最新的數據

const defaultOptions = {
    pollingQuery: {
        pollingFollow: 'no-cache', // 比如跟播請求場景
    },
}
const client = new ApolloClient({
    link: concat(authMiddleware, httpLink),
    cache: new InMemoryCache(),
    defaultOptions: defaultOptions
});

// 發起請求使用輪訓參數
const { loading, error, data } = useQuery(GET_XXX, {
    variables: { anchorID: 1 },
    pollInterval: 1000,
})
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/j8WAHoW8J8lpeG-eZI962w