一篇文章帮你理清GraphQL的核心概念(译)

一年多以前就听说了GraphQL,前段时间接触了一个海归团队(创始人就来自Facebook),技术栈使用Graphql+Apollo+React,在他们的指导下试用了一下觉得真心酷。遂花了半个多月了解了GraphQL的主要思想和基本用法,碰巧看到一篇文章)把GraphQL的核心概念讲的比较清晰易懂,依据该文,大致翻译如下,如果你也对GraphQL感兴趣,欢迎一起来讨论。

什么是GraphQL:

给API设计的一种查询语言,一个依据已有数据执行查询的运行时,为你的API中的数据提供一种完全且容易理解的描述,使得API能够更容易的随着时间而演变,还支持强大的开发者工具。

虽然名字叫做GraphQL 但是和数据库本身并没有直接关系。

GraphQL的特征:

  • 可描述性的:使用GraphQL,你获取的都是你想要的数据,不多也不会少;
  • 分级性的:GraphQL天然遵循对象间的关系,通过一个简单的请求,我们可以获取到一个对象及其相关的对象,比如说,通过一个简单的请求,我们可以获取一个作者和他创建的所有文章,然后可以获取文章的所有评论;
  • 强类型的:使用GraphQL的类型系统,我们可以描述能够被服务器查询的可能的数据,然后确保从服务器获取到的数据和我们查询的一致;
  • 不做语言限制:并不绑定于某一特定的语言,实际上现在已经有一些不同的语言有了实践;
  • 兼容于任何后台:GraphQL不限于某一特定数据库,可以使用已经存在的数据,代码,甚至可以连接第三方的APIs.
  • 好反省的:GraphQL服务器能够查询架构的细节。

GraphQL的核心包括Query,Mutation,Schemas等等,每个概念下又有一些子概念,下面分别做简单的介绍:

Query

Queries用做读操作,也就是从服务器获取数据。Queries定义了我们对模式执行的行为。下面是一个简单的查询及相应的结果:

// Basic GraphQL Query

    {
      author {
        name
        posts {
          title
        }
      }
    }
// Basic GraphQL Query Response

    {
      "data": {
        "author": {
          "name": "Chimezie Enyinnaya",
          "posts": [
            {
              "title": "How to build a collaborative note app using Laravel"
            },
            {
              "title": "Event-Driven Laravel Applications"
            }
          ]
        }
      }
    }

查询和响应具备相同的结构。

对query结果的解释

如果一个操作没有type,GraphQL默认会把这些操作看做QueryQuery还可以拥有名字,虽然是可选的,但是可以帮助识别某个query是做什么的。

query也可以拥有注释,注释以#开头。

Field

Field是我们想从服务器获取的对象的基本组成部分。上述代码中name就是author对象的一个Field.

Argument

和普通的函数一样,Query可以拥有参数,参数是可选的或需求的。参数使用方法如下:

{
  author(id: 5) {
    name
  }
}

需要注意的是,GraphQL中的字符串需要包装在双引号中。

Variables

除了参数,query还允许你使用变量来让参数可动态变化,变量以$开头书写,使用方式如下:

query GetAuthor($authorID: Int!) {
      author(id: $authorID) {
        name
      }
    }

参数可以拥有默认值:

query GetAuthor($authorID: Int! = 5) {
      author(id: $authorID) {
        name
      }
    }

参数也可以是可选的或必须的,比如上述的$authorID变量是必须的,它的定义中包含了!。详细信息可见schema中。

Allases

别名,比如说,我们想分别获取ID5和7,我们可以用下面的方法:

{
      author(id: 5) {
        name
      }
      author(id: 7) {
        name
      }
    }

由于存在相同的name,上述代码会报错,要解决这个问题就要用到别名了Allases

{
      chimezie: author(id: 5) {
        name
      }
      neo: author(id: 7) {
        name
      }
    }

获取的结果如下:

# Response

    {
      "data": {
        "chimezie": {
          "name": "Chimezie Enyinnaya"
        },
        "neo": {
          "name": "Neo Ighodaro"
        }
      }
    }

Fragments

Fragments是一套在queries中可复用的fields。比如说我们想获取twitterHandlefield,我们可以按下面这样做:

{
      chimezie: author(id: 5) {
        name
        twitterHandle
      }
      neo: author(id: 7) {
        name
        twitterHandle
      }
    }

