基于 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 编辑器方案选型:
方案 | 开源协议 | 优缺点 |
---|---|---|
GraphQL | MIT | 轻量的GraphiQL浏览器,查询Shema 文档,请求发送,可以方便地fork出来做二次开发,不支持 Shema 定义能力 |
GraphQL Editor | MIT | 基于monaco-editor UI好看,schema 编辑 schema关系、query 请求、graph 节点查看,LSP能力较好,依赖的核心包也是在同一个github帐号下开源的 同时有收费服务 |
Insomnia | MIT | 类似PostMan的 支持多种类型接口请求(Graphql Restfull RPC) 支持windows mac ,不支持 Shema 定义能力 |
graphql-playground | MIT | 基于GraphiQL二次开发,增强了一些能力 比如主要是是使用体验 query debug |
Apollo-studio | 整体体验比较好,支持定义schema。请求管理 开源不完整,embeddable-explorer 支持第三方开发接入,使用iframe嵌套了 studio.apollographql.com 不推荐 |
编辑器方案结论: 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,
})