GraphQL:一种不一样于REST的接口风格

从去年开始,JS算是彻底踏入ES6时代。在React相关项目中接触到了一些ES6的语法。此次接着GraphQL这种新型的接口风格,从后端的角度接触ES6。javascript

这篇文章从ES6的特征讲起,打好语法基础;而后引用GraphQL的规范说明;最后实验性质地在node环境下实践GraphQL这种接口风格,做为接下来重构接口工做的起点。java

  1. ES6
  2. GraphQL
  3. Node ES6语法环境
  4. 搭建GraphQL Server

ES6

babel learning page

ES6也就是ECMAScript2015于2015年6月正式发布,这是最新的Javascript核心语言标准。新的语法规范涵盖各类语法糖和新概念。ES6既兼容过去编写的JS代码,又以一种新的方式完全改JS代码。ES6始终坚持这样的宗旨:node

凡是新加入的特性,势必已在其它语言中获得强有力的实用性证实。python

下面依据Babeljs的文档介绍ES6的新特性。es6

Arrows:箭头函数

可以编写lambda函数的新语法,它的语法很是简单:标志符=>表达式。表达式能够是返回值,也能够是块语句(块语句须要使用return手动返回)。固然要注意下列代码出现的状况。因为空对象与块语句的符号都是使用{}标志,箭头函数看到{}会断定为空语法块,须要强制使用括号包裹空对象。mongodb

let items = Objs.map(stuff => {}); //空语法块 let items = Objs.map(stuff => ({})); //空对象 

而且,箭头函数的this值继承外围做用域,共享父函数的“arguments”参数变量。数据库

Class:类

咱们知道在ES5中咱们用多种方式实现函数的构造,这些分发看起来都比较复杂。ES6提供了一种原型OO的语法糖。好比使用static添加方法时,函数的.prototype属性也能添加相应的方法。express

Subclassing:子类

ES5中原有的继承方式是这样的:编程

为了使新建立的类继承全部的静态属性,咱们须要让这个新的函数对象继承超类的函数对象;一样,为了使新建立的类继承全部实例方法,咱们须要让新函数的prototype对象继承超类的prototype对象。json

ES6添加使用关键词‘extends’声明子类继承父类,使用关键词‘super’访问父类的属性。而父类可使用new.target来肯定子类的类型。

Template String:模板字符串

`Hello, This is template of ${language}?` 这种使用反引号的字符串就是模板字符串,它为JS提供了简单的字符串插值。

Destructuring:解构

解构赋值容许你使用相似数组或者对象字面量的语法将数组和对象的属性赋给各类变量。

let [foo, [[bar], baz]] = [1, [[2], 3]]; //嵌套数据解构 let { name: nameA } = { name: 'Ips' } //对象解构 

解构还能够应用到交换变量、函数返回多值、函数参数默认值(like python),使编写的代码更加简洁。

Symbols:符号

JS的第七种类型的原始量,可以避免冲突的风险地建立做为属性键的值险。

Iterators:迭代器

ES6增长了新的一种循环语法 for-of。该方法能够正确响应break、continue、return。

向对象添加Symbol.iterator,就能够遍历对象。迭代器对象是具备.next()方法的对象。for-of首次调用集合的Symbol.iterator()方法,紧接着返回一个新的迭代器对象。for-of循环每次调用.next()方法。好比下面这个迭代器实现了每次返回0。

let objIterator = { [Symbol.iterator]: function(){ return this; }; next: function(){ return { done: false, value: 0 }; } } 

Generators:生成器

生成器就是包含 yield 表达式的函数。yield相似return,不过在生成器的执行过程肿,遇到yield时当即暂停,后续能够恢复执行状态。普通函数使用function声明,而生成器函数使用function*声明。

全部的生成器都有内建.next()和Symbol.interator方法的实现,因此生成器就是迭代器。

Modules:模块

模块标志就是一段脚本,Node采用CommonJS的方式模块化。在ES6中的模块默认在严格模式下运行模块,而且可使用关键词‘import’和‘export’。‘export’能够导出最外层的函数、类以及var、let或者const声明的变量。‘import’能够直接导入或者导入模块内部多个模块、重命名模块。除了node使用‘require’关键字外,ES6的模块和node的是同样的。

