GraphQL 技术浅析

背景

7月份咱们前端团队推进落地了一个 toB 类型的系统,因为服务端也由咱们前端工程师来承接,因此服务端技术选型上咱们有了话语权,API 这一起咱们选择了 GraphQL 。本文将阐述我学习 GraphQL 这门技术的一些思考。javascript

GraphQL 在解决什么问题

学习一门新技术,首先要把问题域弄清楚。社区有大量 GraphQL 与传统 API 解决方案(含 REST API)对比文章,总结下来,传统 API 存在如下问题:html

  • 接口数量众多维护成本高:接口的数量一般由业务场景的数量决定,为了尽可能减小接口数量,服务端工程师一般会对业务作抽象,首先构建粒度较小的数据接口,再根据业务场景对数据接口进行组合,对外暴露业务接口,即使这样,服务端对前端暴露的接口数量仍是很是多,由于业务老是多变的。
  • 接口扩展成本高:出于带宽的考虑移动端咱们要求接口返回尽可能少的字段,PC 端一般要展示更多字段;考虑首屏性能,咱们又要求对接口作合并;传统 API 应对这些需求,先后端都面临改造,成本较高。
  • 接口响应的数据格式没法预知:因为接口文档几乎老是不能及时更新,前端工程师没法预知接口响应的数据格式,影响前端开发进度。

针对以上问题,GraphQL 给出了较为完善的解决方案。前端

GraphQL 如何解决问题

接下来我经过一个实例讲解 GraphQL 解决问题的思路,客户端的述求:根据性别查询团队成员列表,返回 idgendernamenickName ,GrahpQL 的处理过程以下图: java

image.png

请求参数在发送到服务端以前会先通过 GraphQL Client 转换成客户端 Schema,这段 Schema 实际上是一段 query 开头的字符串,描述了客户端的对数据的述求:调用哪一个方法,传递什么样的参数,返回哪些字段。服务端拿到这段 Schema 以后,经过事先定义好的服务端 Schema 接收请求参数并执行对应的 resolve 函数提供数据服务。整个过程能够想象成咱们吃自助餐的过程,服务端 Schema 就比如自助餐线,摆上咱们能提供的全部食物;客户端 Schema 就描述了咱们想要吃的食物,按需获取就行了。node

讲到这里,好奇心强的同窗可能已经开始思考这个问题了:客户端 Schema 本质上就是一段字符串,服务端如何识别并响应这段字符串?react

graphql-js

识别与响应客户端 Schema 依赖于官方类库 graphql-js ,服务端拿到客户端 Schema 字符串后会作以下处理: webpack

image.png

  • 解析阶段 为了识别客户端 Schema, graphql-js 定义了一系列的特征标识符:
export const TokenKind = Object.freeze({
    BANG: '!',
    DOLLAR: '$',
    PAREN_L: '(',
    PAREN_R: ')',
    SPREAD: '...',
    COLON: ':',
    EQUALS: '=',
    BRACKET_L: '[',
    BRACKET_R: ']',
    ...
});
复制代码

并定义了 AST 语法树规范,规定语法树支持如下节点:ios

/** * The set of allowed kind values for AST nodes. */
export const Kind = Object.freeze({
  // Name
  NAME: 'Name',

  // Document
  DOCUMENT: 'Document',
  OPERATION_DEFINITION: 'OperationDefinition',
  VARIABLE_DEFINITION: 'VariableDefinition',
  VARIABLE: 'Variable',

  // Values
  INT: 'IntValue',
  FLOAT: 'FloatValue',
  STRING: 'StringValue',
  BOOLEAN: 'BooleanValue',
  ...
});
复制代码

有了特征字符串与 AST 语法树规范,GraphQL Server 对客户端 Schema 进行逐字符扫描(charCodeAt),最终解析阶段的产出物为 document ,上文示例中的客户端 Schema 解析完成以后的部分 documentgit

