作者简介: 薛扬波,来自抖音直播平台前端团队,团队负责主播工会等行业产品研发,以及运营平台、数据策略平台建设
Graphql优点:
Graphql难点:
其他工具:
Graphql请求解析流程上图中从 1 到 5 展示了GraphQL一个请求到响应的过程,5个步骤分别如下:
前后端协作完整请求响应流程:整体的请求流程时序图分为三个部分:
在新模式下前端工作流主要是如下三个阶段:
业务流程分为两阶段:
搭建数据模型管理平台(GMP 平台),统一前后端模型标准,建设业务模型管理方案,实现模型复用,建设服务编排能力。结合低代码实现API快速编排,server 研发精力能聚焦在开发特定需求的专有模型,前端也基于通用的模型完成标准化的消费链路。
平台主要由三部分组成:
GMP平台中主要有两个模块需要使用Graphql 编辑器,分别如下:
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 不推荐 |
平台内管理列表相关的页面有如下功能:
CLI 命令行工具主要是打通 GMP 平台与前端项目,通过 CLI 工具可以直接基于 GMP 平台数据在前端本地项目中进行 前端请求代码生成与 TS 类型生成。
注意点:
编排服务层通过 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)
}
缓存不仅可以让前端在运行时变得更加高效,还可以极大地提升开发效率,并且减少各类数据不一致问题引发的 bug。与其它 API 规范相比,GraphQL 和前端缓存的结合可以让这些优势再次被放大。
# 列表查询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 会优先通过以下方式查询是否命中缓存:
和 redux 类似,apollo-client 的数据缓存也是响应式的。发起数据请求的hooks处会订阅它所依赖的数据,当缓存中的数据更新时,依赖对应数据的 UI 会正确地更新到最新状态。
与redux状态管理方案不同之处在于:
问题点:
在所有查询、更新操作中 apollo-client 缓存都表现良好,但是对于create和delete类的操作数据缓存表现在复杂需求时会大概率出现缓存更新出错问题。apollo-client 提供了两种方式用于解决create和delete类数据后的缓存更新:
解决方案:
GraphQL Fragments是可以在多个Query和Mutation之间共享的一段逻辑
定义规则
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
}
`;
// 直接引入使用
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
}
}
}
`;
研发链路中有两个地方可以进行接口调试
采用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
}
错误处理方式
标准化前端监控报警上报,并对上报进行了类型划分,由于使用 garphql 发起请求时采用固定的请求 path,因此无法通过 path 进行上报区分,因此可借助现有监控 SDK 的自定义上报能力,上报 graphql 请求的 query name 进行请求业务上报。
在实际项目中有些页面需要加载大量数据,导致请求时间较长,用户体验差。比如首屏渲染 因为首屏需要请求更多内容,通常情况下 比原来多了更多HTTP的往返时间(RTT),这造成了白屏,如果白屏时间过长,用户体验会大打折扣。使用gql时可以分两次进行请求
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
},
})}
/>
);
}
轮询场景时可以对该轮训请求禁用缓存策略,保证每次轮训的接口都是最新的数据
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,
})