Links

GraphQL

本文是GraphQL的高度浓缩,若难以理解,推荐教程 前往
GraphQL是一个用于 API 的查询语言。他是强类型的,可以校验入参,并确定响应类型。
GraphQL有两个核心概念:GraphQL schema和GraphQL OPERATION。

GraphQL schema

schema指的是graphql结构定义的集合,类似数据库schema或数据库建表语句,operation指的是从schema中取的子集,类似数据库的查询sql。
我们知道gql是一个强类型语言,它通过4个关键字:type、enum、scalar、input,定义了所有的数据结构。
其数据结构分为两种,一种叫做Scalar Type(标量类型),另一种叫做Object Type(对象类型)。
GraphQL中的内建的标量包含:String、Int、Float、Boolean、Enum,这些概念想必大家都接触过。
其中,enum表示枚举,通过enum关键字,可以定义枚举标量,例如sortorder,花括号里面的asc和desc是它的 枚举值。
GraphQL也支持通过Scalar声明新的标量,比如,这里声明了一个DateTime标量,用来表示日期格式。
总之,我们只需要记住,标量是GraphQL类型系统中最小的颗粒。\
对象类型基于标量构建,其关键字为type。每一个对象有若干字段组成,字段都有类型。例如:TODO 对象类型。它有6个Field,分别是INT类型的id,String类型的Title和Boolean类型的clompeted,xxx。
gql支持对象嵌套,因此 字段不仅可以是标量,也可以是对象,甚至可以是自循环对象。
关于类型,还有一个较重要的概念,即类型修饰符,当前的类型修饰符有两种,分别是List和Required,它们的语法分别为[Type]和Type!。前者代表数组,后者代表必填。
除了type定义对象外,input也用于定义对象,称为 参数类型。可以类比为函数入参的类型。很多编程语言没有区分对象类型和参数类型。
在这里,我们要注意下,type和input定义的对象,都支持嵌套定义,即todowhereinput类型的字段也可以是todowhereinput,可以是别的对象类型,如Intfilter。
因此,type和input,通过这种嵌套,声明了各个模型之间的内在关联(一对多、一对一或多对多)。
此外,还有3类特殊对象,query、mutation和subscription,对于传统的CRUD项目,我们只需要前两种类型就足够了,第三种是针对当前日趋流行的real-time应用提出的,这块后续会讲到。
我们一般叫这3中对象为:Query(大写)。
接下来,我们分别以REST和GraphQL的角度,以todo为数据模型,编写一系列CRUD的接口。例如上图这几个接口。
  • GET /api/v1/todos/
  • GET /api/v1/todo/:id/
  • POST /api/v1/todo/
  • DELETE /api/v1/todo/:id/
获取待做事项列表对应findmanytodo,其中findmanytodo为根字段,可以类比为一个函数名称。
我们还可以发现,GraphQL中用query和mutation两种类型代表了rest接口的众多请求类型,例如QUERY对应get请求,mutation对应POST\DELETE、patch请求。
findmanytodo既然是函数,自然有入参和出参(返回值)。其出参是数组对象。
入参也有类型,但不能用type定义的类型,而是必须要用input定义类型。例如,where参数的类型是todowhereinput类型。
其实,不只是根字段可以有入参,任意对象类型都可以有入参。这个后续我们会遇到。
至此,我们就基本掌握了如何定义gql schema。

GraphQL OPERATION

接下来,我们学习operation。
operation指的是从schema中的Query里取的子集,如果把schema queryr中的跟字段比作函数定义,那operation就是函数调用。
operation和SCHEMA query类型一一对应,也分为3中类型,分别是query,mutation,subscription。
如该operation 调用了 findmanytodo函数,字段入参分别是take和skip。其中take为固定值10,skip为变量值,由变量$skip传入。
变量是一个新概念,是Operaiton上定义的参数,注意不是函数上的参数。变量主要用途是动态设置函数的参数。
变量支持默认值,如$skip默认值为10。此外,变量也支持用修饰符!修饰,表明该变量为必传字段。
接着我们看下operaiton的响应,例如右侧图的灰色部分,就是该operaion的响应。在gql中被称为作用于该mutation opration的选择集。
值得注意的是,一个opratinon中可以有多个函数,因此operation选择集可能会同时包含多个跟字段。
选择集大概率是对应字段的子集,例如mutation里面不仅包含del和cretat,还包括executeraw,而实际上并没有用到后者。
不仅有作用域opration上的选择集,也有作用域字段上的选择集,例如,count就是作用域deletemanytodo字段的选择集。

GraphQL SERVER

接下来,我们学习:如何使用基于GraphQL协议构建的服务。
nodejs构建的GraphQL服务代码
// graphql server code
import express from 'express';
import { createServer } from 'http';
import { PubSub } from 'apollo-server';
import { ApolloServer, gql } from 'apollo-server-express';
const app = express();
const pubsub = new PubSub();
const MESSAGE_CREATED = 'MESSAGE_CREATED';
const typeDefs = gql`
type Query {
messages: [Message!]!
deatail(title:String!):String
}
type Subscription {
messageCreated: Message
}
type Message {
id: String
content: String
}
`;
const resolvers = {
Query: {
messages: (ctx) => {
console.log("test", ctx)
return [
{ id: 0, content: 'Hello!' },
{ id: 1, content: 'Bye!' },
]
},
deatail: (ctx,{title}) => {
console.log("test", ctx,title)
return "echo:"+title
},
},
Subscription: {
messageCreated: {
subscribe: () => pubsub.asyncIterator(MESSAGE_CREATED),
},
},
};
const myPlugin = {
// Fires whenever a GraphQL request is received from a client.
async requestDidStart(requestContext) {
console.log('Request started!')
console.log(requestContext.request.http.headers)
return {
async parsingDidStart(requestContext) {
console.log('Parsing started!');
},
async validationDidStart(requestContext) {
console.log('Validation started!');
},
}
},
};
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
myPlugin
]
});
server.applyMiddleware({ app, path: '/graphql' });
const httpServer = createServer(app);
server.installSubscriptionHandlers(httpServer);
httpServer.listen({ port: 8000 }, () => {
console.log('Apollo Server on http://localhost:8000/graphql');
});
let id = 2;
setInterval(() => {
pubsub.publish(MESSAGE_CREATED, {
messageCreated: { id, content: new Date().toString() },
});
id++;
}, 1000);
服务启动后,本地访问地址:http://localhost:8000/graphql ,界面如下:
Gql服务启动后,会对外暴露gql端点,其路由一般由graphql结尾。
以GET请求访问端点时,会返回一个gql操作界面。
基于界面可以方便的构建OPERATION,并执行该OPERATION拿到响应。分别对应步骤2和3。
其中3的底层是向该Gql端点,发送POST请求。
我们以该opration为例,它有2个根字段:message和detail,其中messsage字段的类型为对象,且无入参;detail的类型为标量,有一个入参title,并通过Operation的变量$titlevar为其赋值。
当我们点击执行按钮时,其请求如下:
curl 'https://fireboom-gql.ansoncode.repl.co/graphql' \
-H 'content-type: application/json' \
--data-raw $'{"operationName":"MyQuery","variables":{"titleVar":"ttttt"},"query":"query MyQuery($titleVar: String\u0021) {\\n messages {\\n content\\n id\\n }\\n deatail(title: $titleVar)\\n}\\n"}' \
--compressed
端点为:https://fireboom-gql.ansoncode.repl.co/graphql,请求为post类型。
请求头content-type为json类型。
请求体有3字段:
  • operationName,操作名称,可以省略
  • query:operation的字符串
  • variables:operation的入参对象
该请求的响应体和operation的选择集一致,只不过会包裹在data对象里面。
响应结构
{
"data": {
"messages": [
{
"id": "0",
"content": "Hello!"
},
{
"id": "1",
"content": "Bye!"
}
],
"deatail": "echo:ssss"
}
}

GraphQL 内省

但有个问题需要回答: 该前端界面如何了解后端SCHEMA的呢?
这就要提到gql的“内省”能力,它是一种特殊的query operation 。能够根据步骤3一样的方式获取schema的结构。
之所以能实现该功能,主要得益于该端点的强类型特性。
向GraphQL端点发送下述请求,可以拿到GraphQL SCHEMA对应的JSON结构体,通过特定库可以将其转换为GraphQL SCHEMA。
内省QUERY OPERATION
query IntrospectionQuery {
__schema {
queryType {
name
}
mutationType {
name
}
subscriptionType {
name
}
types {
...FullType
}
directives {
name
description
locations
args {
...InputValue
}
}
}
}
fragment FullType on __Type {
kind
name
description
fields(includeDeprecated: true) {
name
description
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
description
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}
fragment InputValue on __InputValue {
name
description
type {
...TypeRef
}
defaultValue
}
fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}
}
}

参考