本文由 kazaff 翻译而成,点击阅读原文可以查看作者的博客,感谢作者的优质输出,让我们的技术世界更加美好✌️
很久之前其实就关注过这个技术,记得当时还是React刚刚崭露头角的时期吧。总之那时候,GraphQL感觉还只是概念完备阶段,除了FB自己内部大量使用外,好像社区并不是很健全,不过大家应该都在疯狂的讨论和跟进吧。过了2年,如今再回过头来看,已经涌现出各种开源或商用服务专注于这个领域,各种语言的框架和工具也都很完备了,感觉是时候重新接触GraphQL了。如果你的项目正处于技术选型,你正在犹豫选择一种接口风格的时刻,不妨了解一下这个神奇而强大的玩意儿~~
本文打算翻译一篇感觉很解惑的文章,主要围绕着GraphQL的server端实现,因为相比client端,server端包含了更多的内容。后面如果有机会,也会尝试提供关于client端相关的内容,不过前端同学可以先看一下这里:howtographql[1],这里有各种最佳实践,应该总会找到和你正在使用相关的前端框架的整合方案,好像有个对应的中文版[2]~
关于GraphQL概念的内容,这篇文章并没有涉及太多,不过假如你用搜索引擎去搜的话,相信有非常多的相关文章供你学习,这里就不再重复了~
原文在这里[3],怀疑我翻译能力的同学可以去看原位哦~
相信读完整个文章,对于GraphQL Server会有一个完整的了解。我们开始吧~
目录
目标
一切从Schema开始
创建一个简单的GraphQL服务端
GraphiQL,一个Graphql领域的postman
编写Resolvers
处理数据依赖关系
对接真正的数据库
1+N查询问题
管理自定义Scalar类型
错误处理
日志
认证 & 中间件
Resolvers的单元测试
查询引擎的集成化测试
Resolvers拆分
组织Schemas
结语
目标
我们的目标是针对一个移动app端界面显示所需要的数据,提供支撑,可以实现单一请求次数下就可以获取足够的数据。我们将会用Nodejs来完成这个任务,因为这个语言我们已经在marmelab用了4年了。但你也可以用任何你想用的语言,例如Ruby,Go,甚至PHP,JAVA或C#。
为了显示这个页面,服务端必须能提供下面的响应数据结构:
{
"data": {
"Tweets": [
{
"id": 752,
"body": "consectetur adipisicing elit",
"date": "2017-07-15T13:17:42.772Z",
"Author": {
"username": "alang",
"full_name": "Adrian Lang",
"avatar_url": "http://avatar.acme.com/02ac660cdda7a52556faf332e80de6d8"
}
},
{
"id": 123,
"body": "Lorem Ipsum dolor sit amet",
"date": "2017-07-14T12:44:17.449Z",
"Author": {
"username": "creilly17",
"full_name": "Carole Reilly",
"avatar_url": "http://avatar.acme.com/5be5ce9aba93c62ea7dcdc8abdd0b26b"
}
},
// etc.
],
"User": {
"full_name": "John Doe"
},
"NotificationsMeta": {
"count": 12
}
}
}
我们需要模块化和可维护的代码,需要做单元测试,听起来这很难?你会发现借助于GraphQL工具链,这并不比开发Rest客户端难多少。
一切从Schema开始
当我开发一个GraphQL服务时,我总会从在白板上设计模型开始,而不是上来就写代码。我会和产品和前端开发团队一起来讨论需要提供哪些数据类型,查询或更新操作。如果你了解领域驱动设计方法[4],你会很熟悉这个流程。前端开发团队在拿到服务端返回的数据结构之前是没有办法开始编码的。所以我们需要先对API达成一致。
Tip 命名很重要!不要觉得把时间花在为变量起名字上很浪费。特别是当这些名称会长期使用的时候 - 记住,GraphQL API并没有版本号这回事儿,所以,尽可能让你的Schema具有自解释特性,因为这是其他开发人员了解项目的入口。
下面是我为这个项目提供的GraphQL Schema:
type Tweet {
id: ID!
# The tweet text. No more than 140 characters!
body: String
# When the tweet was published
date: Date
# Who published the tweet
Author: User
# Views, retweets, likes, etc
Stats: Stat
}
type User {
id: ID!
username: String
first_name: String
last_name: String
full_name: String
name: String @deprecated
avatar_url: Url
}
type Stat {
views: Int
likes: Int
retweets: Int
responses: Int
}
type Notification {
id: ID
date: Date
type: String
}
type Meta {
count: Int
}
scalar Url
scalar Date
type Query {
Tweet(id: ID!): Tweet
Tweets(limit: Int, sortField: String, sortOrder: String): [Tweet]
TweetsMeta: Meta
User: User
Notifications(limit: Int): [Notification]
NotificationsMeta: Meta
}
type Mutation {
createTweet(body: String): Tweet
deleteTweet(id: ID!): Tweet
markTweetRead(id: ID!): Boolean
}
我在这个系列的前一篇文章中简短的介绍了Schema的语法。你只需要知道,这里的type
类似REST里的resources概念。你可以用它来定义拥有唯一id键的实体(如Tweet
和User
)。你也可以用它来定义值对象,这种类型嵌套在实体内部,因此不需要唯一键(例如Stat
)。
Tip 尽可能保证
Type
足够轻巧,然后利用组合。举个例子,尽管stats
数据现在看来和tweet
数据关系很近,但是请分开定义它们。因为它们表达的领域不同。这样当有天将stats
数据换其它底层架构来维护,你就会庆幸今天做出的这个决定。
Query
和Mutation
关键字有特别的含义,它们用来定义API的入口。所以你不能声明一个自定义类型用这两个关键字 - 它们是GraphQL预留关键字。你可能会对Query
下定义的字段有个困扰,它们总是和实体类型名字一样 - 但这只是个习惯约定。我就决定把获取Tweet
类型数据的属性名称定义成getTweet
- 记住,GraphQL是一种RPC(译者注:有别于RESTful的资源概念)。
官方GraphQL提供的schema文档[5]提供了所有细节,花十分钟来了解一下对你定义自己的schema会很有帮助。
Tip 你可能看过有些GraphQL教程使用代码风格来定义schema,例如
GraphQLObjectType
。别这么做[6],这种风格显得非常的啰嗦,也不够清晰。
创建一个简单的GraphQL服务端
用Nodejs实现一个HTTP服务端最快的方式是使用express microframework[7]。稍后我们会在http://localhost:4000/graphql[8]下接入一个GraphQL服务。
> npm install express express-graphql graphql-tools graphql --save
express-graphql
库会基于我们定义的schema
和resolver
函数来创建一个graphQL服务。graphql-tools
库提供了schema
的解析和校验的独立包。这两个库前者是来自于Facebook,后者源于Apollo。
// in src/index.js
const fs = require('fs');
const path = require('path');
const express = require('express');
const graphqlHTTP = require('express-graphql');
const { makeExecutableSchema } = require('graphql-tools');
const schemaFile = path.join(__dirname, 'schema.graphql');
const typeDefs = fs.readFileSync(schemaFile, 'utf8');
const schema = makeExecutableSchema({ typeDefs });
var app = express();
app.use('/graphql', graphqlHTTP({
schema: schema,
graphiql: true,
}));
app.listen(4000);
console.log('Running a GraphQL API server at localhost:4000/graphql');
执行下面命令来让我们的服务端跑起来:
> node src/index.js
Running a GraphQL API server at localhost:4000/graphql
我们可以使用curl
来简单请求一下我们的graphQL服务:
> curl 'http://localhost:4000/graphql' \
> -X POST \
> -H "Content-Type: application/graphql" \
> -d "{ Tweet(id: 123) { id } }"
{
"data": {"Tweet":null}
}
正常!
Graphql服务根据我们提供的schema
定义,在执行请求携带的查询语句之前进行了必要的校验,如果我们的查询语句中包含了一个没有声明过的字段,我们会得到一个错误提醒:
> curl 'http://localhost:4000/graphql' \
> -X POST \
> -H "Content-Type: application/graphql" \
> -d "{ Tweet(id: 123) { foo } }"
{
"errors": [
{
"message": "Cannot query field \"foo\" on type \"Tweet\".",
"locations": [{"line":1,"column":26}]
}
]
}
Tip
express-graphql
包生成的GraphQL服务端同时支持GET和POST请求。
Tip 世界上还有一个不错的库可以让我们基于express,koa,HAPI或Restify来建立GraphQL服务:apollo-server[9]。使用的方法和我们用的这个没有太多差异,所以这个教程同样适用。
GraphiQL,一个Graphql领域的postman
curl
并不是一个很好用的工具来测试我们的GraphQL服务。我们使用GraphiQL[10]<