当JS引擎运行模块时,按照下列四个步骤执行:

  1. 语法解析:阅读模块源代码,检查语法错误。
  2. 加载:递归地加载全部被导入的模块。这也正是没被标准化的部分。
  3. 链接:每遇到一个新加载的模块,为其建立做用域并将模块内声明的全部绑定填充到该做用域中,其中包括由其它模块导入的内容。
  4. 运行时:最终,在每个新加载的模块体内执行全部语句。

Proxies:代理

代理(Proxy)对象做为定义对象基础操做(get、set、has等总共14个方法名称)的全局构造函数。它接受两个参数:目标对象与句柄对象。

var p = new Proxy(target, handler);

代理的行为很简单:将代理的全部内部方法转发到目标对象。而句柄对象是用来覆写任意代理的内部方法。

Reflect:反射

ES6的Reflect对象提供对任意对象进行某种特定的可拦截操做(interceptable operation)。Reflect对象提供14个与代理方法名字相同的方法,能够方便的管理对象。使用时直接经过Reflect.method()这样来调用。

Promises:

Promise表明某个将来才会结束的事件的结果,这一般是异步的。ES 
6提供Promise后,就能够将异步操做以同步操做的流程表达出来。Promise接受一个executor参数。executor带有resolve、reject参数,resolve失成功的回调函数,reject是失败的回调函数。

Promise对象是一个返回值的代理,这个返回值在promise对象建立时是未知的。

如图,Promise对象有:pending、fulfilled、rejected状态。pending状态能够转换成带成功值的fulfilled状态,也能够转换成带失败信息的rejected状态。当状态发生变化时,就会调用绑定在.then上的方法。

promise from mdn

建立一个Promise:

let p = new Promise(function(resolve, reject) { if (/* condition */) { resolve(/* value */); // fulfilled successfully } else { reject(/* reason */); // error, rejected } }); 

Promise的.then()方法接受两个参数:第一个函数当Promise成功(fulfilled)时调用,第二个函数当Promise失败(rejected)时掉用。

p.then((val) => console.log("fulfilled:", val), (err) => console.log("rejected: ", err)); 

上述代码等价于

p.then((val) => console.log("fulfilled:", val)) .catch((err) => console.log("rejected:", err)); 

Others:新增数值字面量、数据结构、库函数

ES6还有一些新增的特性,这些都是不对语言原有的内容进行冲突而加入的补充功能。

GraphQL

GraphQL是一种API查询语言,也是开发者定义数据的类型系统在服务器端的运行时。

GraphQL分为定义数据和查询交互过程。好比定义一个包含两个字段的User类型的GraphQL service,其提供数据结构和处理该类型各字段的函数。

type User{  
  id: ID
  name: String
}
function User_name(user){  
  return user.getName();
}

而查询的方式与json类型有点类似。

{
  user{
 name } } 

查询返回的数据能够以一个json对象的形式表达。

{
  "data": { "user": { "name": "Leo" } } } 

@medium

上图来自medium的文章。GraphQL的查询与Rest风格是不同的。Rest的数据是以资源为导向的,交互围绕着定位资源的路由(Route)进行;而GraphQL的模型与对象模型更加相似,模型是经过图的形式组织数据。相比Rest在客户端定义响应数据的结构,GraphQL灵活地将响应数据的结构交给了客户端。这样的好处是:客户端只须要一次请求就可以得到结构复杂的数据。

GraphQL有着本身的规范。依据官网给出的主要概念,规范文档主要分为查询操做和封装数据的类型系统两方面的内容。

Query and Mutation:查询和修改

查询和修改都是针对GraphQL服务器的查询操做。

Field:字段

GraphQL对数据对象的指定字段进行操做。

除了上一节的查询,还能够对内嵌对象、数组进行查询:

{
 user {  name  friends {  name  }  } } 

其json格式的结果以下:

{
    "data": { "user":{ "name": "Leo", "friends": { […] } } } } 

Arguments:查询参数

查询语法还支持传递参数,而且参数也是能够嵌套的。

{
 user(id: "1003"){  name  } } 

Aliases:别名

如同SQL的AS道别名功能同样,咱们能够对每个查询字段的:前面添上别名。

{
 Chinese: user(nation: "china"){  name  } } 

Fragments:片断

片断能够构造查询须要的字段,用分割复杂应用所需的数据来提升查询语句的复用程度。

{
 Chinese: user(nation: "china"){  ...comparisonFields  }  American: user(nation: "America"){  ...comparisonFields  } } fragment comparisonFields on User{  name  age  speaksLanguage } 

Variables:查询中的变量

为了动态传递参数,GraphQL提供了查询语言设置变量的功能,查询以字典的形式传递变量。

query UserNameAndFriends($age: Age) {  //变量定义: 变量以$前缀,后接类型  
  user(age: $age) {
 name  friends {  name  } } } { “age”: 26 } 

Directives:指令

在查询中标记字段的指令,能够改变查询的结构。好比下述这两种指令就能控制字段是否返回。

  • @include(if: Boolean) 条件为真时,只返回当前字段
  • @skip(if: Boolean) 条件为真时,过滤掉该字段

Mutations:修改数据

就像Rest以PUT/POST约定为修改服务器端数据同样,Mutations操做在GraphQL的意义就是修改数据库。就像官网中的例子:

mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) { //!表示必须填写的查询条件  
  createReview(episode: $ep, review: $review) {
 stars  commentary } } { "ep": "JEDI", "review": {  "stars": 5,  "commentary": "This is a great movie!" } } 

须要注意的是,为了保证mutation操做不冲突,mutation只能序列执行。而query能够并行。

Inline Fragments:内联片断

使用内联片断返回接口或者联合类型(interface、union)的数据。若是查询接口或者联合类型的字段,会返回其具体的类型。好比下方的例子,这个查询的fragment以 ... on Droid 标记,表示当Hero的Character是Droid类型时primaryFunction字段才会被执行。一样的,height字段只有在Human类型下才显示。

query HeroForEpisode($ep: Episode!) {  
  hero(episode: $ep) {
 name  ... on Droid {  primaryFunction  }  ... on Human {  height  } } } 

Meta fields:元字段

元字段用来描述查询中的各个字段。好比当Query查询__typename时,服务器端就会返回响应的数据类型。

Schema and Type:数据结构和类型

GraphQL有着本身的类型系统来描述被查询的数据。

Type system:类型系统

当接收到客户端发送的查询时,服务器毁从指定的‘root’对象开始,一层层选择查询字段。GraphQL的结合与返回结果相似,客户端经过schema能够预知服务器大概返回的结果。

Type language:类型语言

GraphQL不依赖特定的编程语言,自有一套GraphQL schema language,与大多数的查询语言相似。

Object types and fields:对象类型和字段

对象类型是GraphQL用来表示该对象结构的对象,其包含查询的目标字段。

Query and Mutation types:

这两个是特殊的类型。每个GraphQL必须有一个Query来指定查询处理。

Scalar types:默认标量类型

GraphQL对象类型有Int、Float、String、Boolean、ID这几种标量类型。

Enumeration types:枚举类型

枚举类型用来指定该类型的取值(可数的)。好比下列Nation类型只能取China、Japan、India这三个值。

enum Nation {  
  China
  Japan
  India
}

Lists:列表

GraphQL支持的数组类型。除了对象、标量、枚举类型这些类型外,还能够将字段定义为数组类型的数据,该字段可以内嵌包含标量的数组。

Interface types:接口类型

接口是一种抽象类型,能够指定实现接口时的类型字段。好比下列代码中的Character接口,和实现它的Human类型。Human类型除了实现接口必备的字段外,还有其特殊拥有的字段。

interface Character {  
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
}
type Human implements Character {  
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  starships: [Starship]
  totalCredits: Int
}

正如咱们上面所说,接口的查询须要借助内联片断来查询。

Union types:联合类型

联合类型与接口很是类似,不过其不须要指定公共字段。而是会把知足查询条件的全部union指定的数据组合在一个结果里。好比下列的SearchResult联合类型,就能够将不一样类型(Hunam | Droid | Starship)的数据对象以一个结果数组返回给客户端。

union SearchResult = Human | Droid | Starship

Input types:输入类型

除了传递标量数据,查询还能够传递复杂的对象。

input ReviewInput {  
  stars: Int!
  commentary: String
}

这样咱们在mutation时就能够传递一个对象ReviewInput做为查询条件。

mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {

Execution:执行

当被承认后,GraphQL查询就会被服务器执行并返回给客户端。GraphQL借助类型系统来执行查询,将每一个字段看成函数或者上个类型的方法。而这类方法就叫作resolver。当执行到一个字段,相应的函数resolver也会被执行。而咱们大多数的开发任务都将在这里完成。

resolver(obj, args, context)的三个参数分别表示:

  • obj: 前一个对象,root字段时这个参数为空
  • args: 查询条件参数
  • context: 上下文信息(好比用户信息、数据库连接)

若是resovler的执行是一种异步的方式(好比node中的数据库操做),GraphQL会等待Promises。

Introspection

该特性支持查询GraphQL Service提供查询的Schema信息。好比schema能够得到查询的数据结构,type能够得到字段的类型。

铺垫了这么多,下面开始动手编写GraphQL。首先,须要有一个支持ES6的node环境,而后搭建一个支持查询MongoDB数据库的Express with GraphQL。

Node with ES6

搭建Node环境版本为6.9.1,其能够经过--harmony参数运行带ES6特性的代码。可是Node不支持模块的导入导出(import)等特性,咱们仍是须要借助Babel库来将ES6的代码转换成兼容版本代码。

首先咱们将必要的包安装好。

{
  "dependencies": { "bluebird": "^3.4.6", //提供异步Promise的 "body-parser": "^1.15.2", //解析http请求主体 "express": "^4.14.0", //后端框架 "express-graphql": "^0.6.1", //封装上graphql的express "graphql": "^0.8.1", //GraphQL的node实现 "mongodb": "^2.2.11" //数据库驱动 }, "devDependencies": { "babel-core": "^6.18.2", //babel编译器 "babel-polyfill": "^6.16.0", //提供ES2015+的环境 "babel-preset-es2015": "^6.18.0", //提供全部2015包含的内容 "babel-preset-node6": "^11.0.0", //在node6.x的preset "babel-preset-stage-3": "^6.17.0", //提供stage-3 "babel-register": "^6.18.0" //babel require的钩子 } } 

上述 babel-preset-* 表示设定转码规则,咱们须要在.babelrc中添加这些规则。

{
  "presets": [ "es2015", "stage-3" ] } 

首先是入口文件,咱们使用babel-register将后续的require改写成使用Babel进行转码。

//index.js //require 'babel/register' to handle JavaScript code(successive 'require's will be babeled) require('babel-register') //rewrite require cmd with Babel transform require('./server.js') 

在写后续的代码(server.js)就可使用ES6的语法,首先是编写一个http服务器。

//server.js import express from 'express'; import schema from './schema.js'; import { graphql} from 'graphql'; import bodyParser from 'body-parser'; 

第一步是使用import引用依赖模块。

//server.js let app = express(); let PORT = 2333; // parse post content as text app.use(bodyParser.text({ type: 'application/graphql'})) app.use('/graphql', (req, res) => { //GraphQL executor graphql(schema, req.body) .then((result) => { res.send(JSON.stringify(result, null, 2)); }) }); 

而后就是配置一个GraphQL的Endpoint。将全部给/graphql路径的请求就交给GraphQL处理,而且请求的正文会被解析为'application/graphql'的文本。

let server = app.listen(PORT, function(){ let host = server.address().address; let port = server.address().port; console.log('GraphQL-api listening at http://%s:%s', host, port); }); 

最后就是启动服务器。

而GraphQL处理请求的schema来自schema.js文件。schema.js中定义了一个简单的schema,其包含一个query操做和一个mutation操做。

// schema.js import { GraphQLObjectType, GraphQLSchema, GraphQLInt, GraphQLString } from 'graphql'; // local variable to give client let count = 0; // return RootQueryType Object { field: count } let schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'RootQueryType', fields: { count: { type: GraphQLInt, description: 'Get count value', resolve: function(){ return count; } } } }), // Note: Mutation is serialization of change data query mutation: new GraphQLObjectType({ name: 'RootMutationType', fields: { updateCount: { type: GraphQLInt, description: 'Update the count', resolve: function(){ count += 1; return count; } } } }) }); export default schema; 

咱们打开命令行,敲入 curl -v -POST -H "Content-Type:application/graphql" -d 'query RootQueryType { count }' http://localhost:2333/graphql 就能够看到结果。

这样,咱们就完成基本GraphQL Service。

Express-GraphQL

上述内容虽然可以完成GraphQL Server基本任务,可是对于调试不太友好。GraphiQL是官方推荐的调试工具,而express-graphql就集成了GraphiQL。因此咱们用express-graphql重构下服务器代码。首先咱们将schema.js移到data目录下方便管理代码。而后用graphqlHTTP替换成处理/graphql路由的函数。

graphqlHTTP接受的参数:schema就是数据对象的schema,graphiql控制GraphiQL(debug通常开启)的提供,pretty参数控制json响应的形式,rootValue用来传递在整个graphql共享的变量,formatError参数来指定处理错误的方式。

//server.js import express from 'express'; import query_schema from './data/schema.js'; import graphqlHTTP from 'express-graphql'; import bodyParser from 'body-parser'; import { MongoClient } from 'mongodb'; import Promise from 'bluebird'; let app = express(); let PORT = 2333; app.use(bodyParser.json({ type: 'application/json' })) app.use('/graphql', graphqlHTTP(req =>({ schema: query_schema, graphiql: true, // debug work pretty: true, rootValue: { db: req.app.locals.db }, // pass db(mongodb) to graphql formatError: error => ({ // return error message: error.message, locations: error.locations, stack: error.stack }) }))); 

在rootValue传递来一个express内置对象req的成员变量,在这个应用里是数据库链接客户端。这个客户端的定义以下。使用MongoClient链接本地数据库,第二个参数中的promiseLibrary用来指定异步处理的库,这里选用的是Bluebird的Promise对象。当app.locals.db的引用变量被指定为成功链接数据库的句柄后,就能够发布GraphQL service了。

MongoClient.connect('mongodb://localhost:27017/atm_analysis', { promiseLibrary: Promise }) .catch(err => console.error(err.stack)) .then(db => { app.locals.db = db; let server = app.listen(PORT, function () { let host = server.address().address; let port = server.address().port; console.log('GraphQL-api listening at http://%s:%s', host, port); // ipv6 is :: }); }); 

接下来看看,schema应该怎么写。

首先是外层的GraphQL Schema对象,里头包含里一个查询。这个对象还内嵌了一个GraphQL Object类型的对象。对于这个内嵌对象,咱们在resolve函数上进行数据库查询操做(node对于直接返回标量数据的resolver,会忽略resolver执行直接得到数据,这样能够加快响应速度)。

let NetnodeType = new GraphQLObjectType({ name: 'netnode', fields: { id: { type: GraphQLID }, net_node_name: { type: GraphQLString }, customer_name: { type: GraphQLString } } }); // create instance of 'GraphQLSchema' let schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'NetNodeInfo', //object description: 'get netnode geograph infomation about fault, alarm', fields: { test: { type: GraphQLString, description: 'test info string', resolve: function () { return 'test graphql'; } }, node: { type: new GraphQLList(NetnodeType), description: 'netnode info', async resolve({ db }, args) { let data = await db.collection('dbo.TBL_NETNODE_INFO').find().limit(1500).sort({ 'ID': 1 }).toArray(); return data.map(x => ({ id: x.ID, net_node_name: x.net_node_name, customer_name: x.customer_name })); } } } }) }); 

注意,这里必定要引入babel-polyfill库,否则会因为node没有彻底支持async的相关特性,async函数的regenerator功能报错。

对于客户端的测试请求,咱们能够先使用GraphiQL工具来操做。在浏览器敲入地址:http://localhost:2333/graphql

首先测试GraphQL的query,咱们对NetNodeInfo的test字段进行查询。

test query

咱们看到返回的data中有对应的数据,证实GraphQL Service正常运行。

而后测试对于数据库操做的字段,咱们对NetNodeInfo的node字段进行查询。经过GraphiQL上右侧的自建文档能够看到,这个字段内部的对象有3个字段。下图的查询结果是只对"id"和"netnodename"字段查询的状况,返回的数据就不会包括没有请求的字段(没有"customer_name"字段)。

test node

GraphQL的这种灵活的接口可以下降对于复杂结构数据的请求数量,进而减小网络通讯;而接口的自洽(自动生成接口文档)能够帮助先后端开发者的沟通,从而提升开发效率。

相关文章
相关标签/搜索