【译】更优秀的GraphQL官方中文文档-客户端如何使用

2,988 阅读10分钟
  • 原文档地址:graphql.org/learn/
  • 文档翻译时间:2019年/3月/20日
  • 译者:贺瑞丰(深度使用过GraqhQL)
  • 目的:提供更接地气的中文说明,还有笔者自己的使用经验 plus:其他的翻译太烂啦

简介

  • 客户端:GraphQL是一种查询语言(for API),即发送Graphql请求
  • 服务器端:在执行时(runtime)使用你预先为你的数据定义好的类型系统(Type System)来处理客户端发送过来的Graphql请求。

它并没有指定特定的数据库或者存储器,完全靠你现有的代码和数据支撑

建立一个GraphQL service 需要定义types和内部的fields,然后还需要一个解析函数来处理这些types.

举例:一个GraphQL service会如下所示 定义types 和 fields

//  who the logged in user is (me) 
type Query {
  me: User
}
// 针对User的查询
type User {
  id: ID
  name: String
}

服务器端需要对每一个Field解析

function Query_me(request) {
  return request.auth.user;
}

function User_name(user) {
  return user.getName();
}

一旦一个GraphQL Service 启动了,就可以接受GraphQL queries并检验和执行。先保证只处理预定好的types-fileds有关的query,然后执行解析函数并返回结果,示例如下

//发送如下query
{
  me {
    name
  }
}
//得到如下json
{
  "me": {
    "name": "Luke Skywalker"
  }
}

Queries and Mutations(查询和修改)

原文地址:graphql.org/learn/queri…

Fields(字段)

最简情况下,Graphql会向对象上请求特定的字段,让我们从一个简单的例子开始

  • Query
    {
      hero {
        name
      }
    }
    
  • Result
      {
      "data": {
          "hero": {
              "name": "R2-D2"
          }
        }
      }
    

你可以立即发现上面的query和result的结构是一致的。这是GraqhQL的特性——以为你总是获取到你想要的,并且服务端精确地知道客户端想要哪些字段。

name 字段返回了一个 String类型,在上例中也就是星球大战中的hero("R2-D2")

在上面的例子中,我们仅仅是查询了hero的名字并得到了一个String,但是fields不仅仅可以是类似name这样一个变量的形式,也可以是一个Objects,这样你就可以对“hero”中的字段进行次级选择(sub-selection ),GraphQL queries可以遍历相关的Objects-Fields,从而使客户端可以在一次请求中获取大量的相关数据。往往我们在REST架构下可能会做多次的往返请求才能实现上述效果。下面是一个例子

  • Query

    {
      hero {
        name
        # Queries can have comments!
        # friends 就是上述所说的次级选择,实现了相关查询 🐂🍺
        friends {
          name
        }
      }
    }
    
  • 返回结果

    {
      "data": {
        "hero": {
          "name": "R2-D2",
          "friends": [
            {
              "name": "Luke Skywalker"
            },
            {
              "name": "Han Solo"
            },
          ]
        }
      }
    }
    

    注意,friends字段返回了一个数组。GraphQL queries会等同的对待单个的items或者列表,然而我们可以通过schema中推导出我们期望返回是哪种类型。

    Arguments(参数)

    如果仅仅是做可以遍历相关对象及其字段的话,GraqhQL已经在获取数据上非常有用了。但是当你在查询时可以接受参数时,事情就会变得更加有趣了!

    在REST风格的系统中,你只能传递一个参数集合--也就是http请求中的查询参数与URL段。但是在GraphQL中,每一个字段或内嵌对象都可以获取一个参数集,这让GraphQL彻底的取代了执行多次API获取这种方式。你甚至可以向 scalar fields传参,来实现在服务端一次性的数据转换而不是分别在每个客户端做。

  • Query

    {
      human(id: "1000") {
        name
        # 这里对高度做了传参,要求单位是英尺
        height(unit: FOOT)
      }
    }
    
  • 返回结果

    {
      "data": {
        "human": {
          "name": "Luke Skywalker",
          # 返回了英尺高度 🐂🍺
          "height": 5.6430448
        }
      }
    }
    

参数可以有很多类型。在上例中,我们使用了一个枚举类型(Enumeration type),即一个有限的选择的集合(set),也就是 米 或者 英尺 等。 GraphQL默认内置了一个类型集, as long as they can be serialized into your transport format.

Read more about the GraphQL type system here.

Aliases(别名)

如果你敏锐的话,你可能已经发现了,既然返回的结果中的对象字段与query中的相关字段是匹配的但是并不包含参数,那么你就不可以针对同一个字段通过传输不同的参数来获取不同的结果了。这就是我们需要别名 aliases的原因。

