基於 Graphql 的前後端協作方案
作者簡介: 薛揚波,來自抖音直播平臺前端團隊,團隊負責主播工會等行業產品研發,以及運營平臺、數據策略平臺建設
一、背景 & 目標
1、當前問題:
-
接口無法複用: 當前業務接口設計初爲面向特定頁面特定功能的,接口與頁面一一綁定,接口字段缺失、冗餘的情況造成已有接口無法輕鬆複用到其他頁面。
-
缺少領域模型: 隨着業務的迭代,難免會出現一些數據聚合、數據過濾的接口需求,目前這種情況都需要 server 服務端同學去配合,研發協作成本上升,前端應用的同類型接口也越來越多。因此需要抽象領域模型或者數據模型這一層,該層是可以跨頁面維度,在不同的模塊下去複用模型,數據模型是要比接口這一層要更穩定更通用。
-
前後端協作流程繁瑣: 目前的前後端協作流程比較繁瑣,接口管理雖然通過內部 HTTP API 管理平臺平臺進行管理,但字段修改、數據結構變化等情況使的前後端開發無法完全分離,並且前後端協作工具缺失,整個協作流程低效。
2、方案目標:
-
業務接口複用: 前端可自由請求所需字段,返回業務需的業務數據,並且能夠根據業務模型管理,產出可複用的請求接口,並可跨頁面複用。
-
數據模型管理: 實現數據模型管理平臺(Graphql Model Management Platform,後文中用 GMP 平臺代稱),該平臺具備:數據模型管理、服務編排管理、Graphql 請求管理 等能力。
-
服務流程編排: 依賴後端服務數據源,能夠實現字段聚合,和對已有服務的編排組合,並且產出可複用的數據模型 Schema Definition 建模語句。
-
前後端協作流程優化: 藉助 Graphql 技術,並且產出相關研發工具,例如:Graphql 請求管理、Grapqhl 前端請求代碼生成、VScode 插件代碼高亮提示、Chrome 插件支持 Graphql 請求代理 mock 等工具。使得前後端開發完全分離,研發提效。
3、技術選型:
-
Graphql 優點:
-
API 字段的定製化,按需取字段
-
API 的聚合,一次請求拿到所有的數據
-
後端不再需要維護接口的版本號
-
完備的類型校驗機制,提供了更健壯的接口
-
解決字段命名問題。駝峯、下劃線等,判斷是否需要返回全大寫、全小寫或首字母大寫等名稱格式,客戶端傳對應參數即可獲取所需的格式,這一點在時間格式、長度格式、面積單位中非常實用。
-
-
Graphql 難點:
-
上手成本較高,需要理解相關概念
-
社區工具較多需要做好工具選型
-
需要產出符合當前業務的相關解決方案
-
二、框架選型
其他工具:
開發插件
-
Apollo client devtools
-
Altair graphql client
-
Apollo graphql
-
Graphql for vscode
-
vscode 插件(代碼高亮、智能補全、代碼跳轉等)
-
chrome 插件(Schema 查看、請求發起等)
請求代碼 & 類型生成
-
graphql-code-generator:通過配置文件,獲取 graphql 服務上的 schema 定義,生成適合 client、server 的 typescript 類型定義、gql 語句、請求方法,提升開發體驗
-
graphql 請求代碼生成能力集成到 CLI 工具,可通過 CLI 進行 graphql 相關請求的生成
接口請求
-
在 GMP 平臺進行請求查看和管理
-
前端使用 Apollo Link 接管 graphql 相關請求,並且配置相應的 link,onError link、Retry link 等
Schema 文檔
- 在 GMP 平臺進行 Schema defination 的編寫、查看和管理
提示 & 靜態檢查
- ESLint:eslint-plugin-graphql
三、相關流程
1、請求流程
Graphql 請求解析流程
-
HTTP 請求描述前端所需要的數據結構
-
請求到達服務端,解析器首先解析 query 下的第一個層 user,在該解析器內可以編寫對 user 的處理邏輯
-
解析器繼續深入 user 下的 id 字段,對 id 字段進行解析
-
解析器對與 id 平級的 name 字段進行解析
-
HTTP 響應返回處理後的結果
前後端協作完整請求響應流程:
-
前端客戶端:主要進行 Graphql 請求的發起,並且對請求的響應進行處理
-
請求網關層:網關層進行請求權限的判斷,在網關層調用權限服務進行接口請求的權限判斷
-
GMP 服務層:該層主要指 GMP 平臺的相關服務,包括編排服務、數據模型管理服務;編排服務主要處理請求的服務編排階段,通過數據模型管理服務隊編排產物、數據模型進行存儲和管理;處理後的請求通過編排服務的數據源代理調用相關的下游服務
2、前端工作流
在新模式下前端工作流主要是如下三個階段:
- GMP 平臺配置階段:該階段內前端同學主要關注 模型 和 請求配置
-
關注現有模型是否可複用
-
根據模型創建符合業務的請求
-
對請求進行在線調試並配置相關權限
- 工程項目使用階段:該階段內前端同學主要關注 請求生成 和 本地調試
-
使用 GMP 平臺配套 CLI 工具 進行 Graphql 請求代碼、ts 類型生成
-
項目頁面中可直接引入生成好的 Graphql 請求方法
-
本地調試可使用 mock 能力進行調試
- 工程化測試 / 部署階段:該階段即爲前端項目的測試 / 部署流程,使用現有內部 CI/CD 平臺流水線進行發佈即可
3、完整業務流程
- 開發階段:該階段主要有以下兩個步驟
-
GMP 平臺配置與服務編排:通過在 GMP 平臺進行數據源管理、流程編排、Graphql 請求管理等,構建符合業務的數據模型
-
本地請求生成與調試:本地請求生成主要結合現有的 GMP 平臺配套 CLI 工具,通過獲取 GMP 平臺的數據,結合 graphql-code-gen 在工程下生成前端請求代碼與 ts 類型定義;研發同學在本地結合 mock 數據進行調試請求,完全做到前後端開發分離
- 運行時階段:運行時階段主要步驟如下
-
前端請求發起,攜帶權限相關信息
-
Loader 層鑑權 / 灰度邏輯處理
-
GMP 服務處理接口流程編排
-
請求下游服務
四、數據模型管理
搭建數據模型管理平臺 (GMP 平臺),統一前後端模型標準,建設業務模型管理方案,實現模型複用,建設服務編排能力。結合低代碼實現 API 快速編排,server 研發精力能聚焦在開發特定需求的專有模型,前端也基於通用的模型完成標準化的消費鏈路。
1、平臺功能
-
數據模型管理:模塊主要功能包括項目管理、數據源管理、Schema defination 管理,具體去做數據域的劃分,通過對 Graphql schema defination 的管理來實現數據模型的抽象和管理。
-
服務編排管理:通過對請求中的數據源進行編排,結合數據模型管理的 schema defination 來實現服務的編排管理,產出適合業務的 Graphql 請求和 schema 定義。
-
Graphql 請求管理:該模塊主要面向前端同學,前端同學可以在該模塊根據已有的服務編排 schema 定義構造具體的業務請求,可以在線進行接口調試、保存、編輯等,並且結合 cli 工具進行本地代碼生成。
2、平臺架構
3、技術方案
3.1 Graphql 編輯器
GMP 平臺中主要有兩個模塊需要使用 Graphql 編輯器,分別如下:
-
數據模型管理 - schema defination 錄入:需要對 schema defination 進行輸入、編輯等操作,因此該場景編輯器需要具備基本的 graphql schema defiantion 語法高亮、格式化等功能。
-
Graphql 請求管理 - 請求在線調試:該模塊功能下需要具備基本的 Graphql 請求構造、調試能力,可以配置請求參數與 header,並且可以查看請求返回結構等功能。
GraphQL 編輯器方案選型:
-
編輯器方案結論:
-
Schema defination 錄入編輯推薦基於 GraphQL Editor SDK 或者基於此進行二次開發
-
Graphql 請求管理調試 推薦使用 Graphiql 方案 搭配 graphiql-code-exporter 、graphiql-explorer 插件進行功能增強
3.2 後臺管理列表
平臺內管理列表相關的頁面有如下功能:
-
項目管理列表 + 詳情
-
數據源管理列表 + 詳情
-
Schema defination 管理列表 + 詳情
-
流程編排管理列表 + 詳情
-
請求管理列表 + 詳情
3.3 CLI 命令行工具
CLI 命令行工具主要是打通 GMP 平臺與前端項目,通過 CLI 工具可以直接基於 GMP 平臺數據在前端本地項目中進行 前端請求代碼生成與 TS 類型生成。
注意點:
-
請求信息的獲取: CLI 工具需要根據用戶的選擇或配置拉取對應的 GMP 平臺中的項目 / 模塊中的請求信息,請求中的 Fragment 片段需要在 CLI 中處理好,有如下兩種方式:
-
GMP 平臺中的請求中已包含 Fragment 片段信息,該 Fragment 是跟隨該請求進行,CLI 拉取時是跟隨請求信息一起獲取,拉到本地時 Fragment 片段信息與用戶在 GMP 平臺錄入時的請求 schema 信息是一致的
-
請求信息與 Fragment 信息分別獲取,拉到本地的請求中只有 Fragment 的使用信息,Fragemnt 再通過接口獲取生成到請求的統計目錄下管理
五、業務解決方案
1、請求鑑權
編排服務層通過 resolver,解析出 query 中的 scheme,scheme 管理到權限點,鑑權時用 scheme 唯一標識進行鑑權。
Graphql schema defination 中 auth keyword 的綁定規則如下:
-
對 query 請求進行 type 級別的 auth 權限點 keyword 綁定和關聯
-
對 mutition 請求則進行 root type 級別的 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、灰度策略
-
前 Graphql 鑑權方案採用在 schema defination 中的具體 type 上進行權限點綁定
-
內部權限平臺的灰度策略配置是基於權限點進行,只是在 GMP 場景下,權限點下關聯的不是具體的 restful 請求,而是相應的 Graphql 請求或者具體的 schema defiantion type 定義
-
用戶在內部權限平臺平臺進行權限點配置,權限點配置進行類型區分,比如:請求權限點、字段權限點;
-
用戶在 GMP 平臺對請求進行創建時可進行相應的權限點綁定動作
-
請求到達編排服務時進行正常的權限校驗,判斷用戶是否有對應的權限點即可
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 會優先通過以下方式查詢是否命中緩存:
-
進入 ROOT_QUERY
-
查詢是否有 getAnchors(page: 1) 對應的結果,得到 [{id: "Anchor:1", typename: "Anchor"}]
-
查詢是否有 Anchor:1 對應的數據
-
所有查詢均命中時則按對應結構將緩存中的數據拼裝爲正確的結構返回給前端(Normalization 過程)
3.2 緩存策略
和 redux 類似,apollo-client 的數據緩存也是響應式的。發起數據請求的 hooks 處會訂閱它所依賴的數據,當緩存中的數據更新時,依賴對應數據的 UI 會正確地更新到最新狀態。
與 redux 狀態管理方案不同之處在於:
-
apollo-client 將這部分狀態存儲和網絡請求緊密結合在了一起。這意味着前端同學不需要再在網絡請求和數據存儲之間編寫額外的模板代碼以及抽象封裝;
-
當發送數據更新 mutation 請求時,apollo-client 也能夠感知這一變化,並自動更新數據緩存而不需要編寫額外代碼;
-
apollo-client 通過 data object id 作爲唯一標識進行 normalization。並且支持對特定 type 進行自定義 id,來保證緩存唯一性;
問題點:
在所有查詢、更新操作中 apollo-client 緩存都表現良好,但是對於 create 和 delete 類的操作數據緩存表現在複雜需求時會大概率出現緩存更新出錯問題。apollo-client 提供了兩種方式用於解決 create 和 delete 類數據後的緩存更新:
-
直接讀寫緩存數據,將其修改爲正確值:通過使用 apollo client 提供的 cache 對象中的方法直接進行緩存更改,但在複雜業務場景下就需要前端維護一份和後端相同的業務邏輯,而部分複雜的邏輯判斷前端甚至無法實現,因此該方法不推薦,成本太高;
-
重新觸發數據獲取:在創建、刪除等請求發起後,apollo client 重新獲取獲取相關列表數據,更新緩存,但有時也可能出現緩存失效後發起多次無效請求的問題;
解決方案:
-
可以借鑑社區開源緩存方案:https://github.com/Yuyz0112/smart-cache,提供了簡單易用的緩存失效方案,能夠從緩存中刪除過期數據,並且能夠只重新獲取當前視圖的數據,惰性重新獲取被 UI 視圖依賴所依賴的活躍數據;
-
針對業務中常見的列表請求場景,如主播列表,可以合理利用緩存機制:
-
FactionID
-
AnchorID
-
UserID
-
利用 dataIdFromObject 自定義緩存 key 能力,針對特定類型的 id 設置緩存 key 格式
-
定義合理的 pagination 模式:offsetLimitPagination
-
Query 類型請求默認開啓緩存機制
-
Mutation 類型請求直接對對應的緩存進行修改,請求結束後再立即進行 refetch
-
安裝 Apollo client devtools chrome 插件可對緩存數據進行查看
4、複用
4.1 模型複用
-
錄入:模型也就是 Graphql schema defination,GMP 平臺中具備數據模型管理模塊,該模塊內可創建模型並且可選擇具體的業務類型,通過列表進行統一管理。
-
使用:模型的使用是在流程編排模塊,一個編排可以選擇多個模型,同一個模型可以用在多個編排場景
4.2 請求複用
GraphQL Fragments 是可以在多個 Query 和 Mutation 之間共享的一段邏輯
-
Fragment 定義維護在 GMP 平臺
-
流程編排使用時進行相應的自動導入
-
前端請求通過通過 GMP 內部平臺配套 CLI 工具進行代碼自動生成,對於研發其實是對這一步進行了屏蔽,前端同學直接進行導入使用,無需關心具體的片段類型
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、接口調試
研發鏈路中有兩個地方可以進行接口調試
-
GMP 平臺請求創建階段
-
該階段是通過在 GMP 平臺創建請求時,可對請求進行相關的調試操作
-
前端瀏覽器發起階段
-
結合 chrome 插件進行調試
-
插件內可展示相關的 graphql 請求 network
-
插件支持搜索相關的 query/mutition 請求
-
插件支持設置 mock 數據進行
6、錯誤處理
採用 Graphql 方式請求業務數據時,會出現不同的業務錯誤,需要根據不同的錯誤類型進行處理,能夠在發生錯誤時對用戶顯示適當的信息。錯誤類型包括:
-
Graphql 語法錯誤
-
error.grapphQLErrors
-
語法錯誤(不會返回數據)狀態碼:4xx
-
校驗錯誤(不會返回數據)狀態碼:4xx
-
解析器錯誤(仍然可以返回部分數據)狀態碼:200
-
Network 網絡請求服務錯誤
-
4xx/5xx 不會返回數據
錯誤返回格式
{
"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
}
錯誤處理方式
-
設置 apollo client 的錯誤處理策略
-
使用 apollo-link 進行相關錯誤請求配置
-
graphql 錯誤:onError link
-
網絡錯誤:retry Link
-
對 apollo-client 中 useQuery、useMutation 等請求相關的 hooks 進行封裝,獲取 error 信息時進行相應的標準化動作(toast 等)
7、監控報警
標準化前端監控報警上報,並對上報進行了類型劃分,由於使用 garphql 發起請求時採用固定的請求 path,因此無法通過 path 進行上報區分,因此可藉助現有監控 SDK 的自定義上報能力,上報 graphql 請求的 query name 進行請求業務上報。
-
結合前端監控 SDK 自定義上報能力
-
結合封裝後的 useQuery、useMutation 在相應的請求生命週期中進行相應的監控上報動作
-
通過 sdk 中 client.report 方法進行上報 可以把相關的上報信息通過 extra 字段上報
8、業務場景
8.1 工作臺
在實際項目中有些頁面需要加載大量數據,導致請求時間較長,用戶體驗差。比如首屏渲染 因爲首屏需要請求更多內容,通常情況下 比原來多了更多 HTTP 的往返時間 (RTT),這造成了白屏,如果白屏時間過長,用戶體驗會大打折扣。使用 gql 時可以分兩次進行請求
-
第一次:只獲取必要展示信息
-
第二次:獲取其餘信息
8.2 分頁
- 使用 fetchMore 函數
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
},
})}
/>
);
}
-
需要配合 InMemoryCache 來進行相關分頁緩存的配置
-
read 函數:讀取本地分頁緩存,可對緩存進行相應修改後返回
-
merge 函數:server 分頁數據 合併到本地分頁緩存中
-
可統一使用 @apollo/client/utilities 中的 offsetLimitPagination 函數進行緩存配置
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