GraphQL分享
什么是GraphQL?
GraphQL是Facebook开源的API查询语言,类似于数据库中的SQL。
GraphQL本质上是一种基于api的查询语言,现在大多数应用程序都需要从服务器中获取数据,这些数据存储可能存储在数据库中,API是提供与应用程序需求相匹配的存储数据的接口。
有的人经常把GraphQL和数据库技术相混淆,这是一个误解,GraphQL是api的查询语言,而不是数据库。从这个意义上说,它是数据库无关的,而且可以在使用API的任何环境中有效使用,可以理解为GraphQL是基于API之上的一层封装,目的是为了更好,更灵活的适用于业务的需求变化。
作为比较,RESTful API依赖于后端隐式被动的数据约定,GraphQL更加显式,在获取数据和更新数据时更加主动,所见即所得。
RESTful的一些不足
1.扩展性,单个RESTful接口返回数据越来越臃肿
比如获取用户信息/users/:id,最初可能只有id、昵称,但随着需求的变化,用户所包含的字段可能会越来越多,年龄、性别、头像、经验、等级,等等。
而具体到某个前端页面,可能只需要其中一小部分数据,这样就会增加网络传输量,前端获取了大量不必要的数据。
2.某个前端展现,实际需要调用多个独立的RESTful API才能获取到足够的数据
比如一个文章详情页,最初可能只需要文章内容,那么前端就调用/articles/:aid获取到文章内容来展现就行了
但随着需求的演进,产品可能会希望加上作者信息(昵称、头像等),这时前端又需要在获取文章详情后,根据其中的作者id字段继续获取作者相关的信息,/user/:uid
然后,需求又变化了,产品希望在加上这篇文章的评论,这时前端需要继续调用/comment/:aid来拉取评论列表
对于前端而言,由于ajax技术的存在,这种的请求数据方式,也就开发上稍微麻烦些,并不会造成太大的问题;但对于App来说,渲染的方式不同,必须要拉取的全部的数据之后,才能绘制界面,就会导致这个界面必须要等到所有3个RESTful接口的返回数据都拿到,才能进行绘制。
GraphQL出现背景
但GraphQL提供了强类型的schema机制,从而天然确保了参数类型的合法性。
当REST的概念被提及出来时,客户端应用程序对数据的需求相对简单,而开发的速度并没有达到今天的水平。因此REST对于许多应用程序来说是非常适合的。然而在业务越发复杂,客户对系统的扩展性有了更高的要求时,API环境发生了巨大的变化。特别是从下面三个方面在挑战api设计的方式:
1.移动端用户的爆发式增长需要更高效的数据加载
Facebook开发GraphQL的最初原因是移动用户的增加、低功耗设备和松散的网络。GraphQL最小化了需要网络传输的数据量,从而极大地改善了在这些条件下运行的应用程序。
2.各种不同的前端框架和平台
前端框架和平台运行客户端应用程序的异构环境使得我们在构建和维护一个符合所有需求的API变得困难,使用GraphQL每个客户机都可以精确地访问它需要的数据。
3.在不同前端框架,不同平台下想要加快产品快速开发变的越来越难
持续部署已经成为许多公司的标准,快速的迭代和频繁的产品更新是必不可少的。对于REST api,服务器公开数据的方式常常需要修改,以满足客户端的特定需求和设计更改。这阻碍了快速开发实践和产品迭代。
GraphQL优点
1.所见即所得
查询的返回结果就是输入的查询结构的精确映射
2.减少网络请求次数
如果设计的数据结构是从属的,直接就能在查询语句中指定;即使数据结构是独立的,也可以在查询语句中指定上下文,只需要一次网络请求,就能获得资源和子资源的数据。
3.代码即文档
GraphQL会把schema定义和相关的注释生成可视化的文档,从而使得代码的变更,直接就反映到最新的文档上,避免RESTful中手工维护可能会造成代码、文档不一致的问题。
4.参数类型强校验
RESTful方案本身没有对参数的类型做规定,往往都需要自行实现参数的校验机制,以确保安全。
这个图展示的是查询流程,查询流程分为几个步骤,涉及多个组件,包括客户端应用程序(Web, 手机, 桌面等App),一个GraphQL服务器用于解析查询,以及多个不同的数据来源。
为什么解决了REST API的大问题, 看如下阐述:
只要你的业务模型没有发生变化,从数据模型不会发生变化,那么我们就不需要修改后端API。 前端按照需要的字段进行查询即可。如果业务发生了变化,我们只需要修改GraphQL的模式定义,并且实现对应的服务器端数据查询逻辑即可。传统的REST查询那些字段是固定的, 客户端不能指定,GraphQL可以让客户端指定要获取那些字段的数据,这给客户端带来了极大的灵活性,让前后端进一步分离。查询是可以嵌套的,返回的JSON对象结构和GraphQL查询的结构是一样的,这样更方便客户端自己定义数据的结构.
GraphQL同样能够让客户端程序高效地批量获取数据。
例如, 看一看下面这个GraphQL请求:获取一篇博客文章和对应评论与作者信息的数据
{
latestPost {
_id,
title,
content,
author {
name
},
comments {
content,
author {
name
}
}
}
}
这个 GraphQL 请求获取了一篇博客文章和对应评论与作者信息的数据。
下面是请求的返回结果:
{
"data": {
"latestPost": {
"_id": "03390abb5570ce03ae524397d215713b",
"title": "New Feature: Tracking Error Status with Kadira",
"content": "Here is a common feedback we received from our users ...",
"author": {
"name": "Pahan Sarathchandra"
},
"comments": [
{
"content": "This is a very good blog post",
"author": {
"name": "Arunoda Susiripala"
}
},
{
"content": "Keep up the good work",
"author": {
"name": "Kasun Indi"
}
}
]
}
}
}
GraphQL适用场景
从Facebook最初开发GraphQL的目的到实践者实际使用的情况而言,GraphQL还是存在较多缺点的,
GraphQL作为RESTful的一种辅助工具,尤其是针对前端App在复杂页面,本来要调用有上下文关系的多次RESTful请求时,采用GraphQL,只需要一次请求,就可以拿回所需的全部数据,可以起到非常好的效果,大大提升App的性能。
实际操作
现在我们通过实际的操作来感受GraphQL具体是一个什么东西。
首先在浏览器中打开: https://sandbox.learngraphql.com ,我们会看到下图的GraphiQL查询界面, 其界面窗口如下所示:
然后在左侧的查询窗口中输入下面的查询语句:
{
posts (category: PRODUCT) {
_id,
title,
summary
}
}
然后右侧出现如下内容:
{
"data": {
"posts": [
{
"_id": "03390abb5570ce03ae524397d215713b",
"title": "New Feature: Tracking Error Status with Kadira",
"summary": "Lot of users asked us to add a feature to set status for errors in the Kadira Error Manager. Now, we've that functionality."
},
{
"_id": "0be4bea0330ccb5ecf781a9f69a64bc8",
"title": "What Should Kadira Build Next?",
"summary": "We are working on the next few major feature releases for Kadira. We would like to know your preference. Pre-order the feature you would most like to see in the next major release (scheduled for August 1)."
},
{
"_id": "19085291c89f0d04943093c4ff16b664",
"title": "Awesome Error Tracking Solution for Meteor Apps with Kadira",
"summary": "Error tracking is so much important and goes side by side with performance issues. This is the public beta announcement of Kadira's error tracking solution."
},
{
"_id": "1afff9dfb0b97b5882c72cb60844e034",
"title": "Tracking Meteor CPU Usage with Kadira",
"summary": "We've replaced EventLoop Utilization chart with actual CPU Usage. See why?"
},
{
"_id": "3d7a3853bf435c0f00e46e15257a94d9",
"title": "Introducing Kadira Debug, Version 2",
"summary": "Today, we are introducing a new version of Kadira Debug. It comes with many UI improvements and support for CPU profiling."
}
]
}
}
这个体验了一下GraphQL是怎么工作的。下面就该构建案例:
快速开始
使用GraphQL.js
首先创建项目文件夹,然后进入该文件在该目录里打开控制台,然后使用npm安装必要的依赖以及nodejs的express框架:
npm init // npm初始
然后为了配合graphql
我们需要进行安装相关依赖:
npm install express --save
npm install graphql express express-graphql --save
npm install babel --save
npm install body-parser --save
先创建一个名为 t1.js
的文件,然后输入以下代码:
var graphql = require('graphql');
var graphqlHTTP = require('express-graphql');
var express = require('express');
// 引入数据
var data = require('./data.json');
// 用"id"和"name"两个字符串字段和"age"整数字段定义User类型
// User的类型是GraphQLObjectType,其子字段具有自己的类型
var userType = new graphql.GraphQLObjectType({
name: 'User',
fields: {
id: { type: graphql.GraphQLString },
name: { type: graphql.GraphQLString },
age: { type: graphql.GraphQLInt },
}
});
// 定义一个*字段架构"User",它接收一个参数"id",并根据ID,来返回用户
// 注意:"query"是GraphQLObjectType,就像"User"。然而我们在上面定义的"user"这个字段,是一个userType
var schema = new graphql.GraphQLSchema({
query: new graphql.GraphQLObjectType({
name: 'Query',
fields: {
user: {
type: userType,
// 'args'描述参数,接受'user'查询
args: {
id: { type: graphql.GraphQLString }
},
resolve: function (_, args) {
return data[args.id];
}
}
}
})
});
express()
.use('/graphql', graphqlHTTP({ schema: schema, pretty: true }))
.listen(3000);
console.log('GraphQL server running on http://localhost:3000/graphql, please continue~');
再创建一个名为data.json
的文件,内容如下:
{
"1": {
"id": "002",
"name": "杨文强",
"age": 24
},
"2": {
"id": "001",
"name": "王建宇",
"age": 25
},
"3": {
"id": "003",
"name": "郭滨溢",
"age": 24
}
}
在你创建的目录下打开控制台,然后使用node命令启动服务器:
node t1.js
运行t1.js,默认会打开:3000
端口,如果你直接访问http://localhost:3000/graphql页面会得到如下反馈:
然后我们在该目录下开启另一个控制台,键入以下内容:
curl -X POST -H "Content-Type:application/graphql" -d '{ user(id:"1"){name} }' http://localhost:3000/graphql
然后会得到如下反馈:
{
"data": {
"user": {
"name": "杨文强"
}
}
}
另一个例子:
新建t2.js
里面的内容进行注释,然后写入以下内容:
var express = require('express');
var app = express();
var schema = require('./schema');
var graphql = require('graphql');
var bodyParser = require('body-parser');
app.use(bodyParser.text({
type: 'application/graphql'
}));
app.post('/graphql', (req, res) => {
graphql.graphql(schema, req.body)
.then((result) => {
res.send(JSON.stringify(result, null, 2));
});
});
app.get('/', function (req, res) {
res.send('Hello World!');
});
var server = app.listen(3000, function () {
var host = server.address().address;
var port = server.address().port;
console.log('Example app listening at http://%s:%s', host, port);
});
同目录下新建一个文件:schema.js
var graphql = require('graphql');
let myName = "杨文强";
let schema = new graphql.GraphQLSchema({
query: new graphql.GraphQLObjectType({
name: 'queryMyName',
fields: {
name: {
type: graphql.GraphQLString,
resolve: function () {
return myName;
}
}
}
})
});
// 把当前模块的路由暴露出去
module.exports = schema;
然后使用node命令启动服务器:
node t2.js
运行t2.js
,然后我们在该目录下开启另一个控制台,键入以下内容:
curl -X POST -H "Content-Type:application/graphql" -d 'query QueryMyName{ name }' http://localhost:3000/graphql
最终会得到如下反馈:
{
"data": {
"name": "杨文强"
}
}
Express 应用生成器创建
上面的案例是入门的,大部分情况下我们开发并不会这样使用,我们大部分情况下需要通过 Express 应用生成器创建应用,首先我们需要全局安装express:npm install express-generator -g
,然后自己想一个文件名进行express初始化:express -e 文件夹名
,我起为nodejs_express,然后初始化完成,它会提醒你如图:
cd nodejs_express && npm install
DEBUG=nodejs_express:* npm start // 说明:启动这个应用
然后在浏览器中打开 http://localhost:3000/ 网址就可以看到这个应用了。
通过 Express 应用生成器创建的应用一般都有如下目录结构:
为了配合graphql
我们需要进行安装相关依赖
npm install express --save
npm install graphql express express-graphql --save
npm install babel --save
安装完依赖我们在项目目录下新建两个文件一个是dao(数据访问接口),一个是graphql,在dao文件下新建一个data.json文件,在graphql文件下新建index.js
/dao/data.json
还是上面的内容:
{
"1": {
"id": "002",
"name": "杨文强",
"age": 24
},
"2": {
"id": "001",
"name": "王建宇",
"age": 25
},
"3": {
"id": "003",
"name": "郭滨溢",
"age": 24
}
}
然后我们打开app.js
,这里面是框架默认配置,我们需要在这里面引入我们刚才添加配置的依赖和我们需要的文件,添加代码:
// 自需引入模块
var graphql = require('graphql');
var graphqlHTTP = require('express-graphql');
var graphqlIndex = require('./graphql/index.js');
app.use('/graphql', graphqlHTTP({ schema: graphqlIndex, pretty: true }))
app.js
的整体:
var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var index = require('./routes/index');
var users = require('./routes/users');
// 自需引入模块
var graphql = require('graphql');
var graphqlHTTP = require('express-graphql');
var graphqlIndex = require('./graphql/index.js');
var app = express();
app.use('/graphql', graphqlHTTP({ schema: graphqlIndex, pretty: true }))
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', index);
app.use('/users', users);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
/graphql/index.js
的内容如下:
var graphql = require('graphql');
// Import the data you created above
var data = require('../dao/data.json');
// 用"id"和"name"两个字符串字段和"age"整数字段定义User类型
// User的类型是GraphQLObjectType,其子字段具有自己的类型
var userType = new graphql.GraphQLObjectType({
name: 'User',
fields: {
id: { type: graphql.GraphQLString },
name: { type: graphql.GraphQLString },
age: { type: graphql.GraphQLInt },
}
});
// 定义一个*字段架构"User",它接收一个参数"id",并根据ID,来返回用户
// 注意:"query"是GraphQLObjectType,就像"User"。然而我们在上面定义的"user"这个字段,是一个userType
var schema = new graphql.GraphQLSchema({
query: new graphql.GraphQLObjectType({
name: 'Query',
fields: {
user: {
type: userType,
// 'args'描述参数,接受'user'查询
args: {
id: { type: graphql.GraphQLString }
},
resolve: function (_, args) {
return data[args.id];
}
}
}
})
});
//把当前模块的路由暴露出去
module.exports = schema;
我们在该项目中使用DEBUG=你的文件名:* npm start
的方式运行项目,打开:3000
端口,如果你直接访问http://localhost:3000/graphql页面会得到如下反馈:
然后我们在该目录下开启另一个控制台,键入以下内容:
curl -X POST -H "Content-Type:application/graphql" -d '{ user(id:"1"){name} }' http://localhost:3000/graphql
然后会得到如下反馈:
{
"data": {
"user": {
"name": "杨文强"
}
}
}
不用GraphQL
1.使用与迁移成本
现有的RESTful如果要改造成GraphQL?
首先,RESTful从端到端已经有成熟高效的解决方案。且主要问题是,对于项目本身,从数据层到业务逻辑层,可能有巨大的影响。所以,它非常不适合现有的复杂系统“先破后立”。其成本和破坏性难以预计。
2.简单问题复杂化
最明显的表现就是错误处理。
REST API的情况下,我们不需要解析Response的内容,只需要看HTTP status code和message,就能知道请求是否成功,大概问题是什么,处理错误的程序也十分容易编写。
然而GraphQL的情景下,只要服务本身还在正常运行,就会得到200的HTTP status,然后需要专门检查response的内容才知道是否有error:
{
"errors": [
{
"message": "Field \"name\" must not have a selection since type \"String\" has no subfields.",
"locations": [
{
"line": 31,
"column": 101
}
]
}
]
}
3.查询的复杂性
在服务端,一个查询需要解析数据,因此一个 GraphQL 相关实现常常需要执行数据库访问,但 GraphQL 其实不关心这些。
还有,GraphQL 在你需要在一个查询中获取多个字段(作者、文章、评论)的时候,它对性能瓶颈没有任何帮助。
无论使用 RESTful 架构还是 GraphQL,不同资源/字段仍然需要从一个数据源去获取。因此当一个客户端需要一次查询很多嵌套字段时,前端开发通常不能很清楚他正在通过服务端访问不同的数据库获取过多的数据。这需要一种机制来制止来自客户端的(性能)昂贵的查询。
4.缓存
一个简单缓存,相比 REST,在 GraphQL 中实现会变得极其复杂。
在 REST 中通过 URL 访问资源,因此可以在资源级别实现缓存,因为资源使用 URL 作为其标识符。
在 GraphQL 中就复杂了,因为即便它操作的是同一个实体,每个查询都各不相同。
比如,一个查询中,你可能只会请求一个作者的名字,但是在另外一次查询中你可能也想知道他的电子邮箱地址。这就需要你有一个更加健全的机制中来确保字段级别的缓存。