{
  "kind":"Document",
  "definitions":[
  {
    "kind":"OperationDefinition",
    "operation":"query",
    "name":{
      "kind":"Name",
      "value":"DisplayMember",
      "loc":{
        "start":13,
        "end":26
      }
    },
    "selectionSet":{
      "kind":"SelectionSet",
      "selections":[
        {
          "kind":"Field",
          "alias":null,
          "name":{
            "kind":"Name",
            "value":"fetchByGender",
            "loc":{
              "start":37,
              "end":50
            }
          },
          "arguments":[
            {
              "kind":"Argument",
              "name":{
                "kind":"Name",
                "value":"gender",
                "loc":{
                  "start":51,
                  "end":57
                }
              },
              "value":{
                "kind":"StringValue",
                "value":"M",
                "loc":{
                  "start":59,
                  "end":62
                }
              },
              "loc":{
                "start":51,
                "end":62
              }
            }
          ],
...
复制代码

若是客户端 Schema 不符合服务端定义的 AST 规范,解析过程会直接抛出语法异常 Syntax Error ,拿上文的示例举例,我将客户端 Schema 中的 fetchByGender(gender: "M") 改成 fetchByGender(gender) ,只传递参数名,不传递参数值,则服务端会响应:github

{
    "errors":[
        {
            "message":"Syntax Error GraphQL request (3:29) Expected :, found )

2: query DisplayMember {
3: fetchByGender(gender) {
^
4: list {
",
            "locations":[
                {
                    "line":3,
                    "column":29
                }
            ]
        }
    ]
}
复制代码

结构化的报错信息也是 GraphQL 的一大特色,定位问题很是方便。只要语法没问题解析阶段就能顺利完成,而后进入校验阶段。

  • 校验阶段

校验阶段用于验证客户端 Schema 是否按照服务端 Schema 定义好的方式获取数据,好比:获取数据的方法名是否有误,必填项是否有值等等,校验范围一共有几十种,我没有办法一一举例。拿上文的示例举例,我将客户端 Schema 中的 fetchByGender 改成 fetchByGenfetchByGen 在服务端根本没有定义,则服务端会响应:

{
    "errors":[
        {
            "message":"Cannot query field "fetchByGen" on type "Query". Did you mean "fetchByGender"?",
            "locations":[
                {
                    "line":3,
                    "column":9
                }
            ]
        }
    ]
}
复制代码

不只返回结构化的报错信息,还很是人性化的告诉你正确的调用方式是什么。校验阶段经过以后会进入执行阶段

  • 执行阶段

执行阶段依赖的输入为:解析阶段的产出物 document ,服务端 Schema;其中 document 准确描述了客户端对数据的述求:请求哪一个方法,参数是什么,须要哪些字段;服务端 Schema 描述了提供数据的方式;拿上文的示例举例,服务端 Schema 须要这样定义:

const graphqlApi = require('graphql');
const {
  GraphQLObjectType,
  GraphQLList,
  GraphQLNonNull,
  GraphQLSchema,
  GraphQLString,
} = graphqlApi;

const dataSource = require('./dataSource');

const memType = new GraphQLObjectType({
  name: 'Male',
  description: 'A member gender is Male.',
  fields: () => ({
    id: {
      type: new GraphQLNonNull(GraphQLString),
      description: 'The id of member',
    },
    name: {
      type: GraphQLString,
      description: 'The name of the character.',
    },
    nickName: {
      type: GraphQLString,
      description: 'The nickName of the character.',
    },
    gender: {
      type: GraphQLString,
      description: 'The gender of the character.',
    },
    list: {
      type: new GraphQLList(memType),
      description: 'The mems list by gender.',
    },
  })
});

const queryType = new GraphQLObjectType({
  name: 'Query',
  fields: () => ({
    fetchByGender: {
      type: memType,
      args: {
        gender: {
          description: 'gender of the human',
          type: new GraphQLNonNull(GraphQLString),
        },
      },
      resolve: (root, { gender }) => {
        // 访问数据库或三方 API 查询成员列表
        return {
          list: dataSource.getMembers(gender),
        };
      },
    },
  }),
});

module.exports = new GraphQLSchema({
  query: queryType,
  types: [memType],
});
复制代码

执行服务端 Schema 中的 resolve 函数,获得执行阶段的输出:

{
    "data":{
        "fetchByGender":{
            "list":[
                {
                    "id":"1",
                    "gender":"M",
                    "name":"童开宏",
                    "nickName":"慕冥"
                }
            ]
        }
    }
}
复制代码

固然要完成服务端 Schema 的定义,你须要学习 GraphQL 的 类型系统 ,你们翻阅 API 文档便可。

技术边界

原理弄清楚以后咱们须要对 GraphQL 这门技术的边界有一个清醒的认识:

  • 客户端边界:核心能力是将请求参数按照服务端定义好的 AST 语法树规范拼装成客户端 Schema 字符串,实现方案你们可参考apollo提供的 Webpack 插件 ,固然也有一些 GraphQL 客户端连发送 Ajax 请求的活儿也干了,无非是在底层调用其余类库好比 axios 发请求。

  • 服务端边界:核心能力是识别客户端 Schema 字符串,并经过服务端 Schema 调用底层的数据服务按需返回用户想要的数据,至于底层数据源来自哪里(数据库或者三方接口),以何种方式获取数据(直连数据库或者 ORM 方法调用),这些不属于 GraphQL 关心的范畴。

问题解决的怎么样

因为 GraphQL 经过客户端 Schema 而不是经过 URL 描述数据述求,因此理论上服务端只须要对客户端暴露一个地址便可,解决了接口数量众多维护成本高的问题;同时,服务端提供的是全量字段,客户端可按需获取,面对接口扩展的需求,服务端没有开发成本;最后,经过 GraphiQL 可视化调试界面展示服务端能提供的全部数据,开发过程再也不依赖接口文档:

image.png

GraphQL 社区在忙什么

GraphQL 官方提供核心能力:

  • graphql-js :GraphQL 理念的 JavaScript 实现,该类库可同时运行在浏览器环境与 Node 环境,该类库的原理我在上文中已经讲过了。
  • graphiql :提高调试体验,我在上文中提过。
  • dataloader :提高性能,经过合并请求尽可能减小数据库查询次数。
  • Relay :前端框架,使 GraphQL 与 React 很好的融合在一块儿,嵌入性较强,须要 GraphQL Server 配合。

咱们还缺什么?

  • 服务端 官方只提供了 JavaScript 语言支持,社区爱好者很快在不一样编程语言中实现了 GraphQL 的理念:JAVA.NET 等等,更多语言支持,请查看 官网

  • 客户端 官方提供的 Relay 解决了 GraphQL 与 React 相结合的问题,Apollo Client 提供了与其余前端框架融合的解决方案,好比 Vue、Angular 等等。

  • 开发体验

    • graphql-tools :在上文示例代码的服务端 Schema 中,咱们将类型的定义(typeDefs)与处理函数的定义(resolvers)放在同一个文件中,职责上不够单一,借助 graphql-tools 咱们能够将两者分不一样的文件定义;

    • egg-graphql :与 Node 框架 egg 相结合,制定 目录规范 并提供语法糖提升开发效率;

总结

GraphQL 的优势上文已经讲过了,真的是从业务痛点出发,解决了传统 API 存在的问题,可是 GraphQL 在解决问题的同时也带了一些新的问题,这些问题在某种程度上阻碍了这门技术的普及:

  • 数据库性能:GraphQL 将数据描述成一张巨大的网,理论上客户端 Schema 能够写出任意嵌套层级的查询语句,好比:
query IAmEvil {
  author(id: "abc") {
    posts {
      author {
        posts {
          author {
            posts {
              author {
                # that could go on as deep as the client wants!
              }
            }
          }
        }
      }
    }
  }
}
复制代码

这样的查询语句会给数据库带来很大的性能开销,服务端不得不作 限流 来规避这样的问题,这也带来了额外的开发成本。

  • 侵入性:GraphQL 受益最大的是前端,却须要服务端鼎力支持,特别是老系统迁移,服务端与前端都面临较大的改造。
  • 学习成本:GraphQL 是一套全新的理念,须要先后端同窗都学习新的知识才能掌握这门技术,这也带来较大的学习成本。

任何技术都有利弊,你们要结合本身的场景权衡收益作出适合本身的技术选型。

参考文档

文章可随意转载,但请保留此 原文连接。 很是欢迎有激情的你加入 ES2049 Studio,简历请发送至 caijun.hcj(at)alibaba-inc.com

相关文章
相关标签/搜索