GraphQL 入门与实践

2,040 阅读3分钟

概述

GraphQL 是一种新的 API 标准,由 Facebook 开发并开源。

Facebook 在 2012 年就在他们移动端应用中使用上了 GraphQL,第一次公开发布是在 2015 年的 React Conf 上。其实不仅 Facebook,其他公司也有在探索相关类似的技术,Netflix 也曾推出他们的方案 Falcor,Coursera 在 Facebook 推出 GraphQL 后,取消了相关研发,直接使用 GraphQL。

为什么会有这么多公司都在研发相关技术,让我们先看 GraphQL 是如何使用的,再看看它和现在的 RESTful api 的对比。

GraphQL 入门示例

我们将使用 apollo-server 来创建 GraphQL 示例,或者你也可以使用 express-graphql。示例来源 Apollo 官方,地址

一个 GraphQL 服务是通过定义类型和类型上的字段来创建的,所以我们先定义类型和字段

const { ApolloServer, gql } = require('apollo-server');

// 定义了一个 Book 类型,后续我们可以在 Query 中使用
const typeDefs = gql(`
    # 定义了一个 Book 类型,后续我们可以在 Query 中使用
    type Book {
        title: String
        author: String
    }
    
   # Query 类型是特殊,它列出所有客户端可查询的字段
    type Query {
        books: [Book]
    }
`)

接着我们需要处理对应的字段解析函数,这边我们需要处理 books 是怎么获取数据的,最后返回一定是一个 Book 类型数组。

const books = [{
    title: 'The Awakening',
    author: 'Kate Chopin',
}, {
    title: 'City of Glass',
    author: 'Paul Auster',
}];

const resolvers = {
    Query: {
        books: () => books,
    }
};

最后创建 ApolloServer 并启动,一个 GraphQL 服务就跑起来了。

const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => {
    console.log(`🚀 Server ready at ${url}`);
});

本地启动服务后,你可以访问 http://localhost:4000/ 并进入 GraphQL playground 来体验(或者使用 apollo 官方 demo 体验)。根据定义的类型和类型上的字段,我们编写对应的查询语句即可获取到想要的数据。你也可以试着只返回 book 的 title 试试。

image.png

从上面例子就可以体验到 GraphQL 的使用并不复杂,并且可以查询自己想要的数据。

GraphQL 优点和缺点

Facebook 设计出 GraphQL 是为了处理 RESTful API 的一些局限性, 所以我们先看下它的优点,再看 GraphQL 自己的局限性。

优点

  1. Overfetching & Underfetching

Overfetching 很好理解,平时我们用 RESTful API 请求数据的时候,接口往往会返回很多字段,有些字段是我们不需要的。而 GraphQL 可以指定要返回什么数据,就不会出现数据冗余。Fackbook 最先也是在移动端应用上使用 GraphQL 的,没有数据冗余对于移动端的访问来说可以更快的展示页面。

Underfetching 指请求的返回的数据信息不够,这样导致我们需要发多个请求去获取数据。比如一个页面需要显示用户信息,包括基本信息和用户文章。这个需求对于 RESTful API 来说通常需要两个接口去获取,一个获取基本信息,一个获取用户的文章。但是对于 GraphQL 来说,一个请求就搞定,指定基本信息和文章的字段,接口自然返回相关数据。

  1. 快速产品迭代

RESTful API 在产品需求迭代中,很可能发生接口数量和接口 url 的变更,这些变更的影响范围可能会比较大;而 GraphQL 的接口只有一个,前端就只需关心自己想要的数据,可以更快速的进行产品迭代。

  1. 灵活而强类型的 Schema

GraphQL 是强类型的,查询基于字段及其关联的数据类型。如果 GraphQL 查询中存在类型不匹配,则服务端将返回明确且有用的错误消息。同时通过 Schema,前端和后端的工作可以各自开发,最后在联调。

  1. 内省分析

GraphQL 支持通过内省机制可以知道它支持哪些查询,这对于 GraphQL IDE 工具 非常友好,我们在上面的例子中就可以体验到。

缺点

  1. HTTP 缓存

由于 GraphQL 在 HTTP 上使用方式为单个端点中的 POST 请求,POST 请求的数据结构又是不固定的,所以无法在 HTTP 层上做数据缓存。社区是有提供一些客户端级别缓存方案,比如 Apollo Client,但是会增加开发成本。

  1. HTTP Status

正常情况下 GraphQL 只会返回 Status Code 200,无论当前数据请求是成功或失败,这样传统方法的 HTTP 状态判断和逻辑就无法使用,虽然开发者可以自定义一套错误处理逻辑,但也增加了复杂度。