译者注:当你需要针对某个字段通过不同的参数在一个Query中完成所有的数据获取时,你就需要用到别名了

  • query
# 星球大战北京。。。。 帝国 绝地武士,关注这里的别名就行了
{
  empireHero: hero(episode: EMPIRE) {
    name
  }
  jediHero: hero(episode: JEDI) {
    name
  }
}
  • result
{
  "data": {
    "empireHero": {
      "name": "Luke Skywalker"
    },
    "jediHero": {
      "name": "R2-D2"
    }
  }
}

译者注 :如果没有别名,你不可能在一个request对hero这个字段获取到两种数据的,所以通过别名传递不同的参数来获得两份数据,并且在返回的结果中也不是hero了,而是替换成了 empireHero 和 jediHero,这样也方便了客户端的处理。

Fragments(片段):可复用单元

加入我们App有一个复杂的页面,在其中两种hero的阵营分别占据页面的一边,你可以发现query一下子就复杂起来了,因为我们会重复的去声明某些字段。

这也就是为什么GraphQL包含了 fragments 这种可复用, fragments 让你可以构建一个字段集并且在query中不停的复用。下面是例子

  • query
{
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  appearsIn
  friends {
    name
  }
}
  • result
{
  "data": {
    "leftComparison": {
      "name": "Luke Skywalker",
      "appearsIn": ["NEWHOPE", "EMPIRE","JEDI"],
      "friends": [
        {  "name": "Han Solo"},
        {  "name": "Leia Organa" },
      ]
    },
    "rightComparison": {
      "name": "R2-D2",
      "appearsIn": ["NEWHOPE","EMPIRE","JEDI"],
      "friends": [{  "name": "Luke Skywalker" } ]
    }
  }
}

你可以发现如果没有fragments 的话,那些字段会重复的出现。片段的概念在将复杂的应用数据切分的时候经常被使用到,特别是当你有多个UI组件(有不同的片段)在初始化数据获取时

在片段中使用变量

这里用法太简单了请翻看官方代码:graphql.org/learn/queri…

Operation name(操作名)

到现在为止,我们已经使用过了query简写的语法糖,我们省略了 query 关键字 和 query的名称,但是在生产型的项目中指定query的名称是很有必要的。

下面是一个完整的例子:query 关键字作为一个 **operation type **, 然后 HeroNameAndFriends 作为操作名

query HeroNameAndFriends {
  hero {
    name
    friends {
      name
    }
  }
}

这种 operation type 既不是 query,mutationm 也不是 subscription, 它仅仅的描述了你想做哪种类型的操作。这非常有必要,因为你使用省略语法糖的话你就不能给你的查询操作传递变量了

译者注:变量的定义都是放在了操作名之后的括号中的,如果省略了操作名,就不能传变量给query了

给你的query命好名字,在 debug 和服务器打 log 的时候非常有用。举个JavaScript的例子,如果你老是喜欢用匿名函数,那么当这个函数报错的时候,debug的时候就很难受。具名的query会让你非常容易跟踪

Variables(变量)

迄今为止,我们都是在query中将参数用字符串写死的,但是在大多数情况下,这些参数都是动态的。举个例子:你可能会有一个下拉框来选择一个 Star Wars episode ,并获取到其值,传递给 query 作为查询参数。

!!请不要这样做!!,直接在你的 query 代码中 嵌入动态的变量,这样虽然在 runtime 的时候变量会被解析成相应的字符串,然后被 serialize 转化成 GraphQL-specific format。 !! 应该这样 !!,GraphQL在处理外部传输过来的动态值的时候可以像对待一等公民的处理方式,并且像一个单独的 dictionary.

要使用变量,需要下面三个步骤

  • 将 query 中的静态值替换成 $variableName
  • 在 query 操作名之后的括号中声名该变量
  • 给变量一个单独的,大多数情况下JSON格式的 变量表,还要指定变量的类型。
query HeroNameAndFriends($episode: Episode) {
  # episode参数接受一个$episode变量,而变量在query操作名中被提前定义好了,并且要指定$episode变量的类型
  # 本例中变量类型是 Episode
  hero(episode: $episode) {
    name
    friends {
      name
        }
      }
    }
// 变量
这里就是为什么上面叫 dictionary的原因,通过episode字段可以来匹配
{
  "episode": "JEDI"
}

现在,我们可以动态的传递episode的值给query,而不是重复的定义多个静态的query(译者注:如何传递变量给query现在还没有涉及到),再次强调,上述的用法是官方推荐,不要在query中采用插值的方式来构建query