但是如果fields过多,就会显得重复和冗余。Fragments在此时就可以起作用了。以下是使用Fragments的语法:

{
      chimezie: author(id: 5) {
        ...authorDetails
      }
      neo: author(id: 7) {
        ...authorDetails
      }
    }

    fragment authorDetails on Author {
      name
      twitterHandle
    }

Directives

Directives提供了一种动态使用变量改变我们的queries的方法。如本例,我们会用到以下两个directive:

  • @include:只有当if中的参数为true时,才会包含对应fragmentField
  • @skip:当if中的参数为true时,会跳过对应fragmentField

这两个directive都接受一个布尔值作为参数;

实例如下:

query GetAuthor($authorID: Int!, $notOnTwitter: Boolean!, $hasPosts: Post) {
      author(id: $authorID) {
        name
        twitterHandle @skip(if: $notOnTwitter)
        posts @include(if: $hasPosts) {
          title
        }
      }
    }

Mutation

传统的API使用场景中,我们会有需要修改服务器上数据的场景,mutations就是应这种场景而生。mutations被用以执行写操作,通过mutations我们会给服务器发送请求来修改和更新数据,并且会接收到包含更新数据的反馈。mutationsqueries具有类似的语法,仅有些许的差别。

mutation UpdateAuthorDetails($authorID: Int!, $twitterHandle: String!) {
      updateAuthor(id: $authorID, twitterHandle: $twitterHandle) {
        twitterHandle
      }
    }

我们在Mutation中以数据为载物发送,比如上面的例子中,我们发送了下面的数据:

# Update data

    {
     "authorID": 5,
     "twitterHandle": "ammezie"
    }

更新完成后,我们将从服务器获取以下内容作为反馈。

# Response after update

    {
      "data": {
        "id": 5,
        "twitterHandle": "ammezie"
      }
    }

我们可以看出,反馈数据中包含我们更新的数据

queries类似,mutations也能够接受,多重fields,不过queriesmutations的一个重大不同之处在于,为了保证数据的完整性mutations是串形执行,而queries可以并行执行。

Schemas

Schemas 描述了 数据的组织形态 以及服务器上的那些数据能够被查询,Schemas提供了你数据中可用的数据的对象类型,GraphQL中的对象是强类型的,因此schema中定义的所有的对象必须具备类型。类型允许GraphQL服务器确定查询是否有效或者是否在运行时。Schemas可用是两种类型QueryMutation

Schemas用GraphQL schemas语言构建,它和我们前面已经学过的query非常类似,下面是一个示例:

type Author {
      name: String!
      posts: [Post]
    }

上面的Schemas定义了一个author对象,它包含两个fields(nameposts),这意味着当我们操作(读取)author时,我们只能使用namefields,每个Field都可以是必须的或者可选的,比如上面的namefield是必须的,因为其后有符号!,而posts是可选的。

Arguments

Schemas中的Fields 也可以接收参数,这些参数可以是可选的或者必须的,必须的参数通过!识别。

type Post {
      allowComments(comments: Boolean!)
    }

标量类型

顺便提一下,GraphQL支持以下标量类型:

  • Int: 带符号的32位整数
  • Float: 带符号的双精度浮点数
  • String: UTF-8 字符串
  • Boolean: true or false
  • ID: 唯一标识符

由上述类型定义的fields 不能 再拥有自己的 fields,我们可以使用scalar关键字,自定义标量类型,比如我们可以定义一个Date类型:

scalar Date

枚举类型

又称Enums,这是一种特殊的标量类型,通过此类型,我们可以限制值为一组特殊的值,比如,我们可以:

  • 保证此类型的任何参数都是允许值之一;
  • 通过类型系统进行通信,该字段始终是有限值集之一。

Enums通过关键字enum进行定义:

enum Media {
      Image
      Video
    }

输入类型

input类型对mutations来说非常重要,在 GraphQL schema 语言中,它看起来和常规的对象类型非常类似,但是我们使用关键字input而非type,input类型按如下定义:

# Input Type

    input CommentInput {
      body: String!
    }

开始实践

我们将使用node.js建设一个简单的任务管理GraphQL serve,这个例子非常简单,但是足以用到我们学过的大部分概念,巩固我们的学习成果。

构建node.js服务器

我们使用Express做为我们的node.js框架,首先我们需要初始化一个node.js项目,使用以下命令:

mkdir graphql-tasks-server
cd graphql-tasks-server
npm init -y
npm install express body-parser apollo-server-express graphql graphql-tools lodash --save