返回 200 具体错误在 errors 中

{
  "errors": [
    {
      "message": "Field "name" must not have a selection since type "String" has no subfields.",
      "locations": [
         {
            "line": 31,
            "column": 101
         }
      ]
    }
 ]
}
  1. 不可预测的执行

GraphQL 的本质是你可以查询组合你想要的任何字段,但这种灵活性不是免费的。有一些问题值得了解,例如性能和 N+1 查询。

GraphQL 使用 resolves 去获取字段的数据,每个字段都有执行 resolves,这就会对性能有一定的影响。同时如果查询中存在很深的嵌套,服务端需要一个机制去阻止这个性能昂贵的查询。

  1. 处理文件上传

GraphQL规范中没有关于文件上传的内容,并且不接受文件类型参数

Learn GraphQL

这个主要介绍 GraphQL 查询能力,对于服务端的 Schema 定义和实现,有兴趣的同学可以根据这个教程进行学习,How to GraphQL

  • 参数 GraphQL 支持在请求时传递参数,每个字段都可以设置参数(服务端支持的情况下)。
{
  human(id: "1000") {
    name
    height(unit: "cm")
  }
}
  • 别名 当你想通过不同参数来查询相同字段就需要使用别名功能,比如下面的 hero 查询,在不使用别名的情况下你就无法构造出这个请求。
{
  empireHero: hero(episode: EMPIRE) {
    name
  }
  jediHero: hero(episode: JEDI) {
    name
  }
}
  • 变量 查询语句支持变量,使用 $ 符定义变量,并在发起请求时传入变量。
# { "graphiql": true, "variables": { "episode": JEDI2 } }
query HeroNameAndFriends($episode: Episode = "JEDI") {
  hero(episode: $episode) {
    name
    friends {
      name
    }
  }
}
  • 片段 片段使你能够组织一组字段,然后在需要它们的地方引入,同时片段内也可以使用变量
query HeroComparison($first: Int = 3) {
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

# 定义了一个片段,并在上面查询中使用
fragment comparisonFields on Character {
  name
  # 使用了 HeroComparison 的变量
  friendsConnection(first: $first) {
    totalCount
    edges {
      node {
        name
      }
    }
  }
}
  • 内联片段 你查询的字段返回的是接口或者联合类型,那么你可能需要使用内联片段来取出下层具体类型的数据
## hero 可能是 Droid 或者 Human 类型,各自类型返回各自数据
query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    ... on Droid {
      primaryFunction
    }
    ... on Human {
      height
    }
  }
}


# 你也可以通过片段方式来实现
query GetMyProfile {
  me {
    name
    profilePicture
    ...HostProfileFields
    ...GuestProfileFields
  }
}
fragment HostProfileFields on Host {
  profileDescription
}
fragment GuestProfileFields on Guest {
  funds
}
  • 指令 一个指令可以附着在字段或者片段包含的字段上,根据指令就会有不同的返回数据
# @include(if: Boolean) 仅在参数为 true 时,包含此字段。
query Hero($episode: Episode, $withFriends: Boolean!) {
  hero(episode: $episode) {
    name
    friends @include(if: $withFriends) {
      name
    }
  }
}


# @skip(if: Boolean) 如果参数为 true,跳过此字段。
query Hero($episode: Episode, $withFriends: Boolean!) {
  hero(episode: $episode) {
    name
    friends @skip(if: $withFriends) {
      name
    }
  }
}
  • Mutation 变更并查询这个字段的新值,类比 RESTful API 中的 POST、PUT 等方式请求
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}
  • 元字段 GraphQL 允许你在查询的任何位置请求 __typename,它是一个元字段,可以获得那个位置的对象类型名称
# 在不适用 __typename 的情况下,你就无法知道这个 name 的字段是那个类型的
{
  search(text: "an") {
    __typename
    ... on Human {
      name
    }
    ... on Droid {
      name
    }
    ... on Starship {
      name
    }
  }
}


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

总结

RESTful 和 GraphQL 都是数据传输解决方案,GraphQL 可以显著的节省网络传输资源,在带宽紧张的环境中(例如移动端),这将发挥巨大的作用。尽管 GraphQL 相比 REST 有很多显著的优点和升级,但在真实场景中,它并不一定是最适合你的实现。

总结来说,如果你希望做的应用追求简单而敏捷,且没有什么特殊考量,那就没什么必要使用 GraphQL,RESTful 可靠、经济、不易出错;反而言之,如果应用的关键点在于组织复杂数据逻辑,请求存在较多 Overfetching、Underfetching 的情况,或者对于网络环境敏感,可以尝试 GraphQL。

参考文章: