GraphQL 基础实践

本次内容是基于以前分享的文字版,诺想看重点的话能够看以前的 PPTjavascript

什么是 GraphQL

GraphQL 是一款由 Facebook 主导开发的数据查询和操做语言, 写过 SQL 查询的同窗能够把它想象成是 SQL 查询语言,但 GraphQL 是给客户端查询数据用的。虽然这让你听起来以为像是一款数据库软件,但实际上 GraphQL 并不是数据库软件。 你能够将 GraphQL 理解成一个中间件,是链接客户端和数据库之间的一座桥梁,客户端给它一个描述,而后从数据库中组合出符合这段描述的数据返回。这也意味着 GraphQL 并不关心数据存在什么数据库上。php

同时 GraphQL 也是一套标准,在这个标准下不一样平台不一样语言有相应的实现。 GraphQL 中还设计了一套类型系统,在这个类型系统的约束下,能够得到与 TypeScript 相近的相对安全的开发体验。html

GraphQL 解决了什么问题

咱们先来回顾一下咱们已经很是熟悉的 RESTful API 设计。简单的说 RESTful API 主要是使用 URL 的方式表达和定位资源,用 HTTP 动词来描述对这个资源的操做。vue

咱们以 IMDB 电影信息详情页为例子,看看咱们得须要什么样的 API 才能知足 RESTful API 设计的要求。先来看看主页面上都须要什么信息。java

能够看到页面上由电影基本信息,演员和评分/评论信息组成,按照设计要求,咱们须要将这三种资源放在不一样 API 之下。首先是电影基本信息,咱们有 API /movie/:id,给定一个电影ID返回基本信息数据。git

伪装 GET 一下得到一个 JSON 格式的数据:github