译者注:比如我之前在JavaScript中经常这么干

  const {episode} = outerParameter
  # 采用ES6 插值字符串的方式来构建
  hero(episode: `${episode}`) {
    name
    friends {
      name
        }
      }
    }

Directives (指令)

我们在上面讨论了如何使用变量来避免手动的采用字符串插值的方法构建动态的查询。在参数中使用变量确实解决了一大类问题。然而我们会需要动态的去调整我们整个query的结构,同样的使用变量的方式。 举个例子:我们的 UI 组件有一个汇总和细节两个视图,其中一个比另一个包含的字段要多很多。

query Hero($episode: Episode, $withFriends: Boolean!) {
  hero(episode: $episode) {
    name
    friends @include(if: $withFriends) {
      name
    }
  }
}
# 变量
{
  "episode": "JEDI",
  "withFriends": false
}
# result
{
  {
  "data": {
    "hero": {
      "name": "R2-D2"
    }
  }
}
}

上述例子就是使用来了特性 directive

directive:可以给附加个一个字段或者 fragment,服务端会根据传递的变量值来处理你的query。

在 GraphQL 标准的核心中写入了两种 directive,按照标准实现的后端服务中都应该支持这两种 directive

  • @include(if: Boolean),只有传递true时,才会返回对应的结果
  • @skip(if: Boolean) 传递true时,不会返回对应结果

译者注:大多数情况下,你并不会用到它,只有当你发现你的query构建的非常复杂时,记得过来看看上述指令是否能帮助到你。

Mutations(变更)

REST 中,一个会对服务器有副作用的请求,不会使用 GET 方法(HTTP Request Method). GraphQL要简单点-技术上来说任何query都可以被时限为数据写入。然而任何对于数据的修改(新建,修改,删除)按照惯例来说都通过 mutation来完成是很有用的。

和 query 一样,如果 mutaion 也返回一个对象,你也可以获取内部的字段,用法几乎和query是一样的。

mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}
# 变量
{
  "ep": "JEDI",
  "review": {
    "stars": 5,
    "commentary": "This is a great movie!"
  }
}
# 返回值
{
  "data": {
    "createReview": {
      "stars": 5,
      "commentary": "This is a great movie!"
    }
  }
}

你可能发现了,我们给review传递的参数比较特殊,是一个object type,意味着它可以接受一个对象,而不仅仅是一个 scalar type

Multiple fields in mutations

query 和 mutation有一点特殊的不同。 query的字段是并行执行的,而mutation的字段是串行的,一个接一个有序的进行的。 这就意味着如果我们在一个请求中发起了两个 incrementCredits mutation的操作的话,第一个请求会首先被返回,确保我们自己的应用对race condition的考虑。

译者注:当你的业务比较复杂时,你的页面可能有很复杂的异步请求,这个时候不仅仅要考虑到客户端请求的发出顺序,还要考虑到服务器对每个请求的返回顺序。

Inline Fragments(内联片段)

类似于其他类型系统,GraphQL schemas 也支持接口和集合类型

如果你想查询的字段返回的是一个接口或者集合类型,那么你就需要用到内敛片段来获取实际数据,如下例:

  • request
    query HeroForEpisode($ep: Episode!) {
      hero(episode: $ep) {
        name
        ... on Droid {
          primaryFunction
        }
        ... on Human {
          height
        }
      }
    }
    // 变量
    {
        "ep": "JEDI"
    }
    
  • result
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "primaryFunction": "Astromech"
    }
  }
}

在上述 query中, hero 的字段返回了一个 Character 类型,依赖于episode参数来返回 Human or Droid,如果没有内联片段你就只能查询Character类型内置好的字段,比如name

Meta fields

使用内联片段的时候,如果没有 __typename这种元字段标明数据的来源,客户端无法分辨数据,看下面的例子你就明白了

  • query
    {
      search(text: "an") {
        __typename
        ... on Human {
          name
        }
        ... on Droid {
          name
        }
        ... on Starship {
          name
        }
      }
    }
    
  • result
    {
      "data": {
        "search": [
          {
            "__typename": "Human",
            "name": "Han Solo"
          },
          {
            "__typename": "Human",
            "name": "Leia Organa"
          },
          {
            "__typename": "Starship",
            "name": "TIE Advanced x1"
          }
        ]
      }
    }
    

译者注:没有标志的话,那么返回的数据是下面这样的?你完全没法处理

{
  "data": {
    "search": [
      {
        "name": "Han Solo"
      },
      {
        "name": "Leia Organa"
      },
      {
        "name": "TIE Advanced x1"
      }
    ]
  }
}

GraphQL 提供了一些元字段,参看下面的链接

!!! 全文完 !!!

文档的Server端的翻译也做好了:juejin.cn/post/684490…