GraphQL是Facebook开发的一套数据查询解决方案,让咱们先来看一下官方的定义:javascript
GraphQL is a query language for your API, and a server-side runtime for executing queries by using a type system you define for your data. GraphQL isn't tied to any specific database or storage engine and is instead backed by your existing code and data.前端
翻译过来就是:java
GraphQL是对你的API的一种查询语言,而且提供了对你采用类型系统所定义的数据进行查询的服务器端运行时方案。GraphQL并不与特定的数据库或存储引擎绑定,而是能对你现有的代码和数据进行支持。node
其中有2个重点:git
在网上能找到的文章每每对第一点描述的比较详细,并且这一点也确实比较吸引人。但对于关键的第二点,如何实现这套查询机制的介绍却很难找到。github
假设咱们的blog有如下两张表:数据库
用户表
中的数据:express
uid | name | avatar |
---|---|---|
1 | Tom | https://pre00.deviantart.net/2930/th/pre/i/2014/182/a/2/tom_cat_by_1997ael-d7ougoa.png |
2 | Jerry | https://vignette.wikia.nocookie.net/tomandjerry/images/2/29/Jerry_2.png |
帖子表
中的数据(考虑到容许用户修改头像,因此帖子表中不冗余做者的信息,而只有做者的ID):npm
pid | title | content | authorId |
---|---|---|---|
1 | foo | xxx | 1 |
2 | bar | yyy | 2 |
而后,界面大体是上下两栏模式的,上部是帖子标题、内容等;下部是做者的名字、头像等。让咱们来看一下resuful和GraphQL方案的实现对比。json
若是采用restful方案,咱们一般会设计以下两个接口:
GET /posts/:id
GET /users/:id
而后,前端先调用拉取帖子内容的接口,拿到相似以下的返回结果:
GET /posts/1
{ "code": 0, "reason": "success", "data": { "pid": 1, "title": "foo", "content": "xxx", "authorId": 1 } }
而后,再根据上述结果中的authorId
去调用拉取用户信息的接口,来获取做者的相关信息:
GET /users/1
{ "code": 0, "reason": "success", "data": { "uid": 1, "name": "Tom", "avatar": "https://pre00.deviantart.net/2930/th/pre/i/2014/182/a/2/tom_cat_by_1997ael-d7ougoa.png" } }
在Web前端这样调用问题还不大,但遇到App时,因为绘制界面是一体化的,因此必需要两个restful接口都调用完毕,才能绘制界面。
而随着需求的变化,这个页面可能还会要展示评论、评论发表者的头像,等等等等;这就会致使这里须要调用的接口愈来愈多,从而使得App渲染这个界面的速度愈来愈慢。
采用GraphQL方式,咱们首先须要对数据进行类型定义:
用户定义:
# user schema type User { uid : ID! name : String! avatar : String! }
帖子定义:
# post schema type Post { pid : ID! title : String! content : String! author : User! }
查询定义:
type Query { post(id: ID): Post }
而后,咱们根据界面要求编写查询语句,由于界面要求同时展示帖子内容和做者信息,因此会有以下的GraphQL查询语句:
query { post(id:1) { pid title content author { uid name avatar } } }
由于数据定义中,post
下的author
成员是User
类型的,因此咱们只须要经过一次查询就可以拿到绘制界面所需的数据:
{ "data": { "post": { "pid": "1", "title": "foo", "content": "xxx", "author": { "uid": "1", "name": "Tom", "avatar": "https://pre00.deviantart.net/2930/th/pre/i/2014/182/a/2/tom_cat_by_1997ael-d7ougoa.png" } } } }
看到这里你们必定能体会到GraphQL的查询语言的爽点所在了,但网上大多数的资料也每每是继续介绍这个查询语言的更多语法,但对于服务器端如何执行查询却介绍的不够深刻,所给出的简单的例子甚至是上述数据结构中的每个成员变量都要写一个对应的resolver
函数来进行查询的状况。
因为存在如上痛点,笔者在进行了相关的探索后,封装了一个使用上更简便的npm库(easy-graphql)。
easy-graphql
设计了一套约定,使得开发会更便捷和规范: SQR
按照上述约定来创建目录结构,指定的目录下存放对应的文件,好比上文blog的例子,咱们创建的目录格式以下:
graphql
目录做为根目录graphql
下创建schemas
和resolvers
两个子目录,分别用于存放数据类型定义文件和对应的查询解决实现函数文件query.graphqls
文件,用于对外提供的查询接口定义graphql # GraphQL相关定义、代码的跟目录 ├── query.graphqls # 对外提供的查询接口定义文件 ├── resolvers # 如何查询数据的函数实现文件所在目录 │ ├── post_resolver.js │ └── user_resolver.js └── schemas # 数据的类型定义文件所在目录 ├── post_schema.graphqls └── user_schema.graphqls
文件存放在graphql/schemas
目录下,命名规则:xxx_schema.graphqls
帖子和用户的数据类型定义上文已有,此处再也不赘述
文件放在graphql
目录下,命名为:query.graphqls
文件放在graphql/reslvers
目录下,命名规则:xxx_resolver.js
这里就只针对上文说起的帖子内容和做者信息的查询是如何实现的(resolvers/post_reslver.js
):
'use strict' const fakeDB = require('../../fakeDB'); function fetchPostById (root, {id}, ctx) { // post的查询,第二个参数是查询语句中传入的 let pid = parseInt(id); return fakeDB.getPostById(pid); } // 对post下的author字段进行查询解决的函数 function fetchUserByAuthorId (root, args, ctx) { // 执行完post的数据查询后,遇到须要author字段的状况,会再来调用本函数,root参数就是前一步查询完的post数据 let uid = root.authorId; return fakeDB.getUserById(uid); } const postReolvers = { Query : { post : fetchPostById, }, Post : { // 针对嵌套的数据结构须要的解决函数 author : fetchUserByAuthorId, }, }; module.exports = postReolvers;
新建一个easy-graphql
对象:
const path = require('path'); const easyGraphqlModule = require('easy-graphql'); const basePath = path.join(__dirname, 'graphql'); const easyGraphqlObj = new easyGraphqlModule(basePath);
对于采用node.js来进行开发的话,GraphQL提供了可视化的图形化的Web界面来编写、调试查询语句。
express插件:express-graphql KOA插件:koa-graphql
easy-graphql
配合express-graphql
使用:
const express = require('express'); const graphqlHTTP = require('express-graphql'); const allSchema = easyGraphqlObj.getSchema(); // using with express-graphql middleware app.use('/graphql', graphqlHTTP({ schema : allSchema, graphiql : true, }));
而后,就能在浏览器中,直接访问对应的网址,打开可视化IDE调试界面了,效果以下图所示:
使用上面的中间件方案,咱们已经能够实现对外提供GraphQL查询的能力了,但每每咱们的项目已经有约定好的返回数据结构了,好比:
{ "code" : 0, "reason" : "success", "data" : {...} }
而直接采用插件形式,并不能自定义返回的数据结构,因此easy-graphql
又提供了一个直接执行GraphQL查询语句的API:
/** * do the GraphQL query execute * @param {*} requestObj - GraphQL query object {query: "..."} * @param {*} context - [optional] query context * @returns {Promise} - GraphQL execute promise */ queryGraphQLAsync(requestObj, {context})
以使用express
框架为例,咱们能够本身实现一个接口供前端调用:
const bodyParser = require('body-parser'); app.use(bodyParser.json()); // for parsing application/json app.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded app.post('/restful', async (req, res) => { let queryObj = req.body; let result; try { // using with your restful service result = await easyGraphqlObj.queryGraphQL(queryObj, {context: req}); } catch (err) { console.error(err); res.json({code : -1, reason : "GraphQL error"}); return; } res.json({ code : 0, reason : "success", data : result.data, }); });
完整的代码示例,请前往gayhub上的test目录下查看,欢迎你们给这个项目点赞!