{
  name: “Manchester by the Sea”,
  ratings: “PG-13”,
  score: 8.2,
  release: “2016”,
  actors:[“https://api/movie/1/actor/1/”],
  reviews:[“https://api/movie/1/reviews”]
}
复制代码

这里面包含了咱们所需的电影名、分级等信息,以及一种叫作 HyperMedia 的数据,一般是一个 URL,指明了可以获取这个资源的 API 端点地址。若是咱们跟着 HyperMedia 指向的链接请求下去,咱们就能获得咱们页面上所需的全部信息。mongodb

GET /api/movue/1/actor/1数据库

{
  name: “Ben Affleck”,
  dob: “1971-01-26”,
  desc: “blablabla”,
  movies:[“https://api/movie/1”]
}
复制代码

GET /api/movie/1/reviewsnpm

[
  {
     content: “Its’s as good as…”,
     score: 9
  }
]
复制代码

最后根据须要,咱们要将全部包含须要信息的 API 端点都请求一遍,对于移动端来讲,发起一个 HTTP 请求仍是比较消耗资源的,特别是在一些网络链接质量不佳的状况下,一下发出多个请求反而会致使很差的体验。

并且在这样的 API 设计之中,特定资源分布在特定的 API 端点之中,对于后端来讲写起来是挺方便的,但对于Web端或者客户端来讲并不必定。例如在 Android 或 iOS 客户端上,发版升级了一个很爆炸的功能,同一个API上可能为了支持这个功能而多吐一些数据。可是对于未升级的客户端来讲,这些新数据是没有意义的,也形成了必定的资源浪费。若是单单将全部资源整合到一个 API 之中,还有可能会由于整合了无关的数据而致使数据量的增长。

而 GraphQL 就是为了解决这些问题而来的,向服务端发送一次描述信息,告知客户端所需的全部数据,数据的控制甚至能够精细到字段,达到一次请求获取全部所需数据的目的。

GraphQL Hello World

GraphQL 请求体

咱们先来看一下一个 GraphQL 请求长什么样:

query myQry ($name: String!) {
  movie(name: “Manchester”) {
    name
    desc
    ratings
  }
}
复制代码

这个请求结构是否是和 JSON 有那么点类似?这是 Facebook 故意设计成这样的,但愿你读完以后就能体会到 Facebook 的用心良苦了。

那么,上面的这个请求描述称为一个 GraphQL 请求体,请求体即用来描述你要从服务器上取什么数据用的。通常请求体由几个部分组成,从里到外了解一下。

首先是字段,字段请求的是一个数据单元。同时在 GraphQL 中,标量字段是粒度最细的一个数据单元了,同时做为返回 JSON 响应数据中的最后一个字段。也就是说,若是是一个 Object,还必须选择至少其中的一个字段。

把咱们所须要的字段合在一块儿,咱们把它称之为某某的选择集。上面的 namedescratings 合在一块儿则称之为 movie 的选择集,同理,moviemyQry 的选择集。须要注意的是,在标量上使用不能使用选择集这种操做,由于它已是最后一层了。

movie 的旁边,name: "Manchester",这个表明着传入 movie 的参数,参数名为 name 值为Manchester,利用这些参数向服务器表达你所需的数据须要符合什么条件。

最后咱们来到请求体的最外层:

  • 操做类型:指定本请求体要对数据作什么操做,相似与 REST 中的 GET POST。GraphQL 中基本操做类型有 query 表示查询,mutation 表示对数据进行操做,例如增删改操做,subscription 订阅操做。
  • 操做名称:操做名称是个可选的参数,操做名称对整个请求并不产生影响,只是赋予请求体一个名字,能够做为调试的依据。
  • 变量定义:在 GraphQL 中,声明一个变量使用$符号开头,冒号后面紧跟着变量的传入类型。若是要使用变量,直接引用便可,例如上面的 movie 就能够改写成 movie(name: $name)

若是上述三者都没有提供,那么这个请求体默认会被视为一个 query 操做。

请求的结果

若是咱们执行上面的请求体,咱们将会获得以下的数据:

{
  "data": {
    "movie": {
      "name": "Manchester By the Sea",
      "desc": "A depressed uncle is asked to take care of his teenage nephew after the boy's father dies. ",
      "ratings": "R"
    }
  }
}
复制代码

仔细对比结果和请求体的结构,你会发现,与请求体的结构是彻底一致的。也就是说,请求体的结构也肯定了最终返回数据的结构

GraphQL Server

在前面的 REST 举例中,咱们请求多个资源有多个 API 端点。在 GraphQL 中,只有一个 API 端点,一样也接受 GET 和 POST 动词,如要操做 mutation 则使用 POST 请求。

前面还提到 GraphQL 是一套标准,怎么用呢,咱们能够借助一些库去解析。例如 Facebook 官方的 GraphQL.js。以及 Meteor 团队开发的 Apollo,同时开发了客户端和服务端,同时也支持流行的 Vue 和 React 框架。调试方面,可使用 Graphiql 进行调试,得益于 GraphQL 的类型系统和 Schema,咱们还能够在 Graphiql 调试中使用自动完成功能。

Schema

前面咱们提到,GraphQL 拥有一个类型系统,那么每一个字段的类型是怎么约定的呢?答案就在本小节中。在 GraphQL 中,类型的定义以及查询自己都是经过 Schema 去定义的。GraphQL 的 Schema 语言全称叫 Schema Definition Language。Schema 自己并不表明你数据库中真实的数据结构,它的定义决定了这整个端点能干些什么事情,肯定了咱们能向端点要什么,操做什么。再次回顾一下前面的请求体,请求体决定了返回数据的结构,而 Schema 的定义决定了端点的能力。

接下来咱们就经过一个一个的例子了解一下 Schema。

类型系统、标量类型、非空类型、参数

先看右边的 Schema:type 是 GraphQL Schema 中最基本的一个概念,表示一个 GraphQL 对象类型,能够简单地将其理解为 JavaScript 中的一个对象,在 JavaScript 中一个对象能够包含各类 key,在 GraphQL 中,type 里面一样能够包含各类字段(field),并且字段类型不只仅能够是标量类型,还能够是 Schema 中定义的其余 type。例如上面的 Schema 中, Query 下的 movie 字段的类型就能够是 Movie

在 GraphQL 中,有以下几种标量类型:Int, Float, String, Boolean, ID ,分别表示整型、浮点型、字符串、布尔型以及一个ID类型。ID类型表明着一个独一无二的标识,ID 类型最终会被转化成String类型,但它必须是独一无二的,例如 mongodb 中的 _id 字段就能够设置为ID类型。同时这些标量类型能够理解为 JavaScript 中的原始类型,上面的标量类型一样能够对应 JavaScript 中的 Number, Number, String, Boolean, Symbol

在这里还要注意一点,type QueryQuery 类型是 Schema 中全部 query 查询的入口,相似的还有 MutationSubscription,都做为对应操做的入口点。

type Query下的 movie 字段中,咱们使用括号定义咱们能够接受的参数名和参数的类型。在上面的 Schema 中,后面紧跟着的感叹号声明了此类型是个不可空类型(Non-Nullable),在参数中声明表示该参数不能传入为空。若是感叹号跟在 field 的后面,则表示返回该 type 的数据时,此字段必定不为空。

经过上面的类型定义,能够看到 GraphQL 中的类型系统起到了很重要的角色。在本例中,Schema 定义了 nameString类型,那么你就不能传 Int类型进去,此时会抛出类型不符的错误。一样的,若是传出的 ratings 数据类型不为 String,也一样会抛出类型不符的错误。

列表(List)、枚举类型(Enum)

若是咱们的某个字段返回不止一个标量类型的数据,而是一组,则须要使用List类型声明,在该标量类型两边使用中括号[]包围便可,与 JavaScript 中数组的写法相同,并且返回的数据也将会是数组类型。

须要注意的是[Movie]![Movie!]两种写法的含义是不一样的:前者表示 movies字段始终返回不可为空但Movie元素能够为空。后者表示movies中返回的 Movie 元素不能为空,但 movies字段的返回是能够为空的。

你可能在请求体中注意到,genre 参数的值没有被双引号括起来,也不是任何内置类型。看到 Schema 定义,COMEDY是枚举类型MovieTypes中的枚举成员。枚举类型用于声明一组取值常量列表,若是声明了某个参数为某个枚举类型,那么该参数只能传入该枚举类型内限定的常量名。

传入复杂结构的参数(Input)

前面的例子中,传入的参数均为标量类型,那么若是咱们想传入一个拥有复杂结构的数据该怎么定义呢。答案是使用关键字input。其使用方法和type彻底一致。

根据本例中的 Schema 定义,咱们在查询 searchdata的参数必须为

{ term: "Deepwater Horizon" }
复制代码

别名(Alias)

想象这么一个页面,我要列出两个电影的信息作对比,为了发挥 GraphQL 的优点,我要同时查询这两部电影的信息,在请求体中请求 movie 数据。前面咱们说到,请求体决定了返回数据的结构。在数据返回前查出两个 key 为 movie 的数据,合并以后因为 key 重复而只能拿到一条数据。那么在这种状况下咱们须要使用别名功能。

别名即为返回字段使用另外一个名字,使用方法也很简单,只须要在请求体的字段前面使用别名:的形式便可,返回的数据将会自动替换为该名称。

片断(Fragment)、片断解构(Fragment Spread)

在上面的例子中,咱们须要对比两部电影的数据。若是换做是硬件对比网站,须要查询的硬件数量每每不止两个。此时编写冗余的选择集显得很是的费劲、臃肿以及难维护。为了解决这个问题,咱们可使用片断功能。GraphQL 容许定义一段公用的选择集,叫片断。定义片断使用 fragment name on Type 的语法,其中 name为自定义的片断名称,Type为片断来自的类型。

本例中的请求体的选择集公共部分提取成片断以后为

fragment movieInfo on Movie {
   name
   desc
}
复制代码

在正式使用片断以前,还须要向各位介绍片断解构功能。相似于 JavaScript 的结构。GraphQL 的片断结构符号将片断内的字段“结构”到选择集中。

接口(Interface)

与其余大多数语言同样,GraphQL 也提供了定义接口的功能。接口指的是 GraphQL 实体类型自己提供字段的集合,定义一组与外部沟通的方式。使用了 implements的类型必须包含接口中定义的字段。

interface Basic {
    name: String!
    year: Number!
}

type Song implements Basic {
    name: String!
    year: Number!
    artist: [String]!
}

type Video implements Basic {
    name: String!
    year: Number!
    performers: [String]!
}

Query {
    search(term: String!): [Basic]!
}
复制代码

在本例中,定义了一个Basic接口,Song以及Video类型都要实现该接口的字段。而后在search查询中返回该接口。

searchMedia查询返回一组Basic接口。因为该接口中的字段是全部实现了该接口的类型所共有的,在请求体上能够直接使用。而对于特定类型上的其余非共有字段,例如Video中的performers,直接选取是会有问题的,由于searchMedia在返回的数据中类型多是全部实现了该接口的类型,而在 Song类型中就没有performers字段。此时咱们能够借助内联片断的帮助(下面介绍)。

联合类型(Union)

联合类型与接口概念差很少相同,不一样之处在于联合类型下的类型之间没有定义公共的字段。在 Union 类型中必须使用内联片断的方式查询,缘由与上面的接口类型一致。

union SearchResult = Song | Video
Query {
    search(term: String!): [SearchResult]!
}
复制代码

内联片断(Inline Fragment)

对接口或联合类型进行查询时,因为返回类型的不一样致使选取的字段可能不一样,此时须要经过内联片断的方式决定在特定类型下使用特定的选择集。内联选择集的概念和用法与普通片断基本相同,不一样的是内联片断直接声明在选择集内,而且不须要fragment声明。

查询接口的例子:

query {
    searchMedia(term: "AJR") {
    	name
    	year
    	
        ...on Song {
            artist
        }
        
        ...on Video {
            performers
        }
    }
}

复制代码

首选咱们须要该接口上的两个公共字段,而且结果为Song类型时选取artist字段,结果为Video类型时选取performers字段。下面查询联合类型的例子也是同样的道理。

查询联合类型的例子:

query {
    searchStats(player: "Aaron") {
        ...on NFLScore {
            YDS
            TD
        }
        
        ...on MLBScore {
            ERA
            IP
        }
    }
}

复制代码

GraphQL 内置指令

GraphQL 中内置了两款逻辑指令,指令跟在字段名后使用。

@include

当条件成立时,查询此字段

query {
    search {
        actors @include(if: $queryActor) {
            name
        }
    }
}

复制代码

@skip

当条件成立时,不查询此字段

query {
    search {
        comments @skip(if: $noComments) {
            from
        }
    }
}

复制代码

Resolvers

前面咱们已经了解了请求体以及 Schema,那么咱们的数据到底怎么来呢?答案是来自 Resolver 函数。

Resolver 的概念很是简单。Resolver 对应着 Schema 上的字段,当请求体查询某个字段时,对应的 Resolver 函数会被执行,由 Resolver 函数负责到数据库中取得数据并返回,最终将请求体中指定的字段返回。

type Movie {
    name
    genre
}

type Query {
    movie: Movie!
}

复制代码

当请求体查询movie时,同名的 Resolver 必须返回Movie类型的数据。固然你还能够单独为name字段使用独立的 Resolver 进行解析。后面的代码例子中将会清楚地了解 Resolver。

使用 ThinkJS 搭建 GraphQL API

ThinkJS 是一款面向将来开发的 Node.js 框架,整合了大量的项目最佳实践,让企业级开发变得如此简单、高效。 框架底层基于 Koa 2.x 实现,兼容 Koa 的全部功能。

本例中咱们将使用 ThinkJS 配合 MongoDB 进行搭建 GraphQL API,ThinksJS 的简单易用性会让你爱不释手!

快速安装

首先安装 ThinkJS 脚手架 npm install -g think-cli

使用 CLI 快速建立项目 thinkjs new gqldemo

切换到工程目录中 npm install && npm start

不到两分钟,ThinkJS 服务端就搭建完了,so easy!

配置 MongoDB 数据库

因为本人比较喜欢 mongoose,恰好 ThinkJS 官方提供了 think-mongoose 库快速使用,安装好以后咱们须要在 src/config/extend.js中引入并加载该插件。

const mongoose = require('think-mongoose');
module.exports = [mongoose(think.app)];

复制代码

接下来,在 adapter.js 中配置数据库链接

export.model = {
    type: 'mongoose',
    mongoose: {
        connectionString: 'mongodb://你的数据库/gql',
        options: {}
    }
};

复制代码

如今,咱们在整个 ThinkJS 应用中都拥有了 mongoose 实例,看看还差啥?数据模型!

借助 ThinkJS 强大的数据 模型功能,咱们只须要以数据集合的名称做为文件名创建文件并定义模型便可使用,相比 mongoose 原生的操做更为简单。

本例中咱们实现 actor 和 movie 两组数据,在 model 目录下分别创建 actor.jsmovie.js,并在里面定义模型。

actor.js

module.exports = class extends think.Mongoose {
  get schema() {
    return {
      name: String,
      desc: String,
      dob: String,
      photo: String,
      addr: String,
      movies: [
        {
          type: think.Mongoose.Schema.Types.ObjectId,
          ref: 'movie'
        }
      ]
    };
  }
};

复制代码

movie.js

module.exports = class extends think.Mongoose {
  get schema() {
    return {
      name: String,
      desc: String,
      ratings: String,
      score: Number,
      release: String,
      cover: String,
      actors: [
        {
          type: think.Mongoose.Schema.Types.ObjectId,
          ref: 'actor'
        }
      ]
    };
  }
};

复制代码

处理 GraphQL 请求的中间件

要处理 GraphQL 请求,咱们就必须拦截特定请求进行解析处理,在 ThinkJS 中,咱们彻底能够借助中间件的能力完成解析和数据返回。中间件的配置在 middleware.js中进行。

ThinkJS 中配置中间件有三个关键参数:

  • match: 用于匹配 URL,咱们想让咱们的请求发送到 /graphql 中进行处理,那么咱们对这个路径进行 match 后进行处理;
  • handle:中间件的处理函数,当 match 到时,此处理函数会被调用执行,咱们的解析任务也在这里进行,并将解析结果返回;
  • options:options 时传给中间件的参数,咱们能够在此将咱们的 Schema 等内容传给解析器使用。

咱们的中间件配置大概长这样:

{
    match: '/graphql',
    handle: () => {},
    options: {}
}

复制代码

解析 GraphQL 的核心

Apollo Server

Apollo Server 是一款构建在 Node.js 基础上的 GraphQL 服务中间件,其强大的兼容性以及卓越的稳定性是本文选取此中间件的首要因素。

尽管 Apollo Server 没有 ThinkJS 版的中间件,可是万变不离其宗,咱们能够经过 Apollo Server Core 中的核心方法 runHttpQuery 进行解析。

将它安装到咱们的项目中: npm install apollo-server-core graphql --save

编写中间件

runHttpQuery主要接受两个参数,第一个是 GraphQLServerOptions,这个咱们能够不须要配置,留空数组便可;第二个是HttpQueryRequest对象,咱们至少须要包含 methods,options以及query

他们分别表示当前请求的方法,GraphQL服务配置以及请求体。

而GraphQL服务配置中咱们至少要给出 schemaschema 应该是一个 GraphQLSchema实例,对于咱们前面例子中直接写的 Schema Language,是不能被识别的,此时咱们须要借助 graphql-tools 中的 makeExecutableSchema 工具将咱们的 Schema 和 Resolvers 进行关联成 GraphQLSchema实例。

将它安装到咱们的项目中:npm install graphql-tools --save

编写 Schema 和 Resolver

在转换成 GraphQLSchema 以前,首先要将咱们的 Schema 和 Resolver 准备好。

运用前面所学的知识,咱们能够很快的编写出一个简单的 Schema 提供查询演员信息和电影信息的接口。

type Movie {
  name: String!
  desc: String!
  ratings: String!
  score: Float!
  cover: String!
  actors: [Actor]
}

type Actor {
  name: String!
  desc: String!
  dob: String!
  photo: String!
  movies: [Movie]
}

type Query {
  movie(name: String!): [Movie]
  actor(name: String!): [Actor]
}

复制代码

接下来,分别编写解析 Querymovieactor字段的 Resolver 函数。

const MovieModel = think.mongoose('movie');
const ActorModel = think.mongoose('actor');

module.exports = {
    Query: {
        movie(prev, args, context) {
          return MovieModel.find({ name: args.name })
            	.sort({ _id: -1 })
            	.exec();
        },
        actor(prev, args, context) {
          return ActorModel.find({ name: args.name })
            	.sort({ _id: -1})
            	.exec();
        }
    }
}

复制代码

为了可以和 Schema 正确关联,Resolver 函数的结构须要与 Schema 的结构保持一致。

到达这一步,有没有发现什么不对呢?

回忆前面的数据模型定义,里面的 moviesactors 字段是一组另外一个集合中数据的引用,目的是方便创建电影和演员信息之间的关系以及维护,在 Resolver 运行以后,moviesactors 字段获得的是一组 id,不符合 Schema 的定义,此时 GraphQL 会抛出错误。

那么这个问题怎么解决呢?前面讲到 Resolver 的时候说到,每一个字段均可以对应一个 Resolver 函数,咱们分别对 moviesactors 字段设置 Resolver 函数,将上一个 Resolver 解析出来的 id 查询一遍得出结果,最终返回的数据就能符合 Schema 的定义了。

const MovieModel = think.mongoose('movie');
const ActorModel = think.mongoose('actor');

module.exports = {
    Query: {
        movie(prev, args, context) {
          return MovieModel.find({ name: args.name })
            	.sort({ _id: -1 })
            	.exec();
        },
        actor(prev, args, context) {
          return ActorModel.find({ name: args.name })
            	.sort({ _id: -1})
            	.exec();
        }
    },
    Actor: {
        movies(prev, args, context) {
            return Promise.all(
            	prev.movies.map(_id => MovieModel.findOne({ _id }).exec())
            );
        }
    },
    Movie: {
        actors(prev, args, context) {
            return Promise.all(
            	prev.actors.map(_id => ActorModel.findOne({ _id }).exec())
            );
        }
    }
}

复制代码

其中用到的 prev 参数就是上一个 Resolver 解析出的数据。

组合成 GraphQLSchema 实例

有了 Schema 和 Resolver 以后,咱们终于能够把它们变成一个 GraphQLSchema 实例了。

调用 graphql-tools 中的 makeEcecutableSchema 进行组合好,放在 options 里面稍后使用。

此时咱们的中间长这样:

const { makeExecutableSchema } = require('graphql-tools');
const Resolvers = require('./resolvers'); // 咱们刚写的 Resolver
const Schema = require('./schema'); // 咱们刚写的 Schema
module.exports = {
    match: '/graphql',
    handle: () => {},
    options: {
        schema: makeExecutableSchema({
            typeDefs: Schema,
            resolvers: Resolvers
        })
    }
}
复制代码
编写 handler

有请apollo-server-core 里面的runHttpQuery出场!

const { runHttpQuery } = require('apollo-server-core');

复制代码

参照 apollo-server-koa,快速构建出 ThinkJS 版的 apollo-server 中间件。

const { runHttpQuery } = require('apollo-server-core');
module.exports = (options = {}) => {
  return ctx => {
    return runHttpQuery([ctx], {
      method: ctx.request.method,
      options,
      query:
        ctx.request.method === 'POST'
          ? ctx.post()
          : ctx.param()
    }).then(
      rsp => {
        ctx.set('Content-Type', 'application/json');
        ctx.body = rsp;
      },
      err => {
        if (err.name !== 'HttpQueryError') throw err;

        err.headers &&
          Object.keys(err.headers).forEach(header => {
            ctx.set(header, err.headers[header]);
          });

        ctx.status = err.statusCode;
        ctx.body = err.message;
      }
    );
  };
};
复制代码

接下来引用到咱们中间件的handle配置中,完美,大功告成,用 ThinkJS 搭建的 GraphQL 服务器就此告一段落,npm start 运行起来以后,用 GraphiQL “播放”一下你的请求体(记得本身先往数据库灌数据)。

GraphQL 的优缺点

优势

  • 所见即所得:所写请求体即为最终数据结构
  • 减小网络请求:复杂数据的获取也能够一次请求完成
  • Schema 即文档:定义的 Schema 也规定了请求的规则
  • 类型检查:严格的类型检查可以消除必定的认为失误

缺点

  • 增长了服务端实现的复杂度:一些业务可能没法迁移使用 GraphQL,虽然可使用中间件的方式将原业务的请求进行代理,这无疑也将增长复杂度和资源的消耗

完整源代码能够在这里找到,中间件能够在这里找到

相关文章
相关标签/搜索