创建如下文件及目录

  • /src/
  • /src/data/
  • /src/data/data.js:
  • /src/schema/
  • /src/schema/index.js
  • /src/schema/resolvers.js
  • /src/server.js
// src/server.js

    const express = require('express');
    const bodyParser = require('body-parser');
    const { graphqlExpress, graphiqlExpress } = require('apollo-server-express');
    const schema = require('./schema/index');

    const PORT = 3000;

    const app = express();

    // Graphql 用以构建Graph服务器
    app.use('/graphql', bodyParser.json(), graphqlExpress({ schema }));

    // Graphiql 用以展示查询客户端
    app.use('/graphiql', graphiqlExpress({ endpointURL: 'graphql' }));

    app.listen(PORT, () => console.log(`GraphiQL is running on http://localhost:${PORT}/graphiql`));

构建Schemas

// src/schema/index.js

    const { makeExecutableSchema } = require('graphql-tools');
    const resolvers = require('./resolvers');

    const typeDefs = `
        type Project {
            id: Int!
            name: String!
            tasks: [Task]
        }
        type Task {
            id: Int!
            title: String!
            project: Project
            completed: Boolean!
        }
        type Query {
            projectByName(name: String!): Project
            fetchTasks: [Task]
            getTask(id: Int!): Task
        }
        type Mutation {
            markAsCompleted(taskID: Int!): Task
        }
    `;

    module.exports = makeExecutableSchema({ typeDefs, resolvers });

我们为我们的app定义了schema,我们定义了两种类型,projectstasks,type task中包含我们要完成的任务,而type Project中包含三项id,nametasksidname是必备fields,tasks则是一系列Task类型的组合,Task type包含4项,id, title, projectcompletedidtitle是必须的,project指明了属于那个项目,而completed表明了其完成情况。

一个项目可以包含多个任务,而一个任务必属于一个项目。

接下来,我们定义了一系列查询,

  • projectByName:用以通过传入的name参数来返回对应的project;
  • fetchTasks:用以获取所有的任务并返回;
  • getTasks:依据传入的id返回特定的任务;

我们也定义了一些Mutation:

  • markAsCompleted:接受一个id做为参数,并返回修改完成状态后的Task

Writing Resolvers

resolver是决定Schemas中的Field该如何执行的函数。

Tip: GraphQL resolvers 可以返回Promises

// src/schema/resolvers.js

    const _ = require('lodash');

    // Sample data
    const { projects, tasks } = require('./../data/data');

    const resolvers = {
        Query: {
            // Get a project by name
            projectByName: (root, { name }) => _.find(projects, { name: name }),

            // Fetch all tasks
            fetchTasks: () => tasks,

            // Get a task by ID
            getTask: (root, { id }) => _.find(tasks, { id: id }),

        },
        Mutation: {
            // Mark a task as completed
            markAsCompleted: (root, { taskID }) => {
                const task = _.find(tasks, { id: taskID });

                // Throw error if the task doesn't exist
                if (!task) {
                    throw new Error(`Couldn't find the task with id ${taskID}`);
                }

                // Throw error if task is already completed
                if (task.completed === true) {
                    throw new Error(`Task with id ${taskID} is already completed`);
                }

                task.completed = true;

                return task;
            }
        },
        Project: {
            tasks: (project) => _.filter(tasks, { projectID: project.id })
        },
        Task: {
            project: (task) => _.find(projects, { id: task.projectID })
        }
    };

    module.exports = resolvers;

我们对Schemas中定义的各项添加了处理函数。

添加数据

// src/data/data.js

    const projects = [
        { id: 1, name: 'Learn React Native' },
        { id: 2, name: 'Workout' },
    ];

    const tasks = [
        { id: 1, title: 'Install Node', completed: true, projectID: 1 },
        { id: 2, title: 'Install React Native CLI:', completed: false, projectID: 1 },
        { id: 3, title: 'Install Xcode', completed: false, projectID: 1 },
        { id: 4, title: 'Morning Jog', completed: true, projectID: 2 },
        { id: 5, title: 'Visit the gym', completed: false, projectID: 2 }
    ];

    module.exports = { projects, tasks };

启动服务器并测试

node src/server.js

在浏览器中打开,http://localhost:3000/graphiql,输入查询即可看到结果。

一篇文章帮你理清GraphQL的核心概念(译)

一些有用的链接

相关推荐