一年多以前就听说了
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默认会把这些操作看做query
。query
还可以拥有名字,虽然是可选的,但是可以帮助识别某个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
。比如说我们想获取twitterHandle
field,我们可以按下面这样做:
{
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
时,才会包含对应fragment
或field
;@skip
:当if
中的参数为true
时,会跳过对应fragment
或field
;
这两个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
我们会给服务器发送请求来修改和更新数据,并且会接收到包含更新数据的反馈。mutations
和queries
具有类似的语法,仅有些许的差别。
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
,不过queries
和mutations
的一个重大不同之处在于,为了保证数据的完整性mutations
是串形执行,而queries
可以并行执行。
Schemas
Schemas 描述了 数据的组织形态 以及服务器上的那些数据能够被查询,Schemas提供了你数据中可用的数据的对象类型,GraphQL中的对象是强类型的,因此schema中定义的所有的对象必须具备类型。类型允许GraphQL服务器确定查询是否有效或者是否在运行时。Schemas可用是两种类型Query
和Mutation
。
Schemas
用GraphQL schemas语言构建,它和我们前面已经学过的query非常类似,下面是一个示例:
type Author {
name: String!
posts: [Post]
}
上面的schemas
定义了一个Author
对象,它包含两个fields
(name
和posts
),这意味着当我们操作(读取)Author
时,我们只能使用name
和fields
,每个field
都可以是必须的或者可选的,比如上面的name
field是必须的,因为其后有符号!
,而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
,我们定义了两种类型,projects
和tasks
,type task中包含我们要完成的任务,而type Project中包含三项id
,name
和tasks
,id
和name
是必备fields
,tasks
则是一系列Task
类型的组合,task
type包含4项,id
, title
,project
和 completed
,id
和title
是必须的,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,输入查询即可看